Initial Commit
Initial commit of Code Base.
This commit is contained in:
parent
293b1213e1
commit
c11a4ebbc2
653 changed files with 36893 additions and 1 deletions
606
addons/twitcher/editor/api_generator/twitch_api_generator.gd
Normal file
606
addons/twitcher/editor/api_generator/twitch_api_generator.gd
Normal file
|
|
@ -0,0 +1,606 @@
|
|||
@icon("res://addons/twitcher/assets/api-icon.svg")
|
||||
@tool
|
||||
extends Twitcher
|
||||
|
||||
class_name TwitchAPIGenerator
|
||||
|
||||
const suffixes: Array[String] = ["Response", "Body", "Opt"]
|
||||
|
||||
const api_output_path = "res://addons/twitcher/generated/"
|
||||
const twitch_api_header : String = """@tool
|
||||
extends Twitcher
|
||||
|
||||
# CLASS GOT AUTOGENERATED DON'T CHANGE MANUALLY. CHANGES CAN BE OVERWRITTEN EASILY.
|
||||
|
||||
## Interaction with the Twitch REST API.
|
||||
class_name TwitchAPI
|
||||
|
||||
static var _log: TwitchLogger = TwitchLogger.new("TwitchAPI")
|
||||
|
||||
static var instance: TwitchAPI
|
||||
|
||||
## Maximal tries to reauthrorize before giving up the request.
|
||||
const MAX_AUTH_ERRORS = 3
|
||||
|
||||
## Called when the API returns unauthenticated mostly cause the accesstoken is expired
|
||||
signal unauthenticated
|
||||
|
||||
## Called when the API returns 403 means there are permissions / scopes missing
|
||||
signal unauthorized
|
||||
|
||||
## To authorize against the Twitch API
|
||||
@export var token: OAuthToken:
|
||||
set(val):
|
||||
token = val
|
||||
update_configuration_warnings()
|
||||
## OAuth settings needed for client information
|
||||
@export var oauth_setting: OAuthSetting:
|
||||
set(val):
|
||||
oauth_setting = val
|
||||
update_configuration_warnings()
|
||||
## URI to the Twitch API
|
||||
@export var api_host: String = "https://api.twitch.tv/helix"
|
||||
|
||||
## Client to make HTTP requests
|
||||
var client: BufferedHTTPClient
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
client = BufferedHTTPClient.new()
|
||||
client.name = "ApiClient"
|
||||
add_child(client)
|
||||
|
||||
|
||||
func _enter_tree() -> void:
|
||||
if instance == null: instance = self
|
||||
|
||||
|
||||
func _exit_tree() -> void:
|
||||
if instance == self: instance = null
|
||||
|
||||
|
||||
func _get_configuration_warnings() -> PackedStringArray:
|
||||
var result: PackedStringArray = []
|
||||
if token == null:
|
||||
result.append("Please set a token to use")
|
||||
if oauth_setting == null:
|
||||
result.append("Please set the correct oauth settings")
|
||||
return result
|
||||
|
||||
|
||||
func request(path: String, method: int, body: Variant = "", content_type: String = "", error_count: int = 0) -> BufferedHTTPClient.ResponseData:
|
||||
var header : Dictionary = {
|
||||
"Authorization": "Bearer %s" % [await token.get_access_token()],
|
||||
"Client-ID": oauth_setting.client_id
|
||||
}
|
||||
if content_type != "":
|
||||
header["Content-Type"] = content_type
|
||||
|
||||
var request_body: String = ""
|
||||
if body == null || (body is String && body == ""):
|
||||
request_body = ""
|
||||
elif body is Object && body.has_method("to_json"):
|
||||
request_body = body.to_json()
|
||||
else:
|
||||
request_body = JSON.stringify(body)
|
||||
|
||||
var req: BufferedHTTPClient.RequestData = client.request(api_host + path, method, header, request_body)
|
||||
var res: BufferedHTTPClient.ResponseData = await client.wait_for_request(req)
|
||||
|
||||
# Try to fix Godot TLS Bug
|
||||
if res.result == 5:
|
||||
return await retry(req, res, path, method, body, content_type, error_count + 1)
|
||||
|
||||
match res.response_code:
|
||||
400:
|
||||
var error_message: String = res.response_data.get_string_from_utf8()
|
||||
_log.e("'%s' failed cause of: \\n%s" % [path, error_message])
|
||||
401: # Token expired / or missing permissions
|
||||
_log.e("'%s' is unauthorized. It is probably your scopes." % path)
|
||||
unauthorized.emit()
|
||||
403:
|
||||
_log.i("'%s' is unauthenticated. Refresh token." % path)
|
||||
unauthenticated.emit()
|
||||
await token.authorized
|
||||
return await retry(req, res, path, method, body, content_type, error_count + 1)
|
||||
return res
|
||||
|
||||
|
||||
func retry(request: BufferedHTTPClient.RequestData,
|
||||
response: BufferedHTTPClient.ResponseData,
|
||||
path: String,
|
||||
method: int,
|
||||
body: Variant = "",
|
||||
content_type: String = "",
|
||||
error_count: int = 0) -> BufferedHTTPClient.ResponseData:
|
||||
if error_count + 1 < MAX_AUTH_ERRORS:
|
||||
return await request(path, method, body, content_type, error_count + 1)
|
||||
else:
|
||||
# Give up the request after trying multiple times and
|
||||
# return an empty response with correct error code
|
||||
var empty_response: BufferedHTTPClient.ResponseData = client.empty_response(request)
|
||||
empty_response.response_code = response.response_code
|
||||
return empty_response
|
||||
|
||||
|
||||
## Converts unix timestamp to RFC 3339 (example: 2021-10-27T00:00:00Z) when passed a string uses as is
|
||||
static func get_rfc_3339_date_format(time: Variant) -> String:
|
||||
if typeof(time) == TYPE_INT:
|
||||
var date_time = Time.get_datetime_dict_from_unix_time(time)
|
||||
return "%s-%02d-%02dT%02d:%02d:%02dZ" % [date_time['year'], date_time['month'], date_time['day'], date_time['hour'], date_time['minute'], date_time['second']]
|
||||
return str(time)
|
||||
|
||||
"""
|
||||
|
||||
@export var parser: TwitchAPIParser
|
||||
|
||||
var grouped_files: Dictionary[String, Variant] = {}
|
||||
|
||||
|
||||
func prepare_component(component: TwitchGenComponent) -> void:
|
||||
if component._is_root:
|
||||
var base_name = get_base_name(component._classname)
|
||||
|
||||
# No suffix class lives by its own
|
||||
if base_name == component._classname:
|
||||
if grouped_files.has(base_name):
|
||||
push_error("That file shouldn't exist: %s" % base_name)
|
||||
component._classname = "Twitch" + component._classname
|
||||
grouped_files[base_name] = component
|
||||
else:
|
||||
var file: GroupedComponent = grouped_files.get(base_name, GroupedComponent.new())
|
||||
file.base_name = "Twitch" + base_name
|
||||
file.components.append(component)
|
||||
grouped_files[base_name] = file
|
||||
component._classname = component._classname.trim_prefix(base_name)
|
||||
component.set_meta("fqdn", file.base_name + "." + component._classname)
|
||||
var sub_components_to_update: Array[TwitchGenComponent] = component._sub_components.values().duplicate()
|
||||
for sub_component in sub_components_to_update:
|
||||
sub_component._classname = component._classname + sub_component._classname
|
||||
sub_components_to_update.append_array(sub_component._sub_components.values())
|
||||
pass
|
||||
|
||||
|
||||
|
||||
func generate_api() -> void:
|
||||
for component: Variant in parser.components:
|
||||
prepare_component(component)
|
||||
|
||||
# Generate TwitchAPI
|
||||
var twitch_api_code = twitch_api_header
|
||||
for method: TwitchGenMethod in parser.methods:
|
||||
twitch_api_code += method_code(method)
|
||||
write_output_file(api_output_path + "twitch_api.gd", twitch_api_code)
|
||||
|
||||
# Generate Components
|
||||
for component: Variant in grouped_files.values():
|
||||
var code = ""
|
||||
if component is GroupedComponent:
|
||||
code = group_code(component)
|
||||
else:
|
||||
code = component_code(component, 0)
|
||||
write_output_file(api_output_path + component.get_filename(), code)
|
||||
|
||||
print("API regenerated you can find it under: %s" % api_output_path)
|
||||
|
||||
|
||||
class GroupedComponent extends RefCounted:
|
||||
var base_name: String
|
||||
var prefix: String
|
||||
var components: Array[TwitchGenComponent] = []
|
||||
|
||||
|
||||
func _update_base_name(val: String) -> void:
|
||||
base_name = val
|
||||
|
||||
|
||||
func get_filename() -> String:
|
||||
return base_name.to_snake_case() + ".gd"
|
||||
|
||||
|
||||
func group_code(group: GroupedComponent) -> String:
|
||||
var code = """@tool
|
||||
extends TwitchData
|
||||
|
||||
# CLASS GOT AUTOGENERATED DON'T CHANGE MANUALLY. CHANGES CAN BE OVERWRITTEN EASILY.
|
||||
|
||||
class_name {name}
|
||||
""".format({"name": group.base_name})
|
||||
for component in group.components:
|
||||
component._is_root = false
|
||||
code += "\n\n"
|
||||
code += component_code(component, 1)
|
||||
return code
|
||||
|
||||
#region Field Code Generation
|
||||
|
||||
func field_declaration(field: TwitchGenField) -> String:
|
||||
var type = get_type(field._type, field._is_array)
|
||||
return """
|
||||
## {description}
|
||||
@export var {name}: {type}:
|
||||
set(val):
|
||||
{name} = val
|
||||
track_data(&"{name}", val)\n""".format({
|
||||
"name": field._name,
|
||||
"description": ident(field._description, 0, "## "),
|
||||
"type": type
|
||||
})
|
||||
|
||||
|
||||
#endregion
|
||||
|
||||
#region Parameter Code Generation
|
||||
|
||||
#func get_code() -> String:
|
||||
#if _name == "broadcaster_id":
|
||||
#var default_value = "default_broadcaster_login" if _type == "String" else "[default_broadcaster_login]"
|
||||
#return "%s: %s = %s" % [_name, get_type(), default_value]
|
||||
#return "%s: %s" % [_name, get_type()]
|
||||
|
||||
#endregion
|
||||
|
||||
#region Method Code Generation
|
||||
|
||||
func parameter_doc(method: TwitchGenMethod) -> String:
|
||||
if method._required_parameters.is_empty():
|
||||
return "## [no required query parameters to describe]"
|
||||
var doc : String = ""
|
||||
for parameter: TwitchGenParameter in method._required_parameters:
|
||||
doc += "## {name} - {documentation} \n".format({
|
||||
'name': parameter._name,
|
||||
'documentation': ident(parameter._description, 0, "## ")
|
||||
})
|
||||
return doc.rstrip("\n")
|
||||
|
||||
|
||||
func parameter_array(method: TwitchGenMethod, with_type: bool = false, fully_qualified: bool = false) -> Array[String]:
|
||||
var parameters : Array[String] = []
|
||||
if method._contains_body: parameters.append(get_parameter("body", method._body_type, false, with_type, fully_qualified))
|
||||
if method._contains_optional: parameters.append(get_parameter("opt", method.get_optional_type(), false, with_type, fully_qualified))
|
||||
|
||||
method._parameters.sort_custom(TwitchGenParameter.sort)
|
||||
for parameter: TwitchGenParameter in method._required_parameters:
|
||||
parameters.append(get_parameter(parameter._name, parameter._type, parameter._is_array, with_type, fully_qualified))
|
||||
return parameters
|
||||
|
||||
|
||||
func method_parameter(method: TwitchGenMethod, with_type: bool = false, fully_qualified: bool = false) -> String:
|
||||
return ", ".join(parameter_array(method, with_type, fully_qualified))
|
||||
|
||||
|
||||
func path_code(method: TwitchGenMethod) -> String:
|
||||
var body_code : String = "var path = \"%s?\"\n" % method._path
|
||||
|
||||
if method._contains_optional:
|
||||
body_code += "var optionals: Dictionary[StringName, Variant] = {}\n"
|
||||
body_code += "if opt != null: optionals = opt.to_dict()\n"
|
||||
|
||||
for parameter: TwitchGenParameter in method._parameters:
|
||||
if parameter._required:
|
||||
body_code += parameter_path_code(parameter) + "\n"
|
||||
else:
|
||||
body_code += "if optionals.has(\"%s\"):\n" % parameter._name
|
||||
body_code += "\t%s\n" % ident(parameter_path_code(parameter, "optionals."), 1)
|
||||
return body_code
|
||||
|
||||
|
||||
func parameter_path_code(parameter: TwitchGenParameter, prefix: String = "") -> String:
|
||||
var body: String
|
||||
if parameter._is_time:
|
||||
body = "path += \"{key}=\" + get_rfc_3339_date_format({value}) + \"&\""
|
||||
|
||||
elif parameter._is_array:
|
||||
body = """
|
||||
for param in {value}:
|
||||
path += "{key}=" + str(param) + "&" """.trim_prefix("\n\t")
|
||||
else:
|
||||
body = "path += \"{key}=\" + str({value}) + \"&\""
|
||||
|
||||
return body.format({
|
||||
'value': prefix + parameter._name,
|
||||
'key': parameter._name
|
||||
})
|
||||
|
||||
## Exceptional method cause twitch api is not uniform
|
||||
func paging_code_stream_schedule() -> String:
|
||||
return """
|
||||
if parsed_result.data.pagination != null:
|
||||
opt.after = parsed_result.data.pagination.cursor
|
||||
parsed_result.data._next_page = get_channel_stream_schedule.bind(opt, broadcaster_id)\n"""
|
||||
|
||||
|
||||
func paging_code(method: TwitchGenMethod) -> String:
|
||||
if method._name == "get_channel_stream_schedule":
|
||||
return paging_code_stream_schedule()
|
||||
|
||||
var code: String = ""
|
||||
code += "if parsed_result.pagination != null:\n"
|
||||
var after_parameter: TwitchGenParameter = method.get_parameter_by_name("after")
|
||||
var result_component: TwitchGenComponent = get_component(method._result_type)
|
||||
var pagination_parameter: TwitchGenField = result_component.get_field_by_name("pagination")
|
||||
if pagination_parameter == null:
|
||||
print("Check %s paging without paging?" % method._name)
|
||||
pass
|
||||
elif pagination_parameter._type == "String":
|
||||
code += "\tvar cursor: String = parsed_result.pagination\n"
|
||||
else:
|
||||
code += "\tvar cursor: String = parsed_result.pagination.cursor\n"
|
||||
if after_parameter._required:
|
||||
code += "\t{parameter} = cursor\n"
|
||||
else:
|
||||
code += "\topt.{parameter} = cursor\n"
|
||||
code += "\tparsed_result._next_page = {name}.bind({parameters})\n"
|
||||
|
||||
return code.format({
|
||||
"parameter": after_parameter._name,
|
||||
"name": method._name,
|
||||
"parameters": method_parameter(method)
|
||||
})
|
||||
|
||||
|
||||
func response_code(method: TwitchGenMethod) -> String:
|
||||
var code: String = ""
|
||||
var result_type = get_type(method._result_type, false, true)
|
||||
if result_type != "BufferedHTTPClient.ResponseData":
|
||||
code = """
|
||||
var result: Variant = JSON.parse_string(response.response_data.get_string_from_utf8())
|
||||
var parsed_result: {result_type} = {result_type}.from_json(result)
|
||||
parsed_result.response = response
|
||||
""".format({ 'result_type': result_type })
|
||||
if method._has_paging: code += paging_code(method)
|
||||
code += "return parsed_result"
|
||||
else:
|
||||
code = "return response"
|
||||
return code
|
||||
|
||||
|
||||
func method_code(method: TwitchGenMethod) -> String:
|
||||
return """
|
||||
|
||||
## {summary}
|
||||
##
|
||||
{parameter_doc}
|
||||
##
|
||||
## {doc_url}
|
||||
func {name}({parameters}) -> {result_type}:
|
||||
{path_code}
|
||||
var response: BufferedHTTPClient.ResponseData = await request(path, HTTPClient.METHOD_{method}, {body_variable}, "{content_type}")
|
||||
{response_code}
|
||||
""".format({
|
||||
"summary": method._summary,
|
||||
"parameter_doc": parameter_doc(method),
|
||||
"doc_url": method._doc_url,
|
||||
"name": method._name,
|
||||
"parameters": method_parameter(method, true, true),
|
||||
"result_type": get_type(method._result_type, false, true),
|
||||
"path_code": ident(path_code(method), 1),
|
||||
"content_type": get_type(method._content_type, false, true),
|
||||
"method": method._http_verb.to_upper(),
|
||||
"body_variable": "body" if method._contains_body else "\"\"",
|
||||
"response_code": ident(response_code(method), 1),
|
||||
})
|
||||
|
||||
|
||||
#endregion
|
||||
|
||||
#region Component Code Generation
|
||||
|
||||
|
||||
func component_code(component: TwitchGenComponent, level: int = 0) -> String:
|
||||
var code: String = ""
|
||||
if component._is_root:
|
||||
code += """@tool
|
||||
extends TwitchData
|
||||
|
||||
# CLASS GOT AUTOGENERATED DON'T CHANGE MANUALLY. CHANGES CAN BE OVERWRITTEN EASILY.
|
||||
|
||||
## {description}
|
||||
## {ref}
|
||||
class_name {classname}
|
||||
"""
|
||||
else:
|
||||
code = """
|
||||
## {description}
|
||||
## {ref}
|
||||
class {classname} extends TwitchData:
|
||||
"""
|
||||
var class_code : String = ""
|
||||
for field: TwitchGenField in component._fields:
|
||||
class_code += field_declaration(field)
|
||||
|
||||
if component._is_response:
|
||||
class_code += "var response: BufferedHTTPClient.ResponseData"
|
||||
class_code += "\n\n"
|
||||
class_code += create_code(component) + "\n\n"
|
||||
class_code += from_json_code(component)
|
||||
|
||||
if component._has_paging:
|
||||
class_code += "\n\n" + iter_code(component)
|
||||
|
||||
var sub_component_code: String
|
||||
for sub_component in component._sub_components.values():
|
||||
sub_component_code += "\n\n" + component_code(sub_component, 1)
|
||||
|
||||
return code.format({
|
||||
"description": ident(component._description, 0, "## "),
|
||||
"classname": component._classname,
|
||||
"ref": component._ref
|
||||
}) + ident(class_code, level) + sub_component_code
|
||||
|
||||
|
||||
func create_code(component: TwitchGenComponent) -> String:
|
||||
var parameters: Array[String] = []
|
||||
for field in component._fields:
|
||||
if field._is_required:
|
||||
parameters.append("_" + get_parameter(field._name, field._type, field._is_array))
|
||||
|
||||
var variable_name = component._classname.to_snake_case()
|
||||
var code : String = """
|
||||
## Constructor with all required fields.
|
||||
static func create({parameters}) -> {classname}:
|
||||
var {variablename}: {classname} = {classname}.new()\n""".format({
|
||||
"parameters": ", ".join(parameters),
|
||||
"classname": component._classname,
|
||||
"variablename": variable_name
|
||||
})
|
||||
|
||||
for field in component._fields:
|
||||
if field._is_required:
|
||||
code += "\t{classname}.{name} = _{name}\n".format({
|
||||
"name": field._name,
|
||||
"classname": variable_name
|
||||
})
|
||||
|
||||
code += "\treturn %s" % variable_name
|
||||
return code
|
||||
|
||||
|
||||
func from_json_code(component: TwitchGenComponent) -> String:
|
||||
var code : String = """
|
||||
static func from_json(d: Dictionary) -> {classname}:
|
||||
var result: {classname} = {classname}.new()
|
||||
""".format({"classname": component._classname})
|
||||
for field: TwitchGenField in component._fields:
|
||||
code += "\tif d.get(\"{name}\", null) != null:\n"
|
||||
if field._is_typed_array:
|
||||
code += """
|
||||
for value in d["{name}"]:
|
||||
result.{name}.append({type}.from_json(value))\n""".lstrip("\n")
|
||||
elif field._is_array:
|
||||
code += """
|
||||
for value in d["{name}"]:
|
||||
result.{name}.append(value)\n""".lstrip("\n")
|
||||
elif field._is_sub_class:
|
||||
code += "\t\tresult.{name} = {type}.from_json(d[\"{name}\"])\n"
|
||||
else:
|
||||
code += "\t\tresult.{name} = d[\"{name}\"]\n"
|
||||
code = code.format({
|
||||
"name": field._name,
|
||||
"type": get_type(field._type, false)
|
||||
})
|
||||
code += "\treturn result\n"
|
||||
|
||||
return code
|
||||
|
||||
|
||||
func iter_code(component: TwitchGenComponent) -> String:
|
||||
var data_variable_name: String = "data"
|
||||
var path_to_data: String = ""
|
||||
if component._ref == "#/components/schemas/GetChannelStreamScheduleResponse/Data":
|
||||
data_variable_name = "segments"
|
||||
path_to_data = "data."
|
||||
|
||||
var code: String
|
||||
if component._ref == "#/components/schemas/GetExtensionLiveChannelsResponse":
|
||||
code += """
|
||||
func _has_pagination() -> bool:
|
||||
if pagination == null || pagination == "": return false
|
||||
return true
|
||||
"""
|
||||
else:
|
||||
code += """
|
||||
func _has_pagination() -> bool:
|
||||
if pagination == null: return false
|
||||
if pagination.cursor == null || pagination.cursor == "": return false
|
||||
return true
|
||||
"""
|
||||
|
||||
code += """
|
||||
var _next_page: Callable
|
||||
var _cur_iter: int = 0
|
||||
|
||||
|
||||
func next_page() -> {response_type}:
|
||||
var response: {response_type} = await _next_page.call()
|
||||
_cur_iter = 0
|
||||
_next_page = response.{path_to_data}_next_page
|
||||
{copy_code}
|
||||
return response
|
||||
|
||||
|
||||
func _iter_init(iter: Array) -> bool:
|
||||
if {data_variable_name}.is_empty(): return false
|
||||
iter[0] = {data_variable_name}[0]
|
||||
return {data_variable_name}.size() > 0
|
||||
|
||||
|
||||
func _iter_next(iter: Array) -> bool:
|
||||
if {data_variable_name}.size() - 1 > _cur_iter:
|
||||
_cur_iter += 1
|
||||
iter[0] = {data_variable_name}[_cur_iter]
|
||||
if {data_variable_name}.size() - 1 == _cur_iter && not _has_pagination():
|
||||
return false
|
||||
return true
|
||||
|
||||
|
||||
func _iter_get(iter: Variant) -> Variant:
|
||||
if {data_variable_name}.size() - 1 == _cur_iter && _has_pagination():
|
||||
await next_page()
|
||||
return iter"""
|
||||
var copy_code: String
|
||||
for field in component._fields:
|
||||
copy_code += "\t{_name} = response.{path_to_data}{_name}\n".format(field)
|
||||
|
||||
return code.format({
|
||||
"data_variable_name": data_variable_name,
|
||||
"copy_code": copy_code,
|
||||
"path_to_data": path_to_data,
|
||||
"response_type": component.get_root_classname()
|
||||
})
|
||||
#endregion
|
||||
|
||||
#region Utils
|
||||
|
||||
|
||||
func get_type(type: String, is_array: bool = false, full_qualified: bool = false) -> String:
|
||||
var result_type : String = ""
|
||||
if type.begins_with("#"):
|
||||
var component: TwitchGenComponent = parser.get_component_by_ref(type)
|
||||
result_type = component._classname
|
||||
if full_qualified and component.has_meta("fqdn"):
|
||||
result_type = component.get_meta("fqdn")
|
||||
else:
|
||||
result_type = type
|
||||
return result_type if not is_array else "Array[%s]" % result_type
|
||||
|
||||
|
||||
func get_component(type: String) -> TwitchGenComponent:
|
||||
if type.begins_with("#"):
|
||||
return parser.get_component_by_ref(type)
|
||||
else:
|
||||
return null
|
||||
|
||||
func ident(code: String, level: int, padding: String = "") -> String:
|
||||
return code.replace("\n", "\n" + "\t".repeat(level) + padding)
|
||||
|
||||
|
||||
# Writes the processed content to the output file.
|
||||
func write_output_file(file_output: String, content: String) -> void:
|
||||
var file = FileAccess.open(file_output, FileAccess.WRITE);
|
||||
if file == null:
|
||||
var error_message = error_string(FileAccess.get_open_error());
|
||||
push_error("Failed to open output file: %s\n%s" % [file_output, error_message])
|
||||
return
|
||||
file.store_string(content)
|
||||
file.flush()
|
||||
file.close()
|
||||
|
||||
|
||||
func get_base_name(file: String) -> String:
|
||||
var new_file: String = file
|
||||
for suffix: String in suffixes:
|
||||
new_file = new_file.trim_suffix(suffix)
|
||||
return new_file
|
||||
|
||||
|
||||
func get_parameter(title: String, type: String, is_array = false, with_type: bool = true, fully_qualified: bool = false) -> String:
|
||||
if with_type:
|
||||
return "{name}: {type}".format({
|
||||
"name": title,
|
||||
"type": get_type(type, is_array, fully_qualified)
|
||||
})
|
||||
else:
|
||||
return title
|
||||
|
||||
#endregion
|
||||
|
|
@ -0,0 +1 @@
|
|||
uid://cetl2un34bjb1
|
||||
247
addons/twitcher/editor/api_generator/twitch_api_parser.gd
Normal file
247
addons/twitcher/editor/api_generator/twitch_api_parser.gd
Normal file
|
|
@ -0,0 +1,247 @@
|
|||
@icon("res://addons/twitcher/assets/api-icon.svg")
|
||||
@tool
|
||||
extends Twitcher
|
||||
|
||||
class_name TwitchAPIParser
|
||||
|
||||
const SWAGGER_API = "https://raw.githubusercontent.com/DmitryScaletta/twitch-api-swagger/refs/heads/main/openapi.json"
|
||||
|
||||
var definition: Dictionary = {}
|
||||
var component_map: Dictionary[String, TwitchGenComponent] = {}
|
||||
var components: Array[TwitchGenComponent] = []
|
||||
var methods: Array[TwitchGenMethod] = []
|
||||
|
||||
var client: BufferedHTTPClient = BufferedHTTPClient.new()
|
||||
|
||||
signal component_added(component: TwitchGenComponent)
|
||||
signal method_added(method: TwitchGenMethod)
|
||||
|
||||
|
||||
class ComponentRepo extends RefCounted:
|
||||
var _component: TwitchGenComponent
|
||||
var _component_map: Dictionary[String, TwitchGenComponent]
|
||||
|
||||
|
||||
func get_comp(component_name: String) -> TwitchGenComponent:
|
||||
var component = _component.get_component(component_name)
|
||||
if component != null: return component
|
||||
return _component_map.get(component_name)
|
||||
|
||||
|
||||
func _init(component: TwitchGenComponent, component_map: Dictionary[String, TwitchGenComponent]) -> void:
|
||||
_component = component
|
||||
_component_map = component_map
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
client.name = "APIGeneratorClient"
|
||||
|
||||
|
||||
func parse_api() -> void:
|
||||
print("start generating API")
|
||||
if definition == {}:
|
||||
print("load Twitch definition")
|
||||
definition = await _load_swagger_definition()
|
||||
|
||||
_parsing_components()
|
||||
_parsing_paths()
|
||||
|
||||
|
||||
func _load_swagger_definition() -> Dictionary:
|
||||
add_child(client)
|
||||
client.max_error_count = 3
|
||||
var request = client.request(SWAGGER_API, HTTPClient.METHOD_GET, {}, "")
|
||||
var response_data = await client.wait_for_request(request)
|
||||
|
||||
if response_data.error:
|
||||
printerr("Cant generate API")
|
||||
return {}
|
||||
var response_str = response_data.response_data.get_string_from_utf8()
|
||||
var response = JSON.parse_string(response_str)
|
||||
remove_child(client)
|
||||
return response
|
||||
|
||||
|
||||
func _parsing_components() -> void:
|
||||
var schemas = definition["components"]["schemas"]
|
||||
for schema_name in schemas:
|
||||
var schema: Dictionary = schemas[schema_name]
|
||||
if schema["type"] != "object":
|
||||
printerr("Not an object")
|
||||
continue
|
||||
|
||||
var ref = "#/components/schemas/" + schema_name
|
||||
var component = TwitchGenComponent.new(schema_name, ref)
|
||||
component._is_root = true
|
||||
component._is_response = true
|
||||
_parse_properties(component, schema)
|
||||
_add_component(ref, component)
|
||||
|
||||
|
||||
func _parse_properties(component: TwitchGenComponent, schema: Dictionary) -> void:
|
||||
var properties = schema["properties"]
|
||||
for property_name: String in properties:
|
||||
var property: Dictionary = properties[property_name]
|
||||
var field: TwitchGenField = TwitchGenField.new()
|
||||
field._name = property_name
|
||||
field._description = property.get("description", "")
|
||||
field._type = _get_param_type(property)
|
||||
|
||||
var classname: String = property_name.capitalize().replace(" ", "")
|
||||
|
||||
|
||||
if property.has("properties"):
|
||||
var sub_component = _add_sub_component(classname, field._description, component, property)
|
||||
field._type = sub_component._ref
|
||||
|
||||
## Arrays that has custom types
|
||||
elif property.get("type", "") == "array":
|
||||
field._is_array = true
|
||||
field._is_sub_class = false
|
||||
var items = property.get("items", {})
|
||||
if items.has("$ref"):
|
||||
field._type = items.get("$ref")
|
||||
elif items.has("properties"):
|
||||
var sub_component = _add_sub_component(classname, field._description, component, items)
|
||||
field._type = sub_component._ref
|
||||
|
||||
component.add_field(field)
|
||||
|
||||
var requires: Array = schema.get("required", [])
|
||||
for required_field: String in requires:
|
||||
var field: TwitchGenField = component.get_field_by_name(required_field)
|
||||
field._is_required = true
|
||||
|
||||
|
||||
func _add_sub_component(classname: String, description: String, parent_component: TwitchGenComponent, properties: Dictionary) -> TwitchGenComponent:
|
||||
var ref: String = parent_component._ref + "/" + classname
|
||||
var sub_component = TwitchGenComponent.new(classname, ref)
|
||||
sub_component._description = description
|
||||
_parse_properties(sub_component, properties)
|
||||
parent_component.add_component(sub_component)
|
||||
_add_component(ref, sub_component)
|
||||
return sub_component
|
||||
|
||||
|
||||
func _parsing_paths() -> void:
|
||||
var paths = definition.get("paths", {})
|
||||
for path in paths:
|
||||
var method_specs = paths[path]
|
||||
for http_verb: String in method_specs:
|
||||
var method_spec = method_specs[http_verb] as Dictionary
|
||||
var method = _parse_method(http_verb, method_spec)
|
||||
method._path = path
|
||||
if method._contains_optional:
|
||||
var component : TwitchGenComponent = method.get_optional_component()
|
||||
_add_component(component._ref, component)
|
||||
methods.append(method)
|
||||
method_added.emit(method)
|
||||
|
||||
|
||||
func _parse_method(http_verb: String, method_spec: Dictionary) -> TwitchGenMethod:
|
||||
var method: TwitchGenMethod = TwitchGenMethod.new()
|
||||
method._http_verb = http_verb
|
||||
method._name = method_spec.get("operationId", "method_" + http_verb).replace("-", "_")
|
||||
method._summary = method_spec.get("summary", "No summary provided.")
|
||||
method._description = method_spec.get("description", "No description provided.")
|
||||
method._doc_url = method_spec.get("externalDocs", {}).get("url", "No link provided")
|
||||
_parse_parameters(method, method_spec)
|
||||
|
||||
# Body Type
|
||||
if method_spec.has("requestBody"):
|
||||
method._body_type = "Dictionary"
|
||||
var ref = method_spec.get("requestBody").get("content", {}).get("application/json", {}).get("schema", {}).get("$ref", "")
|
||||
if ref != "":
|
||||
method._body_type = ref
|
||||
|
||||
# Result Type
|
||||
method._result_type = "BufferedHTTPClient.ResponseData"
|
||||
var responses = method_spec.get("responses", {})
|
||||
if responses.has("200") || responses.has("202"):
|
||||
var content: Dictionary = {}
|
||||
if responses.has("200"):
|
||||
content = responses["200"].get("content", {})
|
||||
elif content == {}:
|
||||
content = responses["202"].get("content", {})
|
||||
|
||||
# Assuming the successful response is a JSON object
|
||||
method._result_type = "Dictionary"
|
||||
|
||||
# Special case for /schedule/icalendar
|
||||
if content.has("text/calendar"):
|
||||
method._result_type = "BufferedHTTPClient.ResponseData"
|
||||
|
||||
|
||||
# Try to resolve the component references
|
||||
var ref = content.get("application/json", {}).get("schema", {}).get("$ref", "")
|
||||
if ref != "":
|
||||
method._result_type = ref
|
||||
|
||||
# Content Type
|
||||
if method_spec.has("requestBody"):
|
||||
var requestBody = method_spec.get("requestBody")
|
||||
var content = requestBody.get("content")
|
||||
method._content_type = content.keys()[0]
|
||||
elif http_verb == "POST":
|
||||
method._content_type = "application/x-www-form-urlencoded"
|
||||
return method
|
||||
|
||||
|
||||
func _parse_parameters(method: TwitchGenMethod, method_spec: Dictionary) -> void:
|
||||
var parameter_specs = method_spec.get("parameters", [])
|
||||
for parameter_spec in parameter_specs:
|
||||
var parameter: TwitchGenParameter = TwitchGenParameter.new()
|
||||
var schema = parameter_spec["schema"]
|
||||
parameter._name = parameter_spec.get("name", "")
|
||||
parameter._description = parameter_spec.get("description", "")
|
||||
parameter._type = _get_param_type(schema)
|
||||
parameter._required = parameter_spec.get("required", false)
|
||||
parameter._is_time = schema.get("format", "") == "date-time"
|
||||
parameter._is_array = schema.get("type", "") == "array"
|
||||
method.add_parameter(parameter)
|
||||
|
||||
|
||||
func _add_component(ref: String, component: TwitchGenComponent) -> void:
|
||||
components.append(component)
|
||||
component_map[ref] = component
|
||||
|
||||
|
||||
func get_component_by_ref(ref: String) -> TwitchGenComponent:
|
||||
return component_map[ref]
|
||||
|
||||
|
||||
func _get_param_type(schema: Dictionary) -> String:
|
||||
if schema.has("$ref"):
|
||||
return schema["$ref"]
|
||||
|
||||
if not schema.has("type"):
|
||||
return "Variant" # Maybe ugly
|
||||
|
||||
var type = schema["type"]
|
||||
var format = schema.get("format", "")
|
||||
match type:
|
||||
"object":
|
||||
if schema.has("additinalProperties"):
|
||||
return _get_param_type(schema["additinalProperties"])
|
||||
return "Dictionary"
|
||||
"string":
|
||||
# Why did I do this in the first place?
|
||||
# Lets disable and see if problems appear
|
||||
#if format == "date-time":
|
||||
# return "Variant"
|
||||
return "String"
|
||||
"integer":
|
||||
return "int"
|
||||
"number":
|
||||
return "float" if format == "float" else "int"
|
||||
"boolean":
|
||||
return "bool"
|
||||
"array":
|
||||
var ref: String = schema["items"].get("$ref", "")
|
||||
if schema["items"].get("type", "") == "string":
|
||||
return "String"
|
||||
elif ref != "":
|
||||
return ref
|
||||
else:
|
||||
return "Variant"
|
||||
_: return "Variant"
|
||||
|
|
@ -0,0 +1 @@
|
|||
uid://o7q04krfh33l
|
||||
3
addons/twitcher/editor/api_generator/twitch_gen.gd
Normal file
3
addons/twitcher/editor/api_generator/twitch_gen.gd
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
extends RefCounted
|
||||
|
||||
class_name TwitchGen
|
||||
1
addons/twitcher/editor/api_generator/twitch_gen.gd.uid
Normal file
1
addons/twitcher/editor/api_generator/twitch_gen.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://cvtstbwwwf7gf
|
||||
60
addons/twitcher/editor/api_generator/twitch_gen_component.gd
Normal file
60
addons/twitcher/editor/api_generator/twitch_gen_component.gd
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
@tool
|
||||
extends TwitchGen
|
||||
|
||||
class_name TwitchGenComponent
|
||||
|
||||
var _classname: String
|
||||
var _ref: String
|
||||
var _description: String
|
||||
var _fields: Array[TwitchGenField] = []
|
||||
var _field_map: Dictionary[String, TwitchGenField] = {}
|
||||
var _parent_component: TwitchGenComponent
|
||||
var _sub_components: Dictionary[String, TwitchGenComponent] = {}
|
||||
var _is_root: bool
|
||||
var _is_response: bool
|
||||
var _has_paging: bool
|
||||
var _filename: String:
|
||||
get(): return _classname.to_snake_case() + ".gd"
|
||||
|
||||
|
||||
func _init(classname: String, ref: String) -> void:
|
||||
_classname = _sanatize_classname(classname)
|
||||
_ref = ref
|
||||
|
||||
|
||||
func add_field(field: TwitchGenField) -> void:
|
||||
_fields.append(field)
|
||||
_field_map[field._name] = field
|
||||
if field._name == "pagination":
|
||||
_has_paging = true
|
||||
|
||||
|
||||
func get_field_by_name(field_name: String) -> TwitchGenField:
|
||||
return _field_map.get(field_name, null)
|
||||
|
||||
|
||||
func get_root_classname() -> String:
|
||||
var parent: TwitchGenComponent = self
|
||||
while parent._parent_component != null:
|
||||
parent = parent._parent_component
|
||||
return parent._classname
|
||||
|
||||
|
||||
func get_filename() -> String:
|
||||
return get_root_classname().to_snake_case() + ".gd"
|
||||
|
||||
|
||||
func _sanatize_classname(val: String) -> String:
|
||||
match val:
|
||||
"Image": return "TwitchImage"
|
||||
"Panel": return "TwitchPanel"
|
||||
_: return val
|
||||
|
||||
|
||||
func get_component(component_name: String) -> TwitchGenComponent:
|
||||
return _sub_components.get(component_name)
|
||||
|
||||
|
||||
func add_component(sub_component: TwitchGenComponent) -> void:
|
||||
_sub_components[sub_component._classname] = sub_component
|
||||
sub_component._parent_component = self
|
||||
|
|
@ -0,0 +1 @@
|
|||
uid://cttaojg743q5y
|
||||
31
addons/twitcher/editor/api_generator/twitch_gen_field.gd
Normal file
31
addons/twitcher/editor/api_generator/twitch_gen_field.gd
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
@tool
|
||||
extends TwitchGen
|
||||
|
||||
class_name TwitchGenField
|
||||
|
||||
var _name: String:
|
||||
set = _update_name
|
||||
var _description: String
|
||||
var _type: String
|
||||
var _is_required: bool
|
||||
var _is_sub_class: bool:
|
||||
get(): return _type.begins_with("#")
|
||||
var _is_array: bool
|
||||
var _is_typed_array: bool:
|
||||
get(): return _is_array && _type.begins_with("#")
|
||||
|
||||
|
||||
## Couple of names from the Twitch API are messed up like keywords for godot or numbers
|
||||
func _update_name(val: String) -> void:
|
||||
match val:
|
||||
"animated": _name = "animated_format"
|
||||
"static": _name = "static_format"
|
||||
"1": _name = "_1"
|
||||
"2": _name = "_2"
|
||||
"3": _name = "_3"
|
||||
"4": _name = "_4"
|
||||
"1.5": _name = "_1_5"
|
||||
"100x100": _name = "_100x100"
|
||||
"24x24": _name = "_24x24"
|
||||
"300x200": _name = "_300x200"
|
||||
_: _name = val
|
||||
|
|
@ -0,0 +1 @@
|
|||
uid://dgx1w74garjdh
|
||||
59
addons/twitcher/editor/api_generator/twitch_gen_method.gd
Normal file
59
addons/twitcher/editor/api_generator/twitch_gen_method.gd
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
@tool
|
||||
extends TwitchGen
|
||||
|
||||
class_name TwitchGenMethod
|
||||
var _http_verb: String
|
||||
var _name: String
|
||||
var _summary: String
|
||||
var _description: String
|
||||
var _path: String
|
||||
var _doc_url: String
|
||||
var _parameters: Array[TwitchGenParameter] = []
|
||||
var _parameter_map: Dictionary[String, TwitchGenParameter] = {}
|
||||
var _required_parameters: Array[TwitchGenParameter]:
|
||||
get(): return _parameters.filter(func(p): return p._required)
|
||||
var _optional_parameters: Array[TwitchGenParameter]:
|
||||
get(): return _parameters.filter(func(p): return not p._required)
|
||||
var _body_type: String
|
||||
var _result_type: String
|
||||
var _content_type: String
|
||||
var _has_paging: bool
|
||||
var _contains_optional: bool
|
||||
var _contains_body: bool:
|
||||
get(): return _body_type != null and _body_type != ""
|
||||
|
||||
|
||||
func add_parameter(parameter: TwitchGenParameter) -> void:
|
||||
_parameters.append(parameter)
|
||||
_contains_optional = _contains_optional || not parameter._required
|
||||
_parameter_map[parameter._name] = parameter
|
||||
if parameter._name == "after":
|
||||
_has_paging = true
|
||||
|
||||
|
||||
func get_parameter_by_name(name: String) -> TwitchGenParameter:
|
||||
return _parameter_map.get(name)
|
||||
|
||||
|
||||
func get_optional_classname() -> String:
|
||||
return _name.capitalize().replace(" ", "") + "Opt"
|
||||
|
||||
|
||||
func get_optional_type() -> String:
|
||||
return "#/components/schemas/" + get_optional_classname()
|
||||
|
||||
|
||||
func get_optional_component() -> TwitchGenComponent:
|
||||
var component = TwitchGenComponent.new(get_optional_classname(), get_optional_type())
|
||||
component._description = "All optional parameters for TwitchAPI.%s" % _name
|
||||
component._is_root = true
|
||||
for parameter: TwitchGenParameter in _optional_parameters:
|
||||
var field = TwitchGenField.new()
|
||||
field._name = parameter._name
|
||||
field._type = parameter._type
|
||||
field._description = parameter._description
|
||||
field._is_required = false
|
||||
field._is_array = parameter._is_array
|
||||
component.add_field(field)
|
||||
|
||||
return component
|
||||
|
|
@ -0,0 +1 @@
|
|||
uid://dkwhf6s838d20
|
||||
22
addons/twitcher/editor/api_generator/twitch_gen_parameter.gd
Normal file
22
addons/twitcher/editor/api_generator/twitch_gen_parameter.gd
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
@tool
|
||||
extends TwitchGen
|
||||
|
||||
class_name TwitchGenParameter
|
||||
var _name: String
|
||||
var _description: String
|
||||
var _required: bool
|
||||
var _type: String
|
||||
var _is_time: bool
|
||||
var _is_array: bool
|
||||
|
||||
static func sort(p1: TwitchGenParameter, p2: TwitchGenParameter) -> bool:
|
||||
if p1._name == "broadcaster_id":
|
||||
return false
|
||||
if p2._name == "broadcaster_id":
|
||||
return true
|
||||
if p1._required && not p2._required:
|
||||
return true
|
||||
if not p1._required && p2._required:
|
||||
return false
|
||||
return p1._name < p2._name
|
||||
|
||||
|
|
@ -0,0 +1 @@
|
|||
uid://1ptmbsygcfyt
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
extends EditorInspectorPlugin
|
||||
|
||||
const EventsubConfigProperty = preload("res://addons/twitcher/editor/inspector/twitch_eventsub_config_property.gd")
|
||||
|
||||
func _can_handle(object: Object) -> bool:
|
||||
return object is TwitchEventsubConfig
|
||||
|
||||
|
||||
func _parse_property(object: Object, type: Variant.Type, name: String, \
|
||||
hint_type: PropertyHint, hint_string: String, usage_flags: int, \
|
||||
wide: bool) -> bool:
|
||||
|
||||
if name == &"condition":
|
||||
add_property_editor("condition", EventsubConfigProperty.new(), true)
|
||||
return true
|
||||
if name == &"type":
|
||||
add_property_editor("type", ToDocs.new(), true, "Documentation")
|
||||
return false
|
||||
|
||||
|
||||
class ToDocs extends EditorProperty:
|
||||
const EXT_LINK = preload("res://addons/twitcher/assets/ext-link.svg")
|
||||
|
||||
var docs = Button.new()
|
||||
|
||||
|
||||
func _init() -> void:
|
||||
docs.text = "To dev.twitch.tv"
|
||||
docs.icon = EXT_LINK
|
||||
docs.pressed.connect(_on_to_docs)
|
||||
add_child(docs)
|
||||
add_focusable(docs)
|
||||
|
||||
|
||||
func _on_to_docs() -> void:
|
||||
var eventsub_config: TwitchEventsubConfig = get_edited_object()
|
||||
OS.shell_open(eventsub_config.definition.documentation_link)
|
||||
|
|
@ -0,0 +1 @@
|
|||
uid://c1afm3xjonwxr
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
extends EditorProperty
|
||||
|
||||
const USER_CONVERTER = preload("res://addons/twitcher/editor/inspector/user_converter.tscn")
|
||||
const TwitchEditorSettings = preload("res://addons/twitcher/editor/twitch_editor_settings.gd")
|
||||
|
||||
var _container: Node = GridContainer.new()
|
||||
|
||||
func _init():
|
||||
_container.columns = 2
|
||||
add_child(_container)
|
||||
set_bottom_editor(_container)
|
||||
|
||||
|
||||
func _on_type_change(new_type: TwitchEventsubDefinition.Type) -> void:
|
||||
var eventsub_config: TwitchEventsubConfig = get_edited_object();
|
||||
if eventsub_config != null:
|
||||
for meta in eventsub_config.get_meta_list():
|
||||
if meta.ends_with("_user"):
|
||||
eventsub_config.remove_meta(meta)
|
||||
_create_conditions()
|
||||
|
||||
|
||||
func _update_property() -> void:
|
||||
_create_conditions()
|
||||
|
||||
|
||||
func _create_conditions() -> void:
|
||||
for node in _container.get_children():
|
||||
node.queue_free()
|
||||
|
||||
var eventsub_config: TwitchEventsubConfig = get_edited_object();
|
||||
if eventsub_config == null || eventsub_config.get_class() == &"EditorDebuggerRemoteObject": return
|
||||
|
||||
for condition_name: StringName in eventsub_config.definition.conditions:
|
||||
var condition_value = eventsub_config.condition.get_or_add(condition_name, "")
|
||||
var condition_title = Label.new()
|
||||
condition_title.text = condition_name.capitalize()
|
||||
_container.add_child(condition_title)
|
||||
var editor_token = TwitchEditorSettings.editor_oauth_token
|
||||
if condition_name.to_lower().ends_with("user_id") && editor_token.is_token_valid():
|
||||
var user_converter = USER_CONVERTER.instantiate()
|
||||
user_converter.changed.connect(_on_changed_user.bind(condition_name))
|
||||
_container.add_child(user_converter)
|
||||
if eventsub_config.has_meta(condition_name + "_user"):
|
||||
var user = eventsub_config.get_meta(condition_name + "_user")
|
||||
user_converter.update_user(user)
|
||||
elif condition_value != "":
|
||||
user_converter.user_id = condition_value
|
||||
user_converter.reload()
|
||||
else:
|
||||
var input = LineEdit.new()
|
||||
input.text_submitted.connect(_on_change_text.bind(condition_name, input))
|
||||
input.focus_exited.connect(_on_change_text.bind("", condition_name, input))
|
||||
input.text = condition_value
|
||||
input.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
_container.add_child(input)
|
||||
|
||||
|
||||
func _on_changed_user(user: TwitchUser, condition_name: StringName) -> void:
|
||||
var eventsub_config: TwitchEventsubConfig = get_edited_object();
|
||||
if user == null:
|
||||
eventsub_config.condition[condition_name] = ""
|
||||
eventsub_config.remove_meta(condition_name + "_user")
|
||||
emit_changed(&"condition", eventsub_config.condition)
|
||||
else:
|
||||
eventsub_config.condition[condition_name] = user.id
|
||||
eventsub_config.set_meta(condition_name + "_user", user)
|
||||
emit_changed(&"condition", eventsub_config.condition)
|
||||
|
||||
|
||||
func _on_change_text(new_text: String, condition_name: StringName, input: LineEdit) -> void:
|
||||
print("BLUB")
|
||||
var eventsub_config: TwitchEventsubConfig = get_edited_object();
|
||||
eventsub_config.condition[condition_name] = input.text
|
||||
#emit_changed(&"condition", eventsub_config.condition)
|
||||
|
|
@ -0,0 +1 @@
|
|||
uid://drv4gmn8akxf2
|
||||
|
|
@ -0,0 +1,83 @@
|
|||
extends EditorInspectorPlugin
|
||||
|
||||
|
||||
func _can_handle(object: Object) -> bool:
|
||||
return object is TwitchEventsub || object is TwitchService
|
||||
|
||||
|
||||
func _parse_property(object: Object, type: Variant.Type, name: String, hint_type: PropertyHint, hint_string: String, usage_flags: int, wide: bool) -> bool:
|
||||
if name == &"scopes" && object.get_class() != &"EditorDebuggerRemoteObject":
|
||||
if (object is TwitchService && object.eventsub != null) || object is TwitchEventsub:
|
||||
add_property_editor(&"scope_validation", ScopeValidation.new(), true, "Scope Validation")
|
||||
return false
|
||||
|
||||
|
||||
class ScopeValidation extends EditorProperty:
|
||||
const WARNING_LABEL_SETTINGS = preload("res://addons/twitcher/assets/warning_label_settings.tres")
|
||||
const INFO_LABEL_SETTINGS = preload("res://addons/twitcher/assets/info_label_settings.tres")
|
||||
var _warning_label: Label = Label.new();
|
||||
var _apply_scopes: Button = Button.new();
|
||||
var _needed_scopes: Dictionary = {}
|
||||
var container: Control = VBoxContainer.new()
|
||||
|
||||
|
||||
func _init():
|
||||
_warning_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
_warning_label.text = "Press validate to check if scopes maybe are missing."
|
||||
|
||||
var validate_button = Button.new();
|
||||
validate_button.text = "Validate";
|
||||
validate_button.tooltip_text = "Checks the scopes of the subscriptions " \
|
||||
+ " if they match the defined scopes in the scope " \
|
||||
+ " property";
|
||||
validate_button.pressed.connect(_on_validate_scopes);
|
||||
|
||||
_apply_scopes.text = "Apply Scopes";
|
||||
_apply_scopes.tooltip_text = "Apply Scopes to the scope resource in " \
|
||||
+ " this TwitchEventsub. It maybe not needed depending on the " \
|
||||
+ " Subscription. Please check the documentation if there is a logical " \
|
||||
+ " condition and apply the scopes accordingly.";
|
||||
_apply_scopes.pressed.connect(_on_apply_scopes);
|
||||
|
||||
add_child(validate_button)
|
||||
container.add_child(_warning_label)
|
||||
add_child(container)
|
||||
set_bottom_editor(container)
|
||||
|
||||
|
||||
func _on_apply_scopes() -> void:
|
||||
var scopes = get_edited_object().scopes;
|
||||
var scopes_to_add: Array[StringName] = [];
|
||||
for scope in _needed_scopes.values():
|
||||
scopes_to_add.append(scope);
|
||||
scopes.add_scopes(scopes_to_add);
|
||||
_clear_warning();
|
||||
|
||||
|
||||
func _on_validate_scopes() -> void:
|
||||
var scopes = get_edited_object().scopes;
|
||||
var subscriptions = get_edited_object().get_subscriptions();
|
||||
|
||||
_needed_scopes.clear()
|
||||
for subscription: TwitchEventsubConfig in subscriptions:
|
||||
if subscription == null: continue
|
||||
for scope in subscription.definition.scopes:
|
||||
_needed_scopes[scope] = scope
|
||||
|
||||
for scope in scopes.used_scopes:
|
||||
_needed_scopes.erase(scope)
|
||||
|
||||
if !_needed_scopes.is_empty():
|
||||
if _apply_scopes.get_parent() == null: container.add_child(_apply_scopes)
|
||||
_warning_label.label_settings = WARNING_LABEL_SETTINGS
|
||||
var needed_scopes = ", ".join(_needed_scopes.values())
|
||||
_warning_label.text = "You may miss scopes please check documentation if you need to add: %s" % needed_scopes;
|
||||
else:
|
||||
_clear_warning()
|
||||
|
||||
|
||||
func _clear_warning() -> void:
|
||||
_warning_label.text = "Scopes seems to be OK for this EventSub."
|
||||
_warning_label.label_settings = INFO_LABEL_SETTINGS
|
||||
if _apply_scopes.get_parent() != null:
|
||||
container.remove_child(_apply_scopes)
|
||||
|
|
@ -0,0 +1 @@
|
|||
uid://cuo2g5oib1cf3
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
@tool
|
||||
extends EditorInspectorPlugin
|
||||
|
||||
func _can_handle(object: Object) -> bool:
|
||||
return object is TwitchMediaLoader
|
||||
|
||||
|
||||
func _parse_property(object: Object, type: Variant.Type, name: String, hint_type: PropertyHint, hint_string: String, usage_flags: int, wide: bool) -> bool:
|
||||
if name == &"cache_emote":
|
||||
var clear_emote_button = ClearCacheEditor.new(object.cache_emote)
|
||||
add_property_editor("cache_cheermote", clear_emote_button, true, "Clear Emote Cache")
|
||||
|
||||
if name == &"cache_badge":
|
||||
var clear_badge_button = ClearCacheEditor.new(object.cache_badge)
|
||||
add_property_editor("cache_cheermote", clear_badge_button, true, "Clear Badge Cache")
|
||||
|
||||
if name == &"cache_cheermote":
|
||||
var clear_cheermote_button = ClearCacheEditor.new(object.cache_cheermote)
|
||||
add_property_editor("cache_cheermote", clear_cheermote_button, true, "Clear Cheermotes Cache")
|
||||
|
||||
return false
|
||||
|
||||
|
||||
class ClearCacheEditor extends EditorProperty:
|
||||
|
||||
var _button: Button
|
||||
|
||||
func _init(path: String) -> void:
|
||||
_button = Button.new()
|
||||
_button.text = "Clear"
|
||||
_button.pressed.connect(_clear.bind(path))
|
||||
add_child(_button)
|
||||
|
||||
|
||||
func _clear(path: String) -> void:
|
||||
var dir: DirAccess = DirAccess.open(path)
|
||||
for file: String in dir.get_files():
|
||||
if file.ends_with(".res"):
|
||||
var err: Error = dir.remove(file)
|
||||
if err != OK:
|
||||
push_error("Can't delete %s/%s cause of %s" % [dir, file, error_string(err)])
|
||||
var tween = create_tween()
|
||||
var _button_color = _button.modulate
|
||||
tween.tween_property(_button, "modulate", Color.GREEN, .25)
|
||||
tween.tween_property(_button, "modulate", _button_color, .25)
|
||||
tween.play()
|
||||
|
|
@ -0,0 +1 @@
|
|||
uid://be76dqjl123t8
|
||||
91
addons/twitcher/editor/inspector/twitch_property.gd
Normal file
91
addons/twitcher/editor/inspector/twitch_property.gd
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
extends RefCounted
|
||||
|
||||
## Helper class for easier editing of Project Settings
|
||||
class_name TwitchProperty
|
||||
|
||||
var key: String;
|
||||
var default_value: Variant;
|
||||
|
||||
func _init(k: String, default_val: Variant = "") -> void:
|
||||
key = k;
|
||||
default_value = default_val;
|
||||
_add_property()
|
||||
|
||||
|
||||
func _add_property():
|
||||
if not ProjectSettings.has_setting(key):
|
||||
ProjectSettings.set_setting(key, default_value);
|
||||
ProjectSettings.set_initial_value(key, default_value);
|
||||
|
||||
|
||||
func get_val() -> Variant:
|
||||
return ProjectSettings.get_setting_with_override(key);
|
||||
|
||||
|
||||
func set_val(val) -> void:
|
||||
ProjectSettings.set(key, val);
|
||||
|
||||
|
||||
func basic() -> TwitchProperty:
|
||||
ProjectSettings.set_as_basic(key, true);
|
||||
return self;
|
||||
|
||||
|
||||
func as_str(description: String = "") -> TwitchProperty:
|
||||
return _add_type_def(TYPE_STRING, PROPERTY_HINT_PLACEHOLDER_TEXT, description);
|
||||
|
||||
|
||||
func as_select(values: Array[String], optional: bool = true) -> TwitchProperty:
|
||||
var hint_string = ",".join(values);
|
||||
var enum_hint = PROPERTY_HINT_ENUM;
|
||||
if optional: enum_hint = PROPERTY_HINT_ENUM_SUGGESTION;
|
||||
return _add_type_def(TYPE_STRING, enum_hint, hint_string);
|
||||
|
||||
|
||||
func as_bit_field(values: Array[String]) -> TwitchProperty:
|
||||
var hint_string = ",".join(values);
|
||||
return _add_type_def(TYPE_INT, PROPERTY_HINT_FLAGS, hint_string);
|
||||
|
||||
|
||||
func as_password(description: String = "") -> TwitchProperty:
|
||||
return _add_type_def(TYPE_STRING, PROPERTY_HINT_PASSWORD, description);
|
||||
|
||||
|
||||
func as_bool(description: String = "") -> TwitchProperty:
|
||||
return _add_type_def(TYPE_BOOL, PROPERTY_HINT_PLACEHOLDER_TEXT, description)
|
||||
|
||||
|
||||
func as_num() -> TwitchProperty:
|
||||
return _add_type_def(TYPE_INT, PROPERTY_HINT_NONE, "")
|
||||
|
||||
|
||||
func as_global() -> TwitchProperty:
|
||||
return _add_type_def(TYPE_STRING, PROPERTY_HINT_GLOBAL_FILE, "");
|
||||
|
||||
|
||||
func as_image() -> TwitchProperty:
|
||||
return _add_type_def(TYPE_STRING, PROPERTY_HINT_FILE, "*.png,*.jpg,*.tres")
|
||||
|
||||
|
||||
func as_dir() -> TwitchProperty:
|
||||
return _add_type_def(TYPE_STRING, PROPERTY_HINT_DIR, "");
|
||||
|
||||
|
||||
## Type should be the generic type of the array
|
||||
func as_list(type: Variant = "") -> TwitchProperty:
|
||||
return _add_type_def(TYPE_ARRAY, PROPERTY_HINT_ARRAY_TYPE, type);
|
||||
|
||||
|
||||
## The hint string can be a set of filters with wildcards like "*.png,*.jpg"
|
||||
func as_global_save(file_types: String = "") -> TwitchProperty:
|
||||
return _add_type_def(TYPE_STRING, PROPERTY_HINT_GLOBAL_SAVE_FILE, file_types)
|
||||
|
||||
|
||||
func _add_type_def(type: int, hint: int, hint_string: Variant) -> TwitchProperty:
|
||||
ProjectSettings.add_property_info({
|
||||
"name": key,
|
||||
"type": type,
|
||||
"hint": hint,
|
||||
"hint_string": hint_string
|
||||
})
|
||||
return self
|
||||
1
addons/twitcher/editor/inspector/twitch_property.gd.uid
Normal file
1
addons/twitcher/editor/inspector/twitch_property.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://ci8bdo18wnodj
|
||||
13
addons/twitcher/editor/inspector/twitch_scope_inspector.gd
Normal file
13
addons/twitcher/editor/inspector/twitch_scope_inspector.gd
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
extends EditorInspectorPlugin
|
||||
|
||||
const TwitchScopeProperty = preload("res://addons/twitcher/editor/inspector/twitch_scope_property.gd")
|
||||
|
||||
func _can_handle(object: Object) -> bool:
|
||||
return object is TwitchOAuthScopes
|
||||
|
||||
|
||||
func _parse_property(object: Object, type: Variant.Type, name: String, hint_type: PropertyHint, hint_string: String, usage_flags: int, wide: bool) -> bool:
|
||||
if name == "used_scopes":
|
||||
add_property_editor("used_scopes", TwitchScopeProperty.new(), true);
|
||||
return false;
|
||||
return false
|
||||
|
|
@ -0,0 +1 @@
|
|||
uid://c7832kf7htajv
|
||||
51
addons/twitcher/editor/inspector/twitch_scope_property.gd
Normal file
51
addons/twitcher/editor/inspector/twitch_scope_property.gd
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
extends EditorProperty
|
||||
|
||||
const TITLE_SETTING := preload("res://addons/twitcher/assets/title_label_settings.tres")
|
||||
|
||||
var _scope_checkboxes: Dictionary[StringName, CheckBox]
|
||||
var grid: GridContainer = GridContainer.new();
|
||||
|
||||
signal scope_selected(scope: TwitchScope.Definition)
|
||||
|
||||
func _init() -> void:
|
||||
grid.columns = 1;
|
||||
var grouped_scopes = TwitchScope.get_grouped_scopes();
|
||||
for category: String in grouped_scopes:
|
||||
var title_category = Label.new();
|
||||
title_category.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER;
|
||||
title_category.text = category.capitalize();
|
||||
title_category.label_settings = TITLE_SETTING;
|
||||
grid.add_child(title_category);
|
||||
grid.add_child(Control.new());
|
||||
|
||||
for scope: TwitchScope.Definition in grouped_scopes[category]:
|
||||
var checkbox: CheckBox = CheckBox.new();
|
||||
checkbox.text = scope.value;
|
||||
checkbox.toggled.connect(_on_checkbox_pressed.bind(scope))
|
||||
checkbox.tooltip_text = scope.description
|
||||
_scope_checkboxes[scope.value] = checkbox
|
||||
grid.add_child(checkbox);
|
||||
add_focusable(checkbox);
|
||||
add_child(grid);
|
||||
|
||||
|
||||
func _on_scope_changed() -> void:
|
||||
update_property()
|
||||
|
||||
|
||||
func _update_property() -> void:
|
||||
for scope: StringName in _scope_checkboxes.keys():
|
||||
var checkbox: CheckBox = _scope_checkboxes[scope];
|
||||
var scopes: OAuthScopes = get_edited_object()
|
||||
checkbox.button_pressed = scopes.used_scopes.find(scope) != -1;
|
||||
|
||||
|
||||
func _on_checkbox_pressed(toggled_on: bool, scope: TwitchScope.Definition) -> void:
|
||||
var scopes: OAuthScopes = get_edited_object()
|
||||
if toggled_on:
|
||||
if scopes.used_scopes.find(scope.value) == -1:
|
||||
scopes.used_scopes.append(scope.value)
|
||||
else:
|
||||
scopes.used_scopes.erase(scope.value)
|
||||
emit_changed("used_scopes", scopes.used_scopes, &"", true)
|
||||
scope_selected.emit(scope)
|
||||
|
|
@ -0,0 +1 @@
|
|||
uid://xldq5o0osdgq
|
||||
14
addons/twitcher/editor/inspector/twitch_token_info.gd
Normal file
14
addons/twitcher/editor/inspector/twitch_token_info.gd
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
@tool
|
||||
extends "res://addons/twitcher/lib/oOuch/oauth_token_info.gd"
|
||||
|
||||
const TWITCH_TOKEN_REVOKE_POPUP = preload("res://addons/twitcher/editor/inspector/twitch_token_revoke_popup.tscn")
|
||||
const TwitchTokenRevokePopup = preload("res://addons/twitcher/editor/inspector/twitch_token_revoke_popup.gd")
|
||||
|
||||
|
||||
func _on_revoke_pressed() -> void:
|
||||
var popup: TwitchTokenRevokePopup = TWITCH_TOKEN_REVOKE_POPUP.instantiate()
|
||||
popup.token = token
|
||||
add_child(popup)
|
||||
popup.popup_centered()
|
||||
var success = await popup.revoked
|
||||
if success: _reset_token()
|
||||
|
|
@ -0,0 +1 @@
|
|||
uid://c8no6da8ae0xt
|
||||
7
addons/twitcher/editor/inspector/twitch_token_info.tscn
Normal file
7
addons/twitcher/editor/inspector/twitch_token_info.tscn
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
[gd_scene load_steps=3 format=3 uid="uid://c7qvkjw425jaf"]
|
||||
|
||||
[ext_resource type="PackedScene" uid="uid://6d2jst8ga4le" path="res://addons/twitcher/lib/oOuch/oauth_token_info.tscn" id="1_0mxfe"]
|
||||
[ext_resource type="Script" uid="uid://c8no6da8ae0xt" path="res://addons/twitcher/editor/inspector/twitch_token_info.gd" id="2_kejyg"]
|
||||
|
||||
[node name="TokenInfo" instance=ExtResource("1_0mxfe")]
|
||||
script = ExtResource("2_kejyg")
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
@tool
|
||||
extends Window
|
||||
|
||||
const TwitchEditorSettings = preload("res://addons/twitcher/editor/twitch_editor_settings.gd")
|
||||
|
||||
@export var token: OAuthToken
|
||||
|
||||
@onready var inspector: HBoxContainer = %Inspector
|
||||
@onready var cancel: Button = %Cancel
|
||||
@onready var revoke_locally: Button = %RevokeLocally
|
||||
@onready var revoke_twitch: Button = %RevokeTwitch
|
||||
@onready var twitch_token_handler: TwitchTokenHandler = %TwitchTokenHandler
|
||||
|
||||
signal revoked(success: bool)
|
||||
|
||||
var picker: EditorResourcePicker
|
||||
|
||||
func _ready() -> void:
|
||||
picker = EditorResourcePicker.new()
|
||||
picker.base_type = "OAuthSetting"
|
||||
picker.edited_resource = TwitchEditorSettings.game_oauth_setting
|
||||
picker.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
|
||||
inspector.add_child(picker)
|
||||
|
||||
cancel.pressed.connect(_on_cancel)
|
||||
revoke_locally.pressed.connect(_on_revoke_locally)
|
||||
revoke_twitch.pressed.connect(_on_revoke_twitch)
|
||||
|
||||
twitch_token_handler.token = token
|
||||
close_requested.connect(_on_cancel)
|
||||
|
||||
|
||||
func _on_cancel() -> void:
|
||||
revoked.emit(false)
|
||||
queue_free()
|
||||
|
||||
|
||||
func _on_revoke_locally() -> void:
|
||||
revoked.emit(true)
|
||||
token.remove_tokens()
|
||||
queue_free()
|
||||
|
||||
|
||||
func _on_revoke_twitch() -> void:
|
||||
revoked.emit(true)
|
||||
if is_instance_valid(picker.edited_resource):
|
||||
twitch_token_handler.oauth_setting = picker.edited_resource
|
||||
await twitch_token_handler.revoke_token()
|
||||
queue_free()
|
||||
|
|
@ -0,0 +1 @@
|
|||
uid://bp1fga8addrlc
|
||||
|
|
@ -0,0 +1,74 @@
|
|||
[gd_scene load_steps=6 format=3 uid="uid://b4n67bt8ni6ge"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://bp1fga8addrlc" path="res://addons/twitcher/editor/inspector/twitch_token_revoke_popup.gd" id="1_4n8su"]
|
||||
[ext_resource type="Script" uid="uid://blnbogtrshw4r" path="res://addons/twitcher/auth/twitch_token_handler.gd" id="2_iycl8"]
|
||||
[ext_resource type="Resource" uid="uid://c4scwuk8q0r40" path="res://addons/twitcher/lib/oOuch/default_key_provider.tres" id="3_4n8su"]
|
||||
[ext_resource type="Script" uid="uid://b52xp7c23ucfk" path="res://addons/twitcher/lib/oOuch/oauth_token.gd" id="4_iycl8"]
|
||||
|
||||
[sub_resource type="Resource" id="Resource_twsgi"]
|
||||
script = ExtResource("4_iycl8")
|
||||
_crypto_key_provider = ExtResource("3_4n8su")
|
||||
_identifier = "Auth-2409"
|
||||
_cache_path = "user://auth.conf"
|
||||
|
||||
[node name="TokenRevokePopup" type="Window"]
|
||||
title = "Revoke Token"
|
||||
position = Vector2i(0, 36)
|
||||
size = Vector2i(400, 200)
|
||||
script = ExtResource("1_4n8su")
|
||||
|
||||
[node name="MarginContainer" type="MarginContainer" parent="."]
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
theme_override_constants/margin_left = 10
|
||||
theme_override_constants/margin_top = 10
|
||||
theme_override_constants/margin_right = 10
|
||||
theme_override_constants/margin_bottom = 10
|
||||
|
||||
[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer"]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="Label" type="Label" parent="MarginContainer/VBoxContainer"]
|
||||
custom_minimum_size = Vector2(0, 50)
|
||||
layout_mode = 2
|
||||
text = "You can decide to revoke the token locally or actually invalidate it on Twitch side too. To revoke it on Twitch the client id must be known: "
|
||||
autowrap_mode = 3
|
||||
|
||||
[node name="Inspector" type="HBoxContainer" parent="MarginContainer/VBoxContainer"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
|
||||
[node name="Label" type="Label" parent="MarginContainer/VBoxContainer/Inspector"]
|
||||
layout_mode = 2
|
||||
text = "OAuth Setting:"
|
||||
|
||||
[node name="HBoxContainer" type="HBoxContainer" parent="MarginContainer/VBoxContainer"]
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 10
|
||||
|
||||
[node name="Cancel" type="Button" parent="MarginContainer/VBoxContainer/HBoxContainer"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
text = "Cancel"
|
||||
|
||||
[node name="RevokeTwitch" type="Button" parent="MarginContainer/VBoxContainer/HBoxContainer"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
text = "Revoke on Twitch"
|
||||
|
||||
[node name="RevokeLocally" type="Button" parent="MarginContainer/VBoxContainer/HBoxContainer"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
text = "Revoke Locally"
|
||||
|
||||
[node name="TwitchTokenHandler" type="Node" parent="."]
|
||||
unique_name_in_owner = true
|
||||
script = ExtResource("2_iycl8")
|
||||
token = SubResource("Resource_twsgi")
|
||||
metadata/_custom_type_script = "uid://blnbogtrshw4r"
|
||||
44
addons/twitcher/editor/inspector/twitch_user_inspector.gd
Normal file
44
addons/twitcher/editor/inspector/twitch_user_inspector.gd
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
@tool
|
||||
extends EditorInspectorPlugin
|
||||
|
||||
const TwitchEditorSettings = preload("res://addons/twitcher/editor/twitch_editor_settings.gd")
|
||||
const UserProperty = preload("res://addons/twitcher/editor/inspector/user_property.gd")
|
||||
const TEST_CREDENTIALS = preload("res://addons/twitcher/editor/setup/test_credentials.tscn")
|
||||
const TestCredentials = preload("res://addons/twitcher/editor/setup/test_credentials.gd")
|
||||
|
||||
func _can_handle(object: Object) -> bool:
|
||||
return true
|
||||
|
||||
|
||||
func _parse_property(object: Object, type: Variant.Type, name: String, hint_type: PropertyHint, hint_string: String, usage_flags: int, wide: bool) -> bool:
|
||||
if hint_string == "TwitchUser":
|
||||
|
||||
if TwitchEditorSettings.is_valid():
|
||||
add_property_editor(name, UserProperty.new())
|
||||
return true
|
||||
else:
|
||||
var info_label: Label = Label.new()
|
||||
info_label.text = "Authorize editor to have a custom inspector for the '%s'." % name.capitalize()
|
||||
info_label.label_settings = LabelSettings.new()
|
||||
info_label.label_settings.font_size = 13
|
||||
info_label.label_settings.font_color = Color.AQUA
|
||||
info_label.autowrap_mode = TextServer.AUTOWRAP_WORD
|
||||
info_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
|
||||
var authorize_editor: TestCredentials = TEST_CREDENTIALS.instantiate()
|
||||
authorize_editor.text = "Authorize Editor"
|
||||
authorize_editor.authorized.connect(_on_authorized.bind(object), CONNECT_DEFERRED)
|
||||
|
||||
|
||||
var hbox: HBoxContainer = HBoxContainer.new()
|
||||
hbox.add_child(info_label)
|
||||
hbox.add_child(authorize_editor)
|
||||
hbox.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
add_custom_control(hbox)
|
||||
|
||||
return false
|
||||
|
||||
|
||||
func _on_authorized(object: Object) -> void:
|
||||
EditorInterface.get_inspector().edit(null)
|
||||
EditorInterface.get_inspector().edit(object)
|
||||
|
|
@ -0,0 +1 @@
|
|||
uid://cpq13q33gcqov
|
||||
134
addons/twitcher/editor/inspector/user_converter.gd
Normal file
134
addons/twitcher/editor/inspector/user_converter.gd
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
@tool
|
||||
extends HBoxContainer
|
||||
|
||||
class_name UserConverter
|
||||
|
||||
const TwitchEditorSettings = preload("res://addons/twitcher/editor/twitch_editor_settings.gd")
|
||||
const TwitchTweens = preload("res://addons/twitcher/editor/twitch_tweens.gd")
|
||||
|
||||
@onready var _login: LineEdit = %Login
|
||||
@onready var _id: LineEdit = %Id
|
||||
@onready var _swap_view: Button = %SwapView
|
||||
@onready var search: Button = %Search
|
||||
|
||||
@export var user: TwitchUser
|
||||
@export var token: OAuthToken
|
||||
@export var setting: OAuthSetting
|
||||
|
||||
static var _current_user: TwitchUser
|
||||
|
||||
var user_login: String:
|
||||
set(val):
|
||||
user_login = val
|
||||
_login.text = val
|
||||
_login.caret_column = val.length()
|
||||
get(): return _login.text
|
||||
|
||||
var user_id: String:
|
||||
set(val):
|
||||
user_id = val
|
||||
_id.text = val
|
||||
_id.caret_column = val.length()
|
||||
get(): return _id.text
|
||||
|
||||
|
||||
signal changed(user: TwitchUser)
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
if token == null: token = TwitchEditorSettings.editor_oauth_token
|
||||
if setting == null: setting = TwitchEditorSettings.editor_oauth_setting
|
||||
|
||||
_login.text_changed.connect(_on_login_changed)
|
||||
_login.text_submitted.connect(_on_text_submitted)
|
||||
_id.text_changed.connect(_on_id_changed)
|
||||
_id.text_submitted.connect(_on_text_submitted)
|
||||
_swap_view.pressed.connect(_on_swap_view)
|
||||
_load_current_user()
|
||||
search.pressed.connect(_on_changed)
|
||||
|
||||
|
||||
## Experimental tries to load user from api key
|
||||
func _load_current_user() -> void:
|
||||
if _current_user == null:
|
||||
var users: TwitchGetUsers.Opt = TwitchGetUsers.Opt.new()
|
||||
_current_user = await _get_user(users)
|
||||
|
||||
if _current_user != null:
|
||||
user_login = _current_user.login
|
||||
user_id = _current_user.id
|
||||
changed.emit(_current_user)
|
||||
|
||||
|
||||
func _on_swap_view() -> void:
|
||||
if _login.visible:
|
||||
_login.visible = false
|
||||
_id.visible = true
|
||||
_swap_view.text = "ID"
|
||||
else:
|
||||
_login.visible = true
|
||||
_id.visible = false
|
||||
_swap_view.text = "Name"
|
||||
|
||||
|
||||
func _on_id_changed(new_text: String) -> void:
|
||||
_login.text = ""
|
||||
TwitchTweens.loading(self, Color.AQUA)
|
||||
|
||||
|
||||
func _on_login_changed(new_text: String) -> void:
|
||||
_id.text = ""
|
||||
TwitchTweens.loading(self, Color.AQUA)
|
||||
|
||||
|
||||
func reload() -> void:
|
||||
TwitchTweens.loading(self)
|
||||
var new_user_login: String = _login.text
|
||||
var new_user_id: String = _id.text
|
||||
if new_user_id == "" && new_user_login == "":
|
||||
changed.emit(null)
|
||||
return
|
||||
|
||||
var users: TwitchGetUsers.Opt = TwitchGetUsers.Opt.new()
|
||||
|
||||
if new_user_login != "" && (user == null || user.login != new_user_login):
|
||||
users.login = [ new_user_login ]
|
||||
elif new_user_id != "" && (user == null || user.id != new_user_id):
|
||||
users.id = [ new_user_id ]
|
||||
|
||||
if users.id != null || users.login != null:
|
||||
user = await _get_user(users)
|
||||
if user == null:
|
||||
await TwitchTweens.flash(self, Color.RED)
|
||||
else:
|
||||
await TwitchTweens.flash(self, Color.GREEN)
|
||||
user_login = user.login
|
||||
user_id = user.id
|
||||
changed.emit(user)
|
||||
|
||||
|
||||
func update_user(user: TwitchUser) -> void:
|
||||
user_login = user.login
|
||||
user_id = user.id
|
||||
|
||||
|
||||
func _on_text_submitted(new_text: String) -> void:
|
||||
reload()
|
||||
|
||||
|
||||
func _on_changed() -> void:
|
||||
reload()
|
||||
|
||||
|
||||
func _get_user(get_user_opt: TwitchGetUsers.Opt) -> TwitchUser:
|
||||
var api: TwitchAPI = TwitchAPI.new()
|
||||
api.token = token
|
||||
api.oauth_setting = setting
|
||||
add_child(api)
|
||||
var response: TwitchGetUsers.Response = await api.get_users(get_user_opt)
|
||||
var data: Array[TwitchUser] = response.data
|
||||
if data.is_empty():
|
||||
printerr("User %s%s was not found." % [ get_user_opt.login, get_user_opt.id ])
|
||||
return null
|
||||
remove_child(api)
|
||||
return data[0]
|
||||
1
addons/twitcher/editor/inspector/user_converter.gd.uid
Normal file
1
addons/twitcher/editor/inspector/user_converter.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://b6qdiwr7rawx1
|
||||
33
addons/twitcher/editor/inspector/user_converter.tscn
Normal file
33
addons/twitcher/editor/inspector/user_converter.tscn
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
[gd_scene load_steps=3 format=3 uid="uid://cus81w3pidhjo"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://b6qdiwr7rawx1" path="res://addons/twitcher/editor/inspector/user_converter.gd" id="1_ior8m"]
|
||||
[ext_resource type="Texture2D" uid="uid://1e6nrtqsuc6" path="res://addons/twitcher/assets/icon_search.tres" id="2_t7vdb"]
|
||||
|
||||
[node name="UserConverter" type="HBoxContainer"]
|
||||
offset_right = 40.0
|
||||
offset_bottom = 40.0
|
||||
size_flags_horizontal = 3
|
||||
script = ExtResource("1_ior8m")
|
||||
|
||||
[node name="Login" type="LineEdit" parent="."]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
placeholder_text = "User Login"
|
||||
|
||||
[node name="Id" type="LineEdit" parent="."]
|
||||
unique_name_in_owner = true
|
||||
visible = false
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
placeholder_text = "User ID"
|
||||
|
||||
[node name="SwapView" type="Button" parent="."]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
text = "Name"
|
||||
|
||||
[node name="Search" type="Button" parent="."]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
icon = ExtResource("2_t7vdb")
|
||||
29
addons/twitcher/editor/inspector/user_property.gd
Normal file
29
addons/twitcher/editor/inspector/user_property.gd
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
@tool
|
||||
extends EditorProperty
|
||||
|
||||
const USER_CONVERTER = preload("res://addons/twitcher/editor/inspector/user_converter.tscn")
|
||||
const UserConverter = preload("res://addons/twitcher/editor/inspector/user_converter.gd")
|
||||
const TwitchEditorSettings = preload("res://addons/twitcher/editor/twitch_editor_settings.gd")
|
||||
|
||||
var _converter: UserConverter
|
||||
|
||||
|
||||
func _init():
|
||||
_converter = USER_CONVERTER.instantiate()
|
||||
_converter.changed.connect(_on_changed)
|
||||
add_child(_converter)
|
||||
|
||||
|
||||
func _update_property() -> void:
|
||||
var user: TwitchUser = get_edited_object()[get_edited_property()]
|
||||
if user == null:
|
||||
_converter.user_id = ""
|
||||
_converter.user_login = ""
|
||||
else:
|
||||
_converter.user_id = user.id
|
||||
_converter.user_login = user.login
|
||||
|
||||
|
||||
func _on_changed(user: TwitchUser) -> void:
|
||||
emit_changed(get_edited_property(), user, &"", true)
|
||||
_update_property()
|
||||
1
addons/twitcher/editor/inspector/user_property.gd.uid
Normal file
1
addons/twitcher/editor/inspector/user_property.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://c4ba54q6ecn7y
|
||||
98
addons/twitcher/editor/project_setting_property.gd
Normal file
98
addons/twitcher/editor/project_setting_property.gd
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
@tool
|
||||
extends RefCounted
|
||||
|
||||
class_name ProjectSettingProperty
|
||||
|
||||
var key: String
|
||||
var default_value: Variant
|
||||
|
||||
|
||||
func _init(k: String, default_val: Variant = "") -> void:
|
||||
key = k
|
||||
default_value = default_val
|
||||
_add_property()
|
||||
|
||||
|
||||
func _add_property():
|
||||
if not ProjectSettings.has_setting(key):
|
||||
ProjectSettings.set_setting(key, default_value)
|
||||
ProjectSettings.set_initial_value(key, default_value)
|
||||
|
||||
|
||||
func get_val() -> Variant:
|
||||
return ProjectSettings.get_setting_with_override(key)
|
||||
|
||||
|
||||
func set_val(val) -> void:
|
||||
ProjectSettings.set(key, val)
|
||||
|
||||
|
||||
func basic() -> ProjectSettingProperty:
|
||||
ProjectSettings.set_as_basic(key, true)
|
||||
return self
|
||||
|
||||
|
||||
func as_str(description: String = "") -> ProjectSettingProperty:
|
||||
return _add_type_def(TYPE_STRING, PROPERTY_HINT_PLACEHOLDER_TEXT, description)
|
||||
|
||||
|
||||
func as_select(values: Array[String], optional: bool = true) -> ProjectSettingProperty:
|
||||
var hint_string = ",".join(values)
|
||||
var enum_hint = PROPERTY_HINT_ENUM
|
||||
if optional: enum_hint = PROPERTY_HINT_ENUM_SUGGESTION
|
||||
return _add_type_def(TYPE_STRING, enum_hint, hint_string)
|
||||
|
||||
|
||||
# Won't work in godot 4.4 the resource is not loaded when you select the project and it will just drop it out of the list.
|
||||
#func as_resoruce(resource_name: StringName) -> ProjectSettingProperty:
|
||||
# return _add_type_def(TYPE_OBJECT, PROPERTY_HINT_RESOURCE_TYPE, resource_name)
|
||||
|
||||
|
||||
func as_bit_field(values: Array[String]) -> ProjectSettingProperty:
|
||||
var hint_string = ",".join(values)
|
||||
return _add_type_def(TYPE_INT, PROPERTY_HINT_FLAGS, hint_string)
|
||||
|
||||
|
||||
func as_password(description: String = "") -> ProjectSettingProperty:
|
||||
return _add_type_def(TYPE_STRING, PROPERTY_HINT_PASSWORD, description)
|
||||
|
||||
|
||||
func as_bool(description: String = "") -> ProjectSettingProperty:
|
||||
return _add_type_def(TYPE_BOOL, PROPERTY_HINT_PLACEHOLDER_TEXT, description)
|
||||
|
||||
|
||||
func as_num() -> ProjectSettingProperty:
|
||||
return _add_type_def(TYPE_INT, PROPERTY_HINT_NONE, "")
|
||||
|
||||
|
||||
func as_global() -> ProjectSettingProperty:
|
||||
return _add_type_def(TYPE_STRING, PROPERTY_HINT_GLOBAL_FILE, "")
|
||||
|
||||
|
||||
## file_type is comma separated values "*.png,*.jpg,*.tres"
|
||||
func as_file(file_type: String) -> ProjectSettingProperty:
|
||||
return _add_type_def(TYPE_STRING, PROPERTY_HINT_FILE, file_type)
|
||||
|
||||
|
||||
func as_dir() -> ProjectSettingProperty:
|
||||
return _add_type_def(TYPE_STRING, PROPERTY_HINT_DIR, "")
|
||||
|
||||
|
||||
## Type should be the generic type of the array
|
||||
func as_list(type: Variant = "") -> ProjectSettingProperty:
|
||||
return _add_type_def(TYPE_ARRAY, PROPERTY_HINT_ARRAY_TYPE, type)
|
||||
|
||||
|
||||
## The hint string can be a set of filters with wildcards like "*.png,*.jpg"
|
||||
func as_global_save(file_types: String = "") -> ProjectSettingProperty:
|
||||
return _add_type_def(TYPE_STRING, PROPERTY_HINT_GLOBAL_SAVE_FILE, file_types)
|
||||
|
||||
|
||||
func _add_type_def(type: int, hint: int, hint_string: Variant) -> ProjectSettingProperty:
|
||||
ProjectSettings.add_property_info({
|
||||
"name": key,
|
||||
"type": type,
|
||||
"hint": hint,
|
||||
"hint_string": hint_string
|
||||
})
|
||||
return self
|
||||
1
addons/twitcher/editor/project_setting_property.gd.uid
Normal file
1
addons/twitcher/editor/project_setting_property.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://cbmgm86618sxe
|
||||
55
addons/twitcher/editor/setup/file_select.gd
Normal file
55
addons/twitcher/editor/setup/file_select.gd
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
@tool
|
||||
extends Control
|
||||
|
||||
class_name FileSelect
|
||||
|
||||
@export var default_path: String
|
||||
@export var path: String: set = update_filepath
|
||||
@export var filters: PackedStringArray: set = update_filters
|
||||
|
||||
@onready var line_edit: LineEdit = %LineEdit
|
||||
@onready var button: Button = %Button
|
||||
@onready var file_dialog: FileDialog = %FileDialog
|
||||
|
||||
signal file_selected(path: String)
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
var icon = EditorInterface.get_editor_theme().get_icon(&"FileBrowse", &"EditorIcons")
|
||||
button.icon = icon
|
||||
button.pressed.connect(_on_open_file_dialog)
|
||||
file_dialog.file_selected.connect(_on_file_selected)
|
||||
line_edit.text_changed.connect(_on_path_changed)
|
||||
update_filepath(path)
|
||||
update_filters(filters)
|
||||
|
||||
|
||||
func update_filepath(new_path: String) -> void:
|
||||
if new_path == null || new_path == "":
|
||||
new_path = default_path
|
||||
path = new_path
|
||||
if is_inside_tree():
|
||||
line_edit.text = new_path
|
||||
file_dialog.current_path = new_path
|
||||
|
||||
|
||||
func update_filters(new_filters: PackedStringArray) -> void:
|
||||
filters = new_filters
|
||||
if is_inside_tree():
|
||||
file_dialog.filters = new_filters
|
||||
|
||||
|
||||
func _on_open_file_dialog() -> void:
|
||||
file_dialog.show()
|
||||
|
||||
|
||||
func _on_path_changed(new_path: String) -> void:
|
||||
file_dialog.current_path = new_path
|
||||
path = new_path
|
||||
file_selected.emit(new_path)
|
||||
|
||||
|
||||
func _on_file_selected(new_path: String) -> void:
|
||||
line_edit.text = new_path
|
||||
path = new_path
|
||||
file_selected.emit(new_path)
|
||||
1
addons/twitcher/editor/setup/file_select.gd.uid
Normal file
1
addons/twitcher/editor/setup/file_select.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://cgfc1nq4f4nae
|
||||
40
addons/twitcher/editor/setup/file_select.tscn
Normal file
40
addons/twitcher/editor/setup/file_select.tscn
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
[gd_scene load_steps=4 format=3 uid="uid://b7smp156mdns6"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://cgfc1nq4f4nae" path="res://addons/twitcher/editor/setup/file_select.gd" id="1_lnnjv"]
|
||||
|
||||
[sub_resource type="Image" id="Image_lnnjv"]
|
||||
data = {
|
||||
"data": PackedByteArray(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 225, 225, 225, 127, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 224, 224, 224, 255, 225, 225, 225, 127, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 225, 225, 225, 127, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 225, 225, 225, 127, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 238, 225, 225, 225, 150, 225, 225, 225, 150, 224, 224, 224, 221, 224, 224, 224, 148, 224, 224, 224, 168, 224, 224, 224, 228, 224, 224, 224, 156, 224, 224, 224, 160, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 238, 230, 230, 230, 30, 255, 255, 255, 1, 255, 255, 255, 1, 0, 0, 0, 0, 255, 255, 255, 1, 255, 255, 255, 1, 0, 0, 0, 0, 255, 255, 255, 1, 255, 255, 255, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 225, 225, 225, 150, 0, 0, 0, 0, 224, 224, 224, 194, 224, 224, 224, 193, 0, 0, 0, 0, 224, 224, 224, 194, 224, 224, 224, 193, 0, 0, 0, 0, 224, 224, 224, 194, 224, 224, 224, 193, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 153, 0, 0, 0, 0, 225, 225, 225, 191, 224, 224, 224, 189, 0, 0, 0, 0, 225, 225, 225, 191, 224, 224, 224, 189, 0, 0, 0, 0, 225, 225, 225, 191, 224, 224, 224, 189, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0),
|
||||
"format": "RGBA8",
|
||||
"height": 16,
|
||||
"mipmaps": false,
|
||||
"width": 16
|
||||
}
|
||||
|
||||
[sub_resource type="ImageTexture" id="ImageTexture_7udr7"]
|
||||
image = SubResource("Image_lnnjv")
|
||||
|
||||
[node name="FileSelect" type="HBoxContainer"]
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
size_flags_horizontal = 3
|
||||
script = ExtResource("1_lnnjv")
|
||||
|
||||
[node name="LineEdit" type="LineEdit" parent="."]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
tooltip_text = "Location where the OAuth Settings get saved to."
|
||||
|
||||
[node name="Button" type="Button" parent="."]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
icon = SubResource("ImageTexture_7udr7")
|
||||
|
||||
[node name="FileDialog" type="FileDialog" parent="."]
|
||||
unique_name_in_owner = true
|
||||
auto_translate_mode = 1
|
||||
initial_position = 1
|
||||
24
addons/twitcher/editor/setup/focus_child_show.gd
Normal file
24
addons/twitcher/editor/setup/focus_child_show.gd
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
extends Node
|
||||
|
||||
@export var show_elements: Array[Control] = []
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
for element: Control in show_elements:
|
||||
element.hide()
|
||||
|
||||
for child in get_children():
|
||||
if child.has_signal(&"focus_entered"):
|
||||
child.connect(&"focus_entered", _on_focus_entered)
|
||||
if child.has_signal(&"focus_exited"):
|
||||
child.connect(&"focus_exited", _on_focus_exited)
|
||||
|
||||
|
||||
func _on_focus_entered() -> void:
|
||||
for element: Node in show_elements:
|
||||
element.show()
|
||||
|
||||
|
||||
func _on_focus_exited() -> void:
|
||||
for element: Node in show_elements:
|
||||
element.hide()
|
||||
1
addons/twitcher/editor/setup/focus_child_show.gd.uid
Normal file
1
addons/twitcher/editor/setup/focus_child_show.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://ddugotjvuahex
|
||||
101
addons/twitcher/editor/setup/page_authorization.gd
Normal file
101
addons/twitcher/editor/setup/page_authorization.gd
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
@tool
|
||||
extends Node
|
||||
|
||||
const TwitchEditorSettings = preload("res://addons/twitcher/editor/twitch_editor_settings.gd")
|
||||
const TwitchTweens = preload("res://addons/twitcher/editor/twitch_tweens.gd")
|
||||
const TWITCH_SERVICE = preload("res://addons/twitcher/twitch_service.tscn")
|
||||
|
||||
@onready var authorization_explaination: RichTextLabel = %AuthExplain
|
||||
|
||||
@onready var client_id: LineEdit = %ClientId
|
||||
@onready var client_secret: LineEdit = %ClientSecret
|
||||
@onready var redirect_url: LineEdit = %RedirectURL
|
||||
|
||||
@onready var oauth_setting_file_select: FileSelect = %OauthSettingFileSelect
|
||||
@onready var token_file_select: FileSelect = %TokenFileSelect
|
||||
|
||||
@onready var to_documentation: Button = %ToDocumentation
|
||||
|
||||
@onready var o_auth_save: Button = %OAuthSave
|
||||
@onready var test_response: Label = %TestResponse
|
||||
|
||||
var has_changes: bool:
|
||||
set(val):
|
||||
has_changes = val
|
||||
changed.emit.call_deferred()
|
||||
o_auth_save.text = o_auth_save.text.trim_suffix(" (unsaved changes)")
|
||||
if has_changes: o_auth_save.text += " (unsaved changes)"
|
||||
|
||||
signal changed
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
authorization_explaination.meta_clicked.connect(_on_link_clicked)
|
||||
|
||||
redirect_url.text_changed.connect(_on_text_changed)
|
||||
client_id.text_changed.connect(_on_text_changed)
|
||||
client_secret.text_changed.connect(_on_text_changed)
|
||||
to_documentation.pressed.connect(_on_to_documentation_pressed)
|
||||
oauth_setting_file_select.file_selected.connect(_on_file_changed)
|
||||
token_file_select.file_selected.connect(_on_file_changed)
|
||||
|
||||
o_auth_save.pressed.connect(_on_save)
|
||||
|
||||
_load_oauth_setting()
|
||||
|
||||
|
||||
func _load_oauth_setting() -> void:
|
||||
var setting: OAuthSetting = TwitchEditorSettings.editor_oauth_setting
|
||||
client_id.text = setting.client_id
|
||||
client_secret.text = setting.get_client_secret()
|
||||
redirect_url.text = setting.redirect_url
|
||||
|
||||
|
||||
func _on_link_clicked(link: Variant) -> void:
|
||||
OS.shell_open(link)
|
||||
|
||||
|
||||
func _on_text_changed(val: String) -> void:
|
||||
reset_response_message()
|
||||
var setting: OAuthSetting = TwitchEditorSettings.editor_oauth_setting
|
||||
setting.client_id = client_id.text
|
||||
setting.set_client_secret(client_secret.text)
|
||||
setting.redirect_url = redirect_url.text
|
||||
has_changes = true
|
||||
|
||||
|
||||
func reset_response_message() -> void:
|
||||
test_response.text = ""
|
||||
|
||||
|
||||
func _on_file_changed() -> void:
|
||||
has_changes = true
|
||||
|
||||
|
||||
func is_auth_existing() -> bool:
|
||||
return is_instance_valid(TwitchEditorSettings.editor_oauth_setting)
|
||||
|
||||
|
||||
func _on_save() -> void:
|
||||
TwitchEditorSettings.save_editor_oauth_setting()
|
||||
TwitchEditorSettings.save_editor_oauth_token()
|
||||
|
||||
var setting_path = oauth_setting_file_select.path
|
||||
var setting = TwitchEditorSettings.editor_oauth_setting.duplicate(true)
|
||||
setting.take_over_path(setting_path)
|
||||
ResourceSaver.save(setting, setting_path)
|
||||
TwitchEditorSettings.game_oauth_setting = setting
|
||||
|
||||
var token_path = token_file_select.path
|
||||
var token = TwitchEditorSettings.editor_oauth_token.duplicate()
|
||||
token.take_over_path(token_path)
|
||||
ResourceSaver.save(token, token_path)
|
||||
TwitchEditorSettings.game_oauth_token = token
|
||||
|
||||
TwitchTweens.flash(o_auth_save, Color.GREEN)
|
||||
ProjectSettings.save()
|
||||
has_changes = false
|
||||
|
||||
|
||||
func _on_to_documentation_pressed() -> void:
|
||||
OS.shell_open("https://dev.twitch.tv/docs/authentication/")
|
||||
1
addons/twitcher/editor/setup/page_authorization.gd.uid
Normal file
1
addons/twitcher/editor/setup/page_authorization.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://dxql15j5ornlc
|
||||
182
addons/twitcher/editor/setup/page_authorization.tscn
Normal file
182
addons/twitcher/editor/setup/page_authorization.tscn
Normal file
|
|
@ -0,0 +1,182 @@
|
|||
[gd_scene load_steps=6 format=3 uid="uid://dm6jvnuikxtei"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://dxql15j5ornlc" path="res://addons/twitcher/editor/setup/page_authorization.gd" id="1_78hk7"]
|
||||
[ext_resource type="LabelSettings" uid="uid://bnsxy6gcm8q11" path="res://addons/twitcher/assets/title_label_settings.tres" id="2_owlil"]
|
||||
[ext_resource type="PackedScene" uid="uid://b7smp156mdns6" path="res://addons/twitcher/editor/setup/file_select.tscn" id="3_dbhpx"]
|
||||
[ext_resource type="Script" uid="uid://ddugotjvuahex" path="res://addons/twitcher/editor/setup/focus_child_show.gd" id="3_o4tdm"]
|
||||
[ext_resource type="PackedScene" uid="uid://bfksyo3klyvdn" path="res://addons/twitcher/editor/setup/test_credentials.tscn" id="5_dbhpx"]
|
||||
|
||||
[node name="Authorization" type="MarginContainer"]
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
theme_override_constants/margin_left = 10
|
||||
theme_override_constants/margin_top = 10
|
||||
theme_override_constants/margin_right = 10
|
||||
theme_override_constants/margin_bottom = 10
|
||||
script = ExtResource("1_78hk7")
|
||||
metadata/_tab_index = 1
|
||||
|
||||
[node name="Layout" type="VBoxContainer" parent="."]
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 3
|
||||
|
||||
[node name="Title" type="Label" parent="Layout"]
|
||||
layout_mode = 2
|
||||
text = "Step 2: Authorization"
|
||||
label_settings = ExtResource("2_owlil")
|
||||
horizontal_alignment = 1
|
||||
|
||||
[node name="ToDocumentation" type="Button" parent="Layout/Title"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 1
|
||||
anchors_preset = 6
|
||||
anchor_left = 1.0
|
||||
anchor_top = 0.5
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 0.5
|
||||
offset_left = -52.0
|
||||
offset_top = -15.5
|
||||
offset_bottom = 15.5
|
||||
grow_horizontal = 0
|
||||
grow_vertical = 2
|
||||
text = "DOCS"
|
||||
metadata/_edit_use_anchors_ = true
|
||||
|
||||
[node name="PanelContainer" type="PanelContainer" parent="Layout"]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="MarginContainer" type="MarginContainer" parent="Layout/PanelContainer"]
|
||||
layout_mode = 2
|
||||
theme_override_constants/margin_left = 10
|
||||
theme_override_constants/margin_top = 10
|
||||
theme_override_constants/margin_right = 10
|
||||
theme_override_constants/margin_bottom = 10
|
||||
|
||||
[node name="VBoxContainer" type="VBoxContainer" parent="Layout/PanelContainer/MarginContainer"]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="AuthorizationTitle" type="Label" parent="Layout/PanelContainer/MarginContainer/VBoxContainer"]
|
||||
layout_mode = 2
|
||||
text = "General"
|
||||
label_settings = ExtResource("2_owlil")
|
||||
horizontal_alignment = 1
|
||||
|
||||
[node name="AuthExplain" type="RichTextLabel" parent="Layout/PanelContainer/MarginContainer/VBoxContainer"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
theme_override_font_sizes/normal_font_size = 12
|
||||
bbcode_enabled = true
|
||||
text = "The credentials that are used by the editor and game to connect to Twitch. You can request your credentials [url=https://dev.twitch.tv/console/apps/create]Twitch Dev Console[/url] for more informations see [url=https://twitcher.kani.dev/#authorization]Documentation[/url]"
|
||||
fit_content = true
|
||||
vertical_alignment = 1
|
||||
|
||||
[node name="AuthorizationOptions" type="GridContainer" parent="Layout/PanelContainer/MarginContainer/VBoxContainer" node_paths=PackedStringArray("show_elements")]
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 3
|
||||
size_flags_stretch_ratio = 3.0
|
||||
columns = 2
|
||||
script = ExtResource("3_o4tdm")
|
||||
show_elements = [NodePath("../AuthExplain")]
|
||||
|
||||
[node name="ClientIdLabel" type="Label" parent="Layout/PanelContainer/MarginContainer/VBoxContainer/AuthorizationOptions"]
|
||||
layout_mode = 2
|
||||
text = "Client ID:"
|
||||
|
||||
[node name="ClientId" type="LineEdit" parent="Layout/PanelContainer/MarginContainer/VBoxContainer/AuthorizationOptions"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
text = "1ae0idgxbvn6vi97ls7d89cyd919oq"
|
||||
|
||||
[node name="ClientSecretLabel" type="Label" parent="Layout/PanelContainer/MarginContainer/VBoxContainer/AuthorizationOptions"]
|
||||
layout_mode = 2
|
||||
text = "Client Secret:"
|
||||
|
||||
[node name="ClientSecret" type="LineEdit" parent="Layout/PanelContainer/MarginContainer/VBoxContainer/AuthorizationOptions"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
text = "k22x037lmmrmkmwvy79xr19qfy993g"
|
||||
secret = true
|
||||
|
||||
[node name="RedirectURLLabel" type="Label" parent="Layout/PanelContainer/MarginContainer/VBoxContainer/AuthorizationOptions"]
|
||||
layout_mode = 2
|
||||
text = "Redirect URL:"
|
||||
|
||||
[node name="RedirectURL" type="LineEdit" parent="Layout/PanelContainer/MarginContainer/VBoxContainer/AuthorizationOptions"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
tooltip_text = "Location that Twitch is calling after the login process. Take care that this one is the same that you used during creation of the application within the twitch dev console."
|
||||
text = "http://localhost:7170"
|
||||
|
||||
[node name="HSeparator2" type="HSeparator" parent="Layout/PanelContainer/MarginContainer/VBoxContainer"]
|
||||
layout_mode = 2
|
||||
theme_override_constants/separation = 10
|
||||
|
||||
[node name="GameSettingTitle" type="Label" parent="Layout/PanelContainer/MarginContainer/VBoxContainer"]
|
||||
layout_mode = 2
|
||||
text = "Game"
|
||||
label_settings = ExtResource("2_owlil")
|
||||
horizontal_alignment = 1
|
||||
|
||||
[node name="GameExplain" type="RichTextLabel" parent="Layout/PanelContainer/MarginContainer/VBoxContainer"]
|
||||
layout_mode = 2
|
||||
theme_override_font_sizes/normal_font_size = 12
|
||||
bbcode_enabled = true
|
||||
text = "These settings are needed for the game to connect with Twitch."
|
||||
fit_content = true
|
||||
vertical_alignment = 1
|
||||
|
||||
[node name="GameSetting" type="GridContainer" parent="Layout/PanelContainer/MarginContainer/VBoxContainer" node_paths=PackedStringArray("show_elements")]
|
||||
layout_mode = 2
|
||||
columns = 2
|
||||
script = ExtResource("3_o4tdm")
|
||||
show_elements = [NodePath("../GameExplain")]
|
||||
|
||||
[node name="OauthSettingLabel" type="Label" parent="Layout/PanelContainer/MarginContainer/VBoxContainer/GameSetting"]
|
||||
layout_mode = 2
|
||||
text = "Auth File Path:"
|
||||
|
||||
[node name="OauthSettingFileSelect" parent="Layout/PanelContainer/MarginContainer/VBoxContainer/GameSetting" instance=ExtResource("3_dbhpx")]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
default_path = "res://addons/twitcher/twitch_oauth_setting.tres"
|
||||
path = "res://addons/twitcher/twitch_oauth_setting.tres"
|
||||
filters = PackedStringArray("*.tres", "*.res")
|
||||
|
||||
[node name="TokenLabel" type="Label" parent="Layout/PanelContainer/MarginContainer/VBoxContainer/GameSetting"]
|
||||
layout_mode = 2
|
||||
text = "Token File Path:"
|
||||
|
||||
[node name="TokenFileSelect" parent="Layout/PanelContainer/MarginContainer/VBoxContainer/GameSetting" instance=ExtResource("3_dbhpx")]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
default_path = "res://addons/twitcher/default_oauth_token.tres"
|
||||
path = "res://addons/twitcher/default_oauth_token.tres"
|
||||
filters = PackedStringArray("*.tres", "*.res")
|
||||
|
||||
[node name="HSeparator" type="HSeparator" parent="Layout/PanelContainer/MarginContainer/VBoxContainer"]
|
||||
layout_mode = 2
|
||||
theme_override_constants/separation = 10
|
||||
|
||||
[node name="HBoxContainer" type="HBoxContainer" parent="Layout"]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="TestCredentials" parent="Layout/HBoxContainer" node_paths=PackedStringArray("test_response") instance=ExtResource("5_dbhpx")]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
test_response = NodePath("../../TestResponse")
|
||||
|
||||
[node name="OAuthSave" type="Button" parent="Layout/HBoxContainer"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
text = "Save"
|
||||
|
||||
[node name="TestResponse" type="Label" parent="Layout"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
139
addons/twitcher/editor/setup/page_use_case.gd
Normal file
139
addons/twitcher/editor/setup/page_use_case.gd
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
@tool
|
||||
extends MarginContainer
|
||||
|
||||
enum UseCase {
|
||||
Overlay, Game, Other
|
||||
}
|
||||
|
||||
const PRESET_GAME_SCOPES = preload("res://addons/twitcher/auth/preset_game_scopes.tres")
|
||||
const PRESET_OVERLAY_SCOPES = preload("res://addons/twitcher/auth/preset_overlay_scopes.tres")
|
||||
const TwitchScopeProperty = preload("res://addons/twitcher/editor/inspector/twitch_scope_property.gd")
|
||||
const TwitchEditorSettings = preload("res://addons/twitcher/editor/twitch_editor_settings.gd")
|
||||
const TwitchTweens = preload("res://addons/twitcher/editor/twitch_tweens.gd")
|
||||
|
||||
@export var choose_button_group: ButtonGroup
|
||||
|
||||
@onready var overlay: CheckBox = %Overlay
|
||||
@onready var game: CheckBox = %Game
|
||||
@onready var something_else: CheckBox = %SomethingElse
|
||||
@onready var scope_list: RichTextLabel = %ScopeList
|
||||
@onready var to_documentation: Button = %ToDocumentation
|
||||
@onready var scope_file_select: FileSelect = %ScopeFileSelect
|
||||
@onready var scopes_container: HBoxContainer = %Scopes
|
||||
@onready var advanced_edit: CheckButton = %AdvancedEdit
|
||||
@onready var other_scope_options: PanelContainer = %OtherScopeOptions
|
||||
@onready var extended_scope_info: PanelContainer = %ExtendedScopeInfo
|
||||
@onready var save: Button = %Save
|
||||
|
||||
var other_scope_property: TwitchScopeProperty = TwitchScopeProperty.new()
|
||||
var scopes: TwitchOAuthScopes: set = update_scopes
|
||||
var has_changes: bool:
|
||||
set(val):
|
||||
has_changes = val
|
||||
changed.emit()
|
||||
save.text = save.text.trim_suffix(" (unsaved changes)")
|
||||
if has_changes: save.text += " (unsaved changes)"
|
||||
|
||||
signal changed
|
||||
signal use_case_changed(use_case: UseCase)
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
to_documentation.pressed.connect(_on_to_documentation_pressed)
|
||||
choose_button_group.pressed.connect(_on_choose)
|
||||
scope_file_select.file_selected.connect(_on_scope_file_selected)
|
||||
save.pressed.connect(_on_save_pressed)
|
||||
other_scope_property.scope_selected.connect(_on_scope_info)
|
||||
advanced_edit.toggled.connect(_on_toggle_advanced_edit)
|
||||
|
||||
scopes_container.hide()
|
||||
extended_scope_info.hide()
|
||||
extended_scope_info.add_child(other_scope_property)
|
||||
|
||||
# Reset radio buttons cause it's a tool script and the radio button stay and won't throw another signal otherwise
|
||||
game.set_pressed_no_signal(false)
|
||||
overlay.set_pressed_no_signal(false)
|
||||
something_else.set_pressed_no_signal(false)
|
||||
|
||||
scope_file_select.path = TwitchEditorSettings.get_scope_path()
|
||||
match TwitchEditorSettings.project_preset:
|
||||
TwitchEditorSettings.PRESET_GAME:
|
||||
game.button_pressed = true
|
||||
TwitchEditorSettings.PRESET_OVERLAY:
|
||||
overlay.button_pressed = true
|
||||
TwitchEditorSettings.PRESET_OTHER:
|
||||
something_else.button_pressed = true
|
||||
|
||||
# Needs to be resetted cause the radio reset will change the has_changges to true
|
||||
has_changes = false
|
||||
|
||||
func _on_scope_file_selected(path: String) -> void:
|
||||
has_changes = true
|
||||
if FileAccess.file_exists(path):
|
||||
var resource = load(path)
|
||||
if resource is OAuthScopes: scopes = resource
|
||||
else: OS.alert("The selected scope is not a scope file, it will be overwritten!")
|
||||
|
||||
|
||||
func _on_toggle_advanced_edit(toggled_on: bool) -> void:
|
||||
extended_scope_info.visible = toggled_on
|
||||
|
||||
|
||||
func _on_choose(button: BaseButton) -> void:
|
||||
match button:
|
||||
overlay:
|
||||
use_case_changed.emit(UseCase.Overlay)
|
||||
scopes = PRESET_OVERLAY_SCOPES.duplicate(true)
|
||||
advanced_edit.button_pressed = false
|
||||
TwitchEditorSettings.project_preset = TwitchEditorSettings.PRESET_OVERLAY
|
||||
game:
|
||||
use_case_changed.emit(UseCase.Game)
|
||||
scopes = PRESET_GAME_SCOPES.duplicate(true)
|
||||
advanced_edit.button_pressed = false
|
||||
TwitchEditorSettings.project_preset = TwitchEditorSettings.PRESET_GAME
|
||||
something_else:
|
||||
use_case_changed.emit(UseCase.Other)
|
||||
scopes = TwitchOAuthScopes.new()
|
||||
advanced_edit.button_pressed = true
|
||||
TwitchEditorSettings.project_preset = TwitchEditorSettings.PRESET_OTHER
|
||||
|
||||
_show_selected_scopes()
|
||||
has_changes = true
|
||||
|
||||
func _on_scope_info(scope: TwitchScope.Definition) -> void:
|
||||
_show_selected_scopes()
|
||||
|
||||
|
||||
func _on_save_pressed() -> void:
|
||||
var s_path = scope_file_select.path
|
||||
scopes.take_over_path(s_path)
|
||||
ResourceSaver.save(scopes, s_path)
|
||||
TwitchEditorSettings.set_scope_path(s_path)
|
||||
TwitchTweens.flash(save, Color.GREEN)
|
||||
has_changes = false
|
||||
|
||||
|
||||
func _show_selected_scopes() -> void:
|
||||
scopes_container.show()
|
||||
|
||||
if scopes.used_scopes.is_empty():
|
||||
scope_list.text = "[i]No scopes selected yet[/i]"
|
||||
return
|
||||
|
||||
var scope_description: String = ""
|
||||
for scope_name: StringName in scopes.used_scopes:
|
||||
var scope: TwitchScope.Definition = TwitchScope.SCOPE_MAP[scope_name]
|
||||
scope_description += "[b]%s[/b] - %s\n\n" % [scope.value, scope.description]
|
||||
|
||||
scope_list.text = scope_description
|
||||
|
||||
|
||||
func _on_to_documentation_pressed() -> void:
|
||||
OS.shell_open("https://dev.twitch.tv/docs/authentication/scopes/")
|
||||
|
||||
|
||||
func update_scopes(val: TwitchOAuthScopes) -> void:
|
||||
scopes = val
|
||||
other_scope_property.set_object_and_property(scopes, "")
|
||||
other_scope_property.update_property()
|
||||
_show_selected_scopes()
|
||||
1
addons/twitcher/editor/setup/page_use_case.gd.uid
Normal file
1
addons/twitcher/editor/setup/page_use_case.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://cjni881olloyf
|
||||
162
addons/twitcher/editor/setup/page_use_case.tscn
Normal file
162
addons/twitcher/editor/setup/page_use_case.tscn
Normal file
|
|
@ -0,0 +1,162 @@
|
|||
[gd_scene load_steps=5 format=3 uid="uid://c7pja1druikbn"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://cjni881olloyf" path="res://addons/twitcher/editor/setup/page_use_case.gd" id="1_2qemh"]
|
||||
[ext_resource type="LabelSettings" uid="uid://bnsxy6gcm8q11" path="res://addons/twitcher/assets/title_label_settings.tres" id="1_r6qea"]
|
||||
[ext_resource type="ButtonGroup" uid="uid://bkocyfdqvh4t" path="res://addons/twitcher/editor/setup/use_case_button_group.tres" id="1_vqr26"]
|
||||
[ext_resource type="PackedScene" uid="uid://b7smp156mdns6" path="res://addons/twitcher/editor/setup/file_select.tscn" id="4_c6y6e"]
|
||||
|
||||
[node name="UseCase" type="MarginContainer"]
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
theme_override_constants/margin_left = 10
|
||||
theme_override_constants/margin_top = 10
|
||||
theme_override_constants/margin_right = 10
|
||||
theme_override_constants/margin_bottom = 10
|
||||
script = ExtResource("1_2qemh")
|
||||
choose_button_group = ExtResource("1_vqr26")
|
||||
metadata/_tab_index = 0
|
||||
|
||||
[node name="ScrollContainer" type="ScrollContainer" parent="."]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="SelectionContainer" type="VBoxContainer" parent="ScrollContainer"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
size_flags_vertical = 3
|
||||
|
||||
[node name="Title" type="Label" parent="ScrollContainer/SelectionContainer"]
|
||||
layout_mode = 2
|
||||
text = "Step 1: Use Case"
|
||||
label_settings = ExtResource("1_r6qea")
|
||||
horizontal_alignment = 1
|
||||
|
||||
[node name="ToDocumentation" type="Button" parent="ScrollContainer/SelectionContainer/Title"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 1
|
||||
anchors_preset = 6
|
||||
anchor_left = 1.0
|
||||
anchor_top = 0.5
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 0.5
|
||||
offset_left = -52.0
|
||||
offset_top = -15.5
|
||||
offset_bottom = 15.5
|
||||
grow_horizontal = 0
|
||||
grow_vertical = 2
|
||||
text = "DOCS"
|
||||
metadata/_edit_use_anchors_ = true
|
||||
|
||||
[node name="Explaination" type="RichTextLabel" parent="ScrollContainer/SelectionContainer"]
|
||||
layout_mode = 2
|
||||
theme_override_font_sizes/normal_font_size = 12
|
||||
bbcode_enabled = true
|
||||
text = "To help you with scopes and authentication please select your use case."
|
||||
fit_content = true
|
||||
vertical_alignment = 1
|
||||
|
||||
[node name="ChooseLabel" type="Label" parent="ScrollContainer/SelectionContainer"]
|
||||
layout_mode = 2
|
||||
text = "What do you want to make:"
|
||||
|
||||
[node name="Overlay" type="CheckBox" parent="ScrollContainer/SelectionContainer"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
button_pressed = true
|
||||
button_group = ExtResource("1_vqr26")
|
||||
text = "Overlay"
|
||||
|
||||
[node name="Game" type="CheckBox" parent="ScrollContainer/SelectionContainer"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
button_group = ExtResource("1_vqr26")
|
||||
text = "Game"
|
||||
|
||||
[node name="SomethingElse" type="CheckBox" parent="ScrollContainer/SelectionContainer"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
button_group = ExtResource("1_vqr26")
|
||||
text = "I know what I do / Something else"
|
||||
|
||||
[node name="HSeparator" type="HSeparator" parent="ScrollContainer/SelectionContainer"]
|
||||
layout_mode = 2
|
||||
theme_override_constants/separation = 20
|
||||
|
||||
[node name="Scopes" type="HBoxContainer" parent="ScrollContainer/SelectionContainer"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 3
|
||||
|
||||
[node name="ExtendedScopeInfo" type="PanelContainer" parent="ScrollContainer/SelectionContainer/Scopes"]
|
||||
unique_name_in_owner = true
|
||||
visible = false
|
||||
layout_mode = 2
|
||||
|
||||
[node name="OtherScopeOptions" type="PanelContainer" parent="ScrollContainer/SelectionContainer/Scopes"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
|
||||
[node name="VBoxContainer" type="VBoxContainer" parent="ScrollContainer/SelectionContainer/Scopes/OtherScopeOptions"]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="ScopeListLabel" type="Label" parent="ScrollContainer/SelectionContainer/Scopes/OtherScopeOptions/VBoxContainer"]
|
||||
layout_mode = 2
|
||||
text = "Scopes:"
|
||||
label_settings = ExtResource("1_r6qea")
|
||||
|
||||
[node name="AdvancedEdit" type="CheckButton" parent="ScrollContainer/SelectionContainer/Scopes/OtherScopeOptions/VBoxContainer/ScopeListLabel"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 1
|
||||
anchors_preset = 6
|
||||
anchor_left = 1.0
|
||||
anchor_top = 0.5
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 0.5
|
||||
offset_left = -168.0
|
||||
offset_top = -15.5
|
||||
offset_bottom = 15.5
|
||||
grow_horizontal = 0
|
||||
grow_vertical = 2
|
||||
text = "Edit (Advanced)"
|
||||
|
||||
[node name="ScopeList" type="RichTextLabel" parent="ScrollContainer/SelectionContainer/Scopes/OtherScopeOptions/VBoxContainer"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
bbcode_enabled = true
|
||||
text = "[b]user:read:chat[/b] - Receive chatroom messages and informational notifications relating to a channel’s chatroom.
|
||||
|
||||
[b]user:write:chat[/b] - Send chat messages to a chatroom.
|
||||
|
||||
[b]moderator:read:followers[/b] - Read the followers of a broadcaster.
|
||||
|
||||
[b]bits:read[/b] - View Bits information for a channel.
|
||||
|
||||
[b]channel:read:redemptions[/b] - View Channel Points custom rewards and their redemptions on a channel.
|
||||
|
||||
[b]channel:manage:redemptions[/b] - Manage Channel Points custom rewards and their redemptions on a channel.
|
||||
|
||||
"
|
||||
fit_content = true
|
||||
|
||||
[node name="HSeparator" type="HSeparator" parent="ScrollContainer/SelectionContainer/Scopes/OtherScopeOptions/VBoxContainer"]
|
||||
layout_mode = 2
|
||||
theme_override_constants/separation = 10
|
||||
|
||||
[node name="ScopeFileLabel" type="Label" parent="ScrollContainer/SelectionContainer/Scopes/OtherScopeOptions/VBoxContainer"]
|
||||
layout_mode = 2
|
||||
text = "Save selected scopes:"
|
||||
|
||||
[node name="ScopeFileSelect" parent="ScrollContainer/SelectionContainer/Scopes/OtherScopeOptions/VBoxContainer" instance=ExtResource("4_c6y6e")]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
default_path = "res://twitch_scopes.tres"
|
||||
path = "res://twitch_scopes.tres"
|
||||
filters = PackedStringArray("*.tres", "*.res")
|
||||
|
||||
[node name="Save" type="Button" parent="ScrollContainer/SelectionContainer/Scopes/OtherScopeOptions/VBoxContainer"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
text = "Save Scopes"
|
||||
74
addons/twitcher/editor/setup/page_utilities.gd
Normal file
74
addons/twitcher/editor/setup/page_utilities.gd
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
@tool
|
||||
extends MarginContainer
|
||||
|
||||
const script_path: String = "res://addons/twitcher/twitch_service.tscn"
|
||||
const autoload_name: String = "Twitch"
|
||||
const setting_key: String = "autoload/%s" % autoload_name
|
||||
const setting_value: String = "*" + script_path
|
||||
|
||||
@onready var autoload_install: Button = %AutoloadInstall
|
||||
@onready var autoload_info: Label = %AutoloadInfo
|
||||
@onready var autoload_description: RichTextLabel = %AutoloadDescription
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
autoload_description.text = autoload_description.text.format({
|
||||
"autoload_name": autoload_name
|
||||
})
|
||||
autoload_install.pressed.connect(_on_install_autoload_pressed)
|
||||
_update_install_autoload()
|
||||
|
||||
|
||||
func _update_install_autoload() -> void:
|
||||
if ProjectSettings.has_setting(setting_key):
|
||||
autoload_install.text = "Uninstall Autoload"
|
||||
else:
|
||||
autoload_install.text = "Install Autoload"
|
||||
|
||||
|
||||
func _on_install_autoload_pressed() -> void:
|
||||
if ProjectSettings.has_setting(setting_key):
|
||||
_uninstall_autoload()
|
||||
else:
|
||||
_install_autload()
|
||||
_update_install_autoload()
|
||||
|
||||
|
||||
func _uninstall_autoload() -> void:
|
||||
ProjectSettings.clear(setting_key)
|
||||
|
||||
var err = ProjectSettings.save()
|
||||
|
||||
if err == OK:
|
||||
autoload_info.text = "Autoload '%s' uninstalled successfully!\nYou might need to reload the current project for changes to fully apply everywhere in the editor immediately." % autoload_name
|
||||
print("Successfully removed autoload: %s" % autoload_name)
|
||||
else:
|
||||
autoload_info.text = "Failed to save project settings.\nError code: %s" % error_string(err)
|
||||
printerr("Failed to save project settings! Error: ", error_string(err))
|
||||
|
||||
|
||||
func _install_autload() -> void:
|
||||
if not FileAccess.file_exists(script_path):
|
||||
OS.alert("The TwitchService file does not exist at:\n" + script_path, "Error")
|
||||
return
|
||||
|
||||
var setting_key: String = "autoload/%s" % autoload_name
|
||||
var setting_value: String = "*" + script_path
|
||||
if ProjectSettings.has_setting(setting_key):
|
||||
var existing_value = ProjectSettings.get_setting(setting_key)
|
||||
if existing_value == setting_value:
|
||||
autoload_info.text = "Autoload '%s' with the same path is already installed." % autoload_name
|
||||
return
|
||||
else:
|
||||
autoload_info.text = "Autoload '%s' already exists but points to a different path (%s)." % [autoload_name, existing_value]
|
||||
return
|
||||
|
||||
ProjectSettings.set_setting(setting_key, setting_value)
|
||||
var err = ProjectSettings.save()
|
||||
|
||||
if err == OK:
|
||||
autoload_info.text = "Autoload '%s' installed successfully!\nYou might need to reload the current project for changes to fully apply everywhere in the editor immediately." % autoload_name
|
||||
print("Successfully added autoload: %s -> %s" % [autoload_name, script_path])
|
||||
else:
|
||||
autoload_info.text = "Failed to save project settings.\nError code: %s" % error_string(err)
|
||||
printerr("Failed to save project settings! Error: ", error_string(err))
|
||||
1
addons/twitcher/editor/setup/page_utilities.gd.uid
Normal file
1
addons/twitcher/editor/setup/page_utilities.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://dw37fk6mah3jk
|
||||
96
addons/twitcher/editor/setup/page_utilities.tscn
Normal file
96
addons/twitcher/editor/setup/page_utilities.tscn
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
[gd_scene load_steps=4 format=3 uid="uid://d4l63q706mkhw"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://dw37fk6mah3jk" path="res://addons/twitcher/editor/setup/page_utilities.gd" id="1_sexj5"]
|
||||
[ext_resource type="LabelSettings" uid="uid://bnsxy6gcm8q11" path="res://addons/twitcher/assets/title_label_settings.tres" id="1_yi5sa"]
|
||||
[ext_resource type="LabelSettings" uid="uid://cng881nsuud80" path="res://addons/twitcher/assets/warning_label_settings.tres" id="3_fj7h2"]
|
||||
|
||||
[node name="Utilities" type="MarginContainer"]
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
theme_override_constants/margin_left = 10
|
||||
theme_override_constants/margin_top = 10
|
||||
theme_override_constants/margin_right = 10
|
||||
theme_override_constants/margin_bottom = 10
|
||||
script = ExtResource("1_sexj5")
|
||||
metadata/_tab_index = 2
|
||||
|
||||
[node name="ScrollContainer" type="ScrollContainer" parent="."]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="Container" type="VBoxContainer" parent="ScrollContainer"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
size_flags_vertical = 3
|
||||
|
||||
[node name="Header" type="Label" parent="ScrollContainer/Container"]
|
||||
layout_mode = 2
|
||||
text = "Utilities"
|
||||
label_settings = ExtResource("1_yi5sa")
|
||||
horizontal_alignment = 1
|
||||
|
||||
[node name="HSeparator" type="HSeparator" parent="ScrollContainer/Container"]
|
||||
layout_mode = 2
|
||||
theme_override_constants/separation = 20
|
||||
|
||||
[node name="AutoloadDescription" type="RichTextLabel" parent="ScrollContainer/Container"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
bbcode_enabled = true
|
||||
text = "For basic use cases an autoload is the easiest way to setup Twitcher. Adds an autoload named [b]{autoload_name}[/b]
|
||||
|
||||
[i]Advantage:[/i]
|
||||
+ easy access everywhere
|
||||
[i]Disadvantage:[/i]
|
||||
- It will be always initialized, even when you want to test a small scene standalone Tiwtcher will do authorization, subscribing to eventsub etc.
|
||||
|
||||
[b]Alternative:[/b]
|
||||
The first nodes in the scene tree of every major nodes like TwitchAPI, TwitchEventsub, TwitchChat, TwitchMediaLoader and TwitchService are available as Singleton via their instance variable.
|
||||
|
||||
Example
|
||||
[code]TwitchAPI.instance.get_users(...)[/code]"
|
||||
fit_content = true
|
||||
|
||||
[node name="AutoloadInstall" type="Button" parent="ScrollContainer/Container"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
text = "Uninstall Autoload"
|
||||
|
||||
[node name="AutoloadInfo" type="Label" parent="ScrollContainer/Container"]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(0, 100)
|
||||
layout_mode = 2
|
||||
label_settings = ExtResource("3_fj7h2")
|
||||
autowrap_mode = 3
|
||||
|
||||
[node name="HSeparator2" type="HSeparator" parent="ScrollContainer/Container"]
|
||||
layout_mode = 2
|
||||
theme_override_constants/separation = 20
|
||||
|
||||
[node name="AutoloadDescription2" type="RichTextLabel" parent="ScrollContainer/Container"]
|
||||
visible = false
|
||||
layout_mode = 2
|
||||
bbcode_enabled = true
|
||||
text = "For a game with Twitch integration you probably want to read the chat from the streamer to act on the commands. For that the streamer need to authorize your application. For the authorization flow use the [url=https://dev.twitch.tv/docs/authentication/getting-tokens-oauth/#device-code-grant-flow]Device Code Flow[/url] so that you don't share the client secret in the code.
|
||||
|
||||
|
||||
[b]Hints:[/b]
|
||||
- Take care that you take the least amount of scopes for the game otherwise you could scare the streamer away.
|
||||
[i]Advantage:[/i]
|
||||
+ easy access everywhere
|
||||
[i]Disadvantage:[/i]
|
||||
- It will be always initialized, even when you want to test a small scene standalone Tiwtcher will do authorization, subscribing to eventsub etc.
|
||||
|
||||
[b]Alternative:[/b]
|
||||
The first nodes in the scene tree of every major nodes like TwitchAPI, TwitchEventsub, TwitchChat, TwitchMediaLoader and TwitchService are available as Singleton via their instance variable.
|
||||
|
||||
Example
|
||||
[code]TwitchAPI.instance.get_users(...)[/code]"
|
||||
fit_content = true
|
||||
|
||||
[node name="Button" type="Button" parent="ScrollContainer/Container"]
|
||||
visible = false
|
||||
layout_mode = 2
|
||||
text = "Add Game Example"
|
||||
58
addons/twitcher/editor/setup/setup.gd
Normal file
58
addons/twitcher/editor/setup/setup.gd
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
@tool
|
||||
extends Window
|
||||
|
||||
const PageUseCase = preload("res://addons/twitcher/editor/setup/page_use_case.gd")
|
||||
const TwitchEditorSettings = preload("res://addons/twitcher/editor/twitch_editor_settings.gd")
|
||||
const PageAuthorization = preload("res://addons/twitcher/editor/setup/page_authorization.gd")
|
||||
|
||||
#Setup
|
||||
#- Check for Authorization Stuff
|
||||
#-- Client Credentials
|
||||
#-- Editor Token
|
||||
#-- Scopes
|
||||
#- Auth Button
|
||||
#- Create Base Node Structure
|
||||
|
||||
@onready var authorization: PageAuthorization = %Authorization
|
||||
@onready var use_case: PageUseCase = %UseCase as PageUseCase
|
||||
@onready var close: Button = %Close
|
||||
@onready var startup_check: CheckButton = %StartupCheck
|
||||
|
||||
|
||||
func _ready():
|
||||
close_requested.connect(_on_close)
|
||||
close.pressed.connect(_on_close)
|
||||
startup_check.toggled.connect(_on_toggle_startup_check)
|
||||
startup_check.button_pressed = TwitchEditorSettings.show_setup_on_startup
|
||||
use_case.changed.connect(_on_changed)
|
||||
authorization.changed.connect(_on_changed)
|
||||
pass
|
||||
|
||||
|
||||
func _on_changed() -> void:
|
||||
close.text = close.text.trim_suffix(" (unsaved changes)")
|
||||
if use_case.has_changes || authorization.has_changes:
|
||||
close.text = close.text + " (unsaved changes)"
|
||||
|
||||
|
||||
func _on_toggle_startup_check(toggle_on: bool) -> void:
|
||||
TwitchEditorSettings.show_setup_on_startup = toggle_on
|
||||
ProjectSettings.save()
|
||||
|
||||
|
||||
func _input(event: InputEvent) -> void:
|
||||
if event is InputEventKey:
|
||||
var key_event: InputEventKey = event as InputEventKey
|
||||
if key_event.keycode == KEY_ESCAPE:
|
||||
_on_close()
|
||||
|
||||
|
||||
func _on_close() -> void:
|
||||
if use_case.has_changes || authorization.has_changes:
|
||||
var popup = ConfirmationDialog.new()
|
||||
popup.dialog_text = "You have unsaved changes! Are you sure to close the setup?"
|
||||
popup.confirmed.connect(queue_free)
|
||||
add_child(popup)
|
||||
popup.popup_centered()
|
||||
else:
|
||||
queue_free()
|
||||
1
addons/twitcher/editor/setup/setup.gd.uid
Normal file
1
addons/twitcher/editor/setup/setup.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://bbguje3a0cl8t
|
||||
51
addons/twitcher/editor/setup/setup.tscn
Normal file
51
addons/twitcher/editor/setup/setup.tscn
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
[gd_scene load_steps=6 format=3 uid="uid://wu1fprbhr62"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://bbguje3a0cl8t" path="res://addons/twitcher/editor/setup/setup.gd" id="1_o5snq"]
|
||||
[ext_resource type="PackedScene" uid="uid://c7pja1druikbn" path="res://addons/twitcher/editor/setup/page_use_case.tscn" id="2_6678v"]
|
||||
[ext_resource type="PackedScene" uid="uid://dm6jvnuikxtei" path="res://addons/twitcher/editor/setup/page_authorization.tscn" id="3_qcivh"]
|
||||
[ext_resource type="PackedScene" uid="uid://d4l63q706mkhw" path="res://addons/twitcher/editor/setup/page_utilities.tscn" id="4_qcivh"]
|
||||
|
||||
[sub_resource type="ButtonGroup" id="ButtonGroup_6678v"]
|
||||
|
||||
[node name="SetupWindow" type="Window"]
|
||||
title = "Setup Twitcher"
|
||||
initial_position = 2
|
||||
size = Vector2i(800, 800)
|
||||
script = ExtResource("1_o5snq")
|
||||
|
||||
[node name="VBoxContainer" type="VBoxContainer" parent="."]
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
|
||||
[node name="Setup" type="TabContainer" parent="VBoxContainer"]
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 3
|
||||
current_tab = 0
|
||||
|
||||
[node name="UseCase" parent="VBoxContainer/Setup" instance=ExtResource("2_6678v")]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
choose_button_group = SubResource("ButtonGroup_6678v")
|
||||
|
||||
[node name="Authorization" parent="VBoxContainer/Setup" instance=ExtResource("3_qcivh")]
|
||||
unique_name_in_owner = true
|
||||
visible = false
|
||||
layout_mode = 2
|
||||
|
||||
[node name="Utilities" parent="VBoxContainer/Setup" instance=ExtResource("4_qcivh")]
|
||||
visible = false
|
||||
layout_mode = 2
|
||||
|
||||
[node name="StartupCheck" type="CheckButton" parent="VBoxContainer"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
button_pressed = true
|
||||
text = "Show on startup (you can open it via 'Project/Tools/Twitcher Setup')"
|
||||
|
||||
[node name="Close" type="Button" parent="VBoxContainer"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
text = "Close"
|
||||
58
addons/twitcher/editor/setup/test_credentials.gd
Normal file
58
addons/twitcher/editor/setup/test_credentials.gd
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
@tool
|
||||
extends Button
|
||||
|
||||
const TwitchTweens = preload("res://addons/twitcher/editor/twitch_tweens.gd")
|
||||
const TwitchEditorSettings = preload("res://addons/twitcher/editor/twitch_editor_settings.gd")
|
||||
|
||||
@export var oauth_setting: OAuthSetting: set = update_oauth_setting
|
||||
@export var oauth_token: OAuthToken: set = update_oauth_token
|
||||
@export var test_response: Label
|
||||
|
||||
@onready var twitch_auth: TwitchAuth = %TwitchAuth
|
||||
|
||||
signal authorized
|
||||
|
||||
func _ready() -> void:
|
||||
pressed.connect(_pressed)
|
||||
oauth_setting = TwitchEditorSettings.editor_oauth_setting
|
||||
oauth_token = TwitchEditorSettings.editor_oauth_token
|
||||
|
||||
|
||||
func _notification(what: int) -> void:
|
||||
if what == NOTIFICATION_PREDELETE:
|
||||
oauth_token.authorized.disconnect(_on_authorized)
|
||||
|
||||
|
||||
func _pressed() -> void:
|
||||
TwitchTweens.loading(self)
|
||||
await twitch_auth.authorize()
|
||||
|
||||
if twitch_auth.token.is_token_valid():
|
||||
if test_response:
|
||||
test_response.text = "Credentials are valid!"
|
||||
test_response.add_theme_color_override(&"font_color", Color.GREEN)
|
||||
TwitchTweens.flash(self, Color.GREEN)
|
||||
authorized.emit()
|
||||
else:
|
||||
if test_response:
|
||||
test_response.text = "Credentials are invalid!"
|
||||
test_response.add_theme_color_override(&"font_color", Color.RED)
|
||||
TwitchTweens.flash(self, Color.RED)
|
||||
|
||||
|
||||
func update_oauth_token(new_oauth_token: OAuthToken) -> void:
|
||||
oauth_token = new_oauth_token
|
||||
oauth_token.authorized.connect(_on_authorized)
|
||||
if is_inside_tree():
|
||||
twitch_auth.token = new_oauth_token
|
||||
|
||||
|
||||
func update_oauth_setting(new_oauth_setting: OAuthSetting) -> void:
|
||||
oauth_setting = new_oauth_setting
|
||||
disabled = not oauth_setting.is_valid()
|
||||
if is_inside_tree():
|
||||
twitch_auth.oauth_setting = oauth_setting
|
||||
|
||||
|
||||
func _on_authorized() -> void:
|
||||
if is_inside_tree(): authorized.emit()
|
||||
1
addons/twitcher/editor/setup/test_credentials.gd.uid
Normal file
1
addons/twitcher/editor/setup/test_credentials.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://13afcys4swos
|
||||
37
addons/twitcher/editor/setup/test_credentials.tscn
Normal file
37
addons/twitcher/editor/setup/test_credentials.tscn
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
[gd_scene load_steps=9 format=3 uid="uid://bfksyo3klyvdn"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://13afcys4swos" path="res://addons/twitcher/editor/setup/test_credentials.gd" id="1_j2v3o"]
|
||||
[ext_resource type="Script" uid="uid://iv0mgv0lu8b0" path="res://addons/twitcher/auth/twitch_auth.gd" id="1_kojf4"]
|
||||
[ext_resource type="Script" uid="uid://b3n3et8mebjcc" path="res://addons/twitcher/auth/twitch_oauth_scopes.gd" id="3_2rqpn"]
|
||||
[ext_resource type="Resource" path="user://editor_oauth_setting.tres" id="3_kojf4"]
|
||||
[ext_resource type="Resource" path="user://editor_oauth_token.tres" id="4_j2v3o"]
|
||||
[ext_resource type="Script" uid="uid://bf0wi70haua35" path="res://addons/twitcher/lib/oOuch/oauth.gd" id="6_hkawa"]
|
||||
[ext_resource type="Script" uid="uid://blnbogtrshw4r" path="res://addons/twitcher/auth/twitch_token_handler.gd" id="7_v5ghs"]
|
||||
|
||||
[sub_resource type="Resource" id="Resource_1cpcx"]
|
||||
script = ExtResource("3_2rqpn")
|
||||
used_scopes = Array[StringName]([])
|
||||
metadata/_custom_type_script = "uid://b3n3et8mebjcc"
|
||||
|
||||
[node name="TestCredentials" type="Button"]
|
||||
text = "Test Credentials"
|
||||
script = ExtResource("1_j2v3o")
|
||||
|
||||
[node name="TwitchAuth" type="Node" parent="."]
|
||||
unique_name_in_owner = true
|
||||
script = ExtResource("1_kojf4")
|
||||
oauth_setting = ExtResource("3_kojf4")
|
||||
token = ExtResource("4_j2v3o")
|
||||
scopes = SubResource("Resource_1cpcx")
|
||||
metadata/_custom_type_script = "uid://iv0mgv0lu8b0"
|
||||
|
||||
[node name="OAuth" type="Node" parent="TwitchAuth" node_paths=PackedStringArray("token_handler")]
|
||||
script = ExtResource("6_hkawa")
|
||||
oauth_setting = ExtResource("3_kojf4")
|
||||
scopes = SubResource("Resource_1cpcx")
|
||||
token_handler = NodePath("../TokenHandler")
|
||||
|
||||
[node name="TokenHandler" type="Node" parent="TwitchAuth"]
|
||||
script = ExtResource("7_v5ghs")
|
||||
oauth_setting = ExtResource("3_kojf4")
|
||||
token = ExtResource("4_j2v3o")
|
||||
3
addons/twitcher/editor/setup/use_case_button_group.tres
Normal file
3
addons/twitcher/editor/setup/use_case_button_group.tres
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
[gd_resource type="ButtonGroup" format=3 uid="uid://bkocyfdqvh4t"]
|
||||
|
||||
[resource]
|
||||
145
addons/twitcher/editor/twitch_editor_settings.gd
Normal file
145
addons/twitcher/editor/twitch_editor_settings.gd
Normal file
|
|
@ -0,0 +1,145 @@
|
|||
@tool
|
||||
extends RefCounted
|
||||
|
||||
## Utilitiy Node for Inspector to calling API functionallity at any point
|
||||
|
||||
const EDITOR_OAUTH_TOKEN: String = "user://editor_oauth_token.tres"
|
||||
const EDITOR_OAUTH_SETTING: String = "user://editor_oauth_setting.tres"
|
||||
const GAME_OAUTH_TOKEN: String = "res://twitch_oauth_token.tres"
|
||||
const GAME_OAUTH_SETTING: String = "res://twitch_oauth_setting.tres"
|
||||
const TWITCH_DEFAULT_SCOPE: String = "res://addons/twitcher/auth/preset_overlay_scopes.tres"
|
||||
|
||||
const PRESET_GAME: StringName = &"Game"
|
||||
const PRESET_OVERLAY: StringName = &"Overlay"
|
||||
const PRESET_OTHER: StringName = &"Other"
|
||||
|
||||
static var _editor_oauth_token_property: ProjectSettingProperty
|
||||
static var editor_oauth_token: OAuthToken:
|
||||
set(val):
|
||||
editor_oauth_token = val
|
||||
_editor_oauth_token_property.set_val(val.resource_path)
|
||||
|
||||
static var _editor_oauth_setting_property: ProjectSettingProperty
|
||||
static var editor_oauth_setting: OAuthSetting:
|
||||
set(val):
|
||||
editor_oauth_setting = val
|
||||
_editor_oauth_setting_property.set_val(val.resource_path)
|
||||
|
||||
static var _game_oauth_token_property: ProjectSettingProperty
|
||||
static var game_oauth_token: OAuthToken:
|
||||
set(val):
|
||||
game_oauth_token = val
|
||||
_game_oauth_token_property.set_val(val.resource_path)
|
||||
|
||||
static var _game_oauth_setting_property: ProjectSettingProperty
|
||||
static var game_oauth_setting: OAuthSetting:
|
||||
set(val):
|
||||
game_oauth_setting = val
|
||||
_game_oauth_setting_property.set_val(val.resource_path)
|
||||
|
||||
static var _scope_property: ProjectSettingProperty
|
||||
static var scopes: TwitchOAuthScopes:
|
||||
set(val):
|
||||
scopes = val
|
||||
_scope_property.set_val(val.resource_path)
|
||||
|
||||
static var _show_setup_on_startup: ProjectSettingProperty
|
||||
static var show_setup_on_startup: bool:
|
||||
set(val): _show_setup_on_startup.set_val(val)
|
||||
get: return _show_setup_on_startup.get_val()
|
||||
|
||||
static var _project_preset: ProjectSettingProperty
|
||||
static var project_preset: StringName:
|
||||
set(val): _project_preset.set_val(val)
|
||||
get: return _project_preset.get_val()
|
||||
|
||||
static var _initialized: bool
|
||||
|
||||
|
||||
static func setup() -> void:
|
||||
if not _initialized:
|
||||
_initialized = true
|
||||
_setup_project_settings()
|
||||
_reload_setting()
|
||||
|
||||
|
||||
static func _setup_project_settings() -> void:
|
||||
_editor_oauth_token_property = ProjectSettingProperty.new("twitcher/editor/editor_oauth_token", EDITOR_OAUTH_TOKEN)
|
||||
_editor_oauth_token_property.as_file("*.res,*.tres")
|
||||
|
||||
_editor_oauth_setting_property = ProjectSettingProperty.new("twitcher/editor/editor_oauth_setting", EDITOR_OAUTH_SETTING)
|
||||
_editor_oauth_setting_property.as_file("*.res,*.tres")
|
||||
|
||||
_game_oauth_token_property = ProjectSettingProperty.new("twitcher/editor/game_oauth_token", GAME_OAUTH_TOKEN)
|
||||
_game_oauth_token_property.as_file("*.res,*.tres")
|
||||
|
||||
_game_oauth_setting_property = ProjectSettingProperty.new("twitcher/editor/game_oauth_setting", GAME_OAUTH_SETTING)
|
||||
_game_oauth_setting_property.as_file("*.res,*.tres")
|
||||
|
||||
_scope_property = ProjectSettingProperty.new("twitcher/editor/default_scopes", TWITCH_DEFAULT_SCOPE)
|
||||
_scope_property.as_file("*.res,*.tres")
|
||||
|
||||
_show_setup_on_startup = ProjectSettingProperty.new("twitcher/editor/show_setup_on_startup", true)
|
||||
|
||||
_project_preset = ProjectSettingProperty.new("twitcher/editor/project_preset")
|
||||
_project_preset.as_select([PRESET_GAME, PRESET_OVERLAY, PRESET_OTHER], false)
|
||||
|
||||
|
||||
static func _reload_setting() -> void:
|
||||
editor_oauth_setting = load(_editor_oauth_setting_property.get_val())
|
||||
editor_oauth_token = load(_editor_oauth_token_property.get_val())
|
||||
game_oauth_setting = load(_game_oauth_setting_property.get_val())
|
||||
game_oauth_token = load(_game_oauth_token_property.get_val())
|
||||
|
||||
var editor_oauth_token_path: String = _editor_oauth_token_property.get_val()
|
||||
if editor_oauth_token_path:
|
||||
if not FileAccess.file_exists(editor_oauth_token_path):
|
||||
_create_editor_oauth_token()
|
||||
|
||||
var editor_oauth_setting_path: String = _editor_oauth_setting_property.get_val()
|
||||
if editor_oauth_setting_path:
|
||||
if not FileAccess.file_exists(editor_oauth_setting_path):
|
||||
_create_editor_oauth_setting()
|
||||
|
||||
var scope_path: String = get_scope_path()
|
||||
if scope_path and FileAccess.file_exists(scope_path):
|
||||
scopes = load(scope_path)
|
||||
|
||||
|
||||
static func save_editor_oauth_setting() -> void:
|
||||
ResourceSaver.save(editor_oauth_setting)
|
||||
|
||||
|
||||
static func save_editor_oauth_token() -> void:
|
||||
ResourceSaver.save(editor_oauth_token)
|
||||
|
||||
|
||||
static func get_scope_path() -> String:
|
||||
return _scope_property.get_val()
|
||||
|
||||
|
||||
static func set_scope_path(path: String) -> void:
|
||||
_scope_property.set_val(path)
|
||||
|
||||
|
||||
static func is_valid() -> bool:
|
||||
var token_valid = is_instance_valid(editor_oauth_token) && editor_oauth_token.is_token_valid()
|
||||
var setting_valid = is_instance_valid(editor_oauth_setting) && editor_oauth_setting.is_valid()
|
||||
return token_valid && setting_valid
|
||||
|
||||
|
||||
static func _create_editor_oauth_token() -> void:
|
||||
var path: String = _editor_oauth_token_property.get_val()
|
||||
var token = OAuthToken.new()
|
||||
token._identifier = "EditorToken"
|
||||
token.take_over_path(path)
|
||||
editor_oauth_token = token
|
||||
save_editor_oauth_token()
|
||||
|
||||
|
||||
static func _create_editor_oauth_setting() -> void:
|
||||
var path: String = _editor_oauth_setting_property.get_val()
|
||||
var setting = TwitchAuth.create_default_oauth_setting()
|
||||
setting.take_over_path(path)
|
||||
editor_oauth_setting = setting
|
||||
save_editor_oauth_setting()
|
||||
1
addons/twitcher/editor/twitch_editor_settings.gd.uid
Normal file
1
addons/twitcher/editor/twitch_editor_settings.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://kqcukq2xqnuf
|
||||
17
addons/twitcher/editor/twitch_tweens.gd
Normal file
17
addons/twitcher/editor/twitch_tweens.gd
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
static func flash(object: Control, color: Color, duration: float = .25) -> void:
|
||||
var tween = object.create_tween()
|
||||
tween.tween_property(object, ^"modulate", color, duration)\
|
||||
.set_ease(Tween.EASE_OUT)\
|
||||
.set_trans(Tween.TRANS_CIRC)
|
||||
tween.tween_property(object, ^"modulate", Color.WHITE, duration)\
|
||||
.set_ease(Tween.EASE_OUT)\
|
||||
.set_trans(Tween.TRANS_CIRC)
|
||||
await tween.finished
|
||||
|
||||
|
||||
static func loading(object: Control, color: Color = Color.YELLOW) -> void:
|
||||
var tween: Tween = object.create_tween()
|
||||
tween.tween_property(object, ^"modulate", color, 0.2) \
|
||||
.set_trans(Tween.TRANS_LINEAR) \
|
||||
.set_ease(Tween.EASE_IN_OUT)
|
||||
await tween.finished
|
||||
1
addons/twitcher/editor/twitch_tweens.gd.uid
Normal file
1
addons/twitcher/editor/twitch_tweens.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://duhsr84u352ef
|
||||
Loading…
Add table
Add a link
Reference in a new issue