mirror of
https://github.com/zen-browser/theme-store.git
synced 2025-07-07 17:05:31 +02:00
refactor(submit-theme): refactored to use the new preferences syntax (#423)
This commit is contained in:
parent
5ed468f156
commit
640f563c73
1 changed files with 231 additions and 86 deletions
|
@ -1,11 +1,40 @@
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
import argparse
|
import argparse
|
||||||
import json
|
import json
|
||||||
import uuid
|
import uuid
|
||||||
import sys
|
import sys
|
||||||
import requests
|
import requests
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
|
from enum import StrEnum
|
||||||
|
|
||||||
|
|
||||||
|
class PreferenceFields(StrEnum):
|
||||||
|
PROPERTY = "property"
|
||||||
|
LABEL = "label"
|
||||||
|
TYPE = "type"
|
||||||
|
OPTIONS = "options"
|
||||||
|
DEFAULT_VALUE = "defaultValue"
|
||||||
|
DISABLED_ON = "disabledOn"
|
||||||
|
PLACEHOLDER = "placeholder"
|
||||||
|
|
||||||
|
|
||||||
|
class PreferenceTypes(StrEnum):
|
||||||
|
CHECKBOX = "checkbox"
|
||||||
|
DROPDOWN = "dropdown"
|
||||||
|
STRING = "string"
|
||||||
|
|
||||||
|
def valid_types(self) -> list[type]:
|
||||||
|
match self:
|
||||||
|
case self.CHECKBOX:
|
||||||
|
return [bool]
|
||||||
|
|
||||||
|
case self.DROPDOWN:
|
||||||
|
return [str, int, float]
|
||||||
|
|
||||||
|
case self.STRING:
|
||||||
|
return [str]
|
||||||
|
|
||||||
|
|
||||||
STYLES_FILE = "chrome.css"
|
STYLES_FILE = "chrome.css"
|
||||||
COLORS_FILE = "colors.json"
|
COLORS_FILE = "colors.json"
|
||||||
|
@ -17,120 +46,234 @@ TEMPLATE_STYLES_FILE = "./theme-styles.css"
|
||||||
TEMPLATE_README_FILE = "./theme-readme.md"
|
TEMPLATE_README_FILE = "./theme-readme.md"
|
||||||
TEMPLATE_PREFERENCES_FILE = "./theme-preferences.json"
|
TEMPLATE_PREFERENCES_FILE = "./theme-preferences.json"
|
||||||
|
|
||||||
|
VALID_OS = set(["linux", "macos", "windows"])
|
||||||
|
PLACEHOLDER_TYPES = [PreferenceTypes.DROPDOWN, PreferenceTypes.STRING]
|
||||||
|
REQUIRED_FIELDS = set(
|
||||||
|
[PreferenceFields.PROPERTY, PreferenceFields.LABEL, PreferenceFields.TYPE]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def panic(string: str, error=None):
|
||||||
|
print(string, file=sys.stderr)
|
||||||
|
|
||||||
|
if error:
|
||||||
|
print(error, file=sys.stderr)
|
||||||
|
|
||||||
|
exit(1)
|
||||||
|
|
||||||
|
|
||||||
def create_theme_id():
|
def create_theme_id():
|
||||||
return str(uuid.uuid4())
|
return str(uuid.uuid4())
|
||||||
|
|
||||||
|
|
||||||
def get_static_asset(theme_id, asset):
|
def get_static_asset(theme_id, asset):
|
||||||
return f"https://raw.githubusercontent.com/zen-browser/theme-store/main/themes/{theme_id}/{asset}"
|
return f"https://raw.githubusercontent.com/zen-browser/theme-store/main/themes/{theme_id}/{asset}"
|
||||||
|
|
||||||
|
|
||||||
def get_styles(is_color_theme, theme_id):
|
def get_styles(is_color_theme, theme_id):
|
||||||
with open(TEMPLATE_STYLES_FILE, 'r') as f:
|
with open(TEMPLATE_STYLES_FILE, "r") as f:
|
||||||
content = f.read()
|
content = f.read()
|
||||||
content = content[len("```css"):]
|
content = content[len("```css") :]
|
||||||
content = content[:-len("```")]
|
content = content[: -len("```")]
|
||||||
|
|
||||||
# we actually have a JSON file here that needs to be generated
|
# we actually have a JSON file here that needs to be generated
|
||||||
if is_color_theme:
|
if is_color_theme:
|
||||||
with open(f"themes/{theme_id}/{COLORS_FILE}", 'w') as f:
|
with open(f"themes/{theme_id}/{COLORS_FILE}", "w") as f:
|
||||||
json.dump(json.loads(content), f, indent=4)
|
json.dump(json.loads(content), f, indent=4)
|
||||||
return "/* This is a color theme. */"
|
return "/* This is a color theme. */"
|
||||||
return content
|
return content
|
||||||
|
|
||||||
|
|
||||||
def get_readme():
|
def get_readme():
|
||||||
with open(TEMPLATE_README_FILE, 'r') as f:
|
with open(TEMPLATE_README_FILE, "r") as f:
|
||||||
content = f.read()
|
content = f.read()
|
||||||
content = content[len("```markdown"):]
|
content = content[len("```markdown") :]
|
||||||
content = content[:-len("```")]
|
content = content[: -len("```")]
|
||||||
return content
|
return content
|
||||||
|
|
||||||
|
|
||||||
def validate_url(url, allow_empty=False):
|
def validate_url(url, allow_empty=False):
|
||||||
if allow_empty and len(url) == 0:
|
if allow_empty and len(url) == 0:
|
||||||
return
|
return
|
||||||
try:
|
try:
|
||||||
result = urllib.parse.urlparse(url)
|
result = urllib.parse.urlparse(url)
|
||||||
if result.scheme != 'https':
|
if result.scheme != "https":
|
||||||
print("URL must be HTTPS.", file=sys.stderr)
|
panic("URL must be HTTPS.")
|
||||||
exit(1)
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print("URL is invalid.", file=sys.stderr)
|
panic("URL is invalid.", e)
|
||||||
print(e, file=sys.stderr)
|
|
||||||
|
|
||||||
|
def get_enum_error(value, Enum: StrEnum):
|
||||||
|
return f"Field must be one of {', '.join(Enum._value2member_map_)} but received \"{value}\""
|
||||||
|
|
||||||
|
|
||||||
|
def parse_field_to_enum(key: tuple[str, any]) -> tuple[PreferenceFields, any]:
|
||||||
|
try:
|
||||||
|
converted_key = re.sub(r"(?<!^)(?=[A-Z])", "_", key[0]).upper()
|
||||||
|
return (PreferenceFields[converted_key], key[1])
|
||||||
|
except:
|
||||||
|
panic(key[0], PreferenceFields)
|
||||||
exit(1)
|
exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_type(value: str) -> PreferenceTypes:
|
||||||
|
try:
|
||||||
|
converted_value = re.sub(r"(?<!^)(?=[A-Z])", "_", value).upper()
|
||||||
|
return PreferenceTypes[converted_value]
|
||||||
|
except:
|
||||||
|
panic(get_enum_error(value, PreferenceTypes))
|
||||||
|
|
||||||
|
|
||||||
|
def check_value_type(value, arr_types: list[type]):
|
||||||
|
return type(value) in arr_types
|
||||||
|
|
||||||
|
|
||||||
|
def is_value_in_enum(value, Enum: StrEnum) -> bool:
|
||||||
|
try:
|
||||||
|
Enum[value.upper()]
|
||||||
|
|
||||||
|
return True
|
||||||
|
except:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def is_empty_str(value: str) -> bool:
|
||||||
|
return not isinstance(value, str) or len(value) == 0
|
||||||
|
|
||||||
|
|
||||||
def validate_preferences(preferences):
|
def validate_preferences(preferences):
|
||||||
for key, value in preferences.items():
|
for entry in preferences:
|
||||||
if not isinstance(key, str):
|
properties = dict(
|
||||||
print("Preference key must be a string.", file=sys.stderr)
|
map(
|
||||||
exit(1)
|
lambda key: parse_field_to_enum(key),
|
||||||
if not isinstance(value, str):
|
entry.items(),
|
||||||
print("Preference description must be a string.", file=sys.stderr)
|
)
|
||||||
exit(1)
|
)
|
||||||
if len(key) == 0:
|
|
||||||
print("Preference key is required.", file=sys.stderr)
|
if not set(properties.keys()).issuperset(REQUIRED_FIELDS):
|
||||||
exit(1)
|
panic(f"Required fields ({", ".join(REQUIRED_FIELDS)}) are not in {entry}.")
|
||||||
for char in key:
|
|
||||||
if not char.isalnum() and char != '.' and char != '-' and char != '_':
|
current_type = parse_type(properties[PreferenceFields.TYPE])
|
||||||
print("Preference key must only contain letters, numbers, periods, dashes, and underscores.", file=sys.stderr)
|
valid_type_list = current_type.valid_types()
|
||||||
exit(1)
|
current_property = properties[PreferenceFields.PROPERTY]
|
||||||
if len(value) == 0:
|
|
||||||
print("Preference description is required.", file=sys.stderr)
|
for key, value in properties.items():
|
||||||
exit(1)
|
match key:
|
||||||
|
case PreferenceFields.PROPERTY:
|
||||||
|
if is_empty_str(value) or re.search(r"[^A-z0-9\-_.]", value):
|
||||||
|
panic(
|
||||||
|
f"Property must only contain letters, numbers, periods, dashes, and underscores. Received {current_property}"
|
||||||
|
)
|
||||||
|
|
||||||
|
case PreferenceFields.LABEL:
|
||||||
|
if not isinstance(value, str) or len(value) == 0:
|
||||||
|
panic(f"Label for {current_property} is required.")
|
||||||
|
|
||||||
|
case PreferenceFields.TYPE:
|
||||||
|
if not isinstance(value, str) or len(value) == 0:
|
||||||
|
panic(f"Type in {current_property} is required.")
|
||||||
|
elif not is_value_in_enum(value, PreferenceTypes):
|
||||||
|
panic(get_enum_error(value, PreferenceTypes))
|
||||||
|
|
||||||
|
case PreferenceFields.OPTIONS:
|
||||||
|
if current_type != PreferenceTypes.DROPDOWN:
|
||||||
|
panic("Dropdown type is required for property options")
|
||||||
|
elif not isinstance(value, list) or len(value) == 0:
|
||||||
|
panic(f"Options in {current_property} cannot be empty")
|
||||||
|
else:
|
||||||
|
for option in value:
|
||||||
|
option_label = option.get("label")
|
||||||
|
option_value = option.get("value")
|
||||||
|
|
||||||
|
if is_empty_str(option_label):
|
||||||
|
panic(
|
||||||
|
f"Label for option in {current_property} is required."
|
||||||
|
)
|
||||||
|
|
||||||
|
if not check_value_type(option_value, valid_type_list):
|
||||||
|
panic(
|
||||||
|
f"Option {option_label} in {current_property} was expecting value of any type in {", ".join(map(lambda type: type.__name__, valid_type_list))}, but received {type(option_value).__name__}"
|
||||||
|
)
|
||||||
|
|
||||||
|
case PreferenceFields.DEFAULT_VALUE:
|
||||||
|
if not check_value_type(value, valid_type_list):
|
||||||
|
panic(
|
||||||
|
f"Field defaultValue in {current_property} was expecting value with any type in {", ".join(map(lambda type: type.__name__, valid_type_list))} but received {value}"
|
||||||
|
)
|
||||||
|
|
||||||
|
case PreferenceFields.DISABLED_ON:
|
||||||
|
if not isinstance(value, list):
|
||||||
|
panic(
|
||||||
|
f"Field disabledOn in {current_property} is expecting an array"
|
||||||
|
)
|
||||||
|
elif len(value) != 0 and not set(value).issuperset(VALID_OS):
|
||||||
|
panic(
|
||||||
|
f"Field disabledOn in {current_property} is expecting one or more of {", ".join(VALID_OS)} but received {", ".join(value)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
case PreferenceFields.PLACEHOLDER:
|
||||||
|
if not current_type in PLACEHOLDER_TYPES:
|
||||||
|
panic(
|
||||||
|
f"Placeholder in {current_property} can only be used for types {", ".join(PLACEHOLDER_TYPES)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
case _:
|
||||||
|
panic("This should be unreachable.")
|
||||||
|
|
||||||
return preferences
|
return preferences
|
||||||
|
|
||||||
|
|
||||||
def get_preferences():
|
def get_preferences():
|
||||||
with open(TEMPLATE_PREFERENCES_FILE, 'r') as f:
|
with open(TEMPLATE_PREFERENCES_FILE, "r") as f:
|
||||||
try:
|
try:
|
||||||
content = f.read()
|
content = f.read()
|
||||||
if content.strip() == "":
|
if content.strip() == "":
|
||||||
return {}
|
return {}
|
||||||
content = content[len("```json"):]
|
content = re.sub(r"```json\n*", "", content)
|
||||||
content = content[:-len("```")]
|
content = re.sub(r"\n*```\n*", "", content)
|
||||||
return validate_preferences(json.loads(content))
|
return validate_preferences(json.loads(content))
|
||||||
except json.JSONDecodeError as e:
|
except json.JSONDecodeError as e:
|
||||||
print("Preferences file is invalid.", file=sys.stderr)
|
panic("Preferences file is invalid.", e)
|
||||||
print(e, file=sys.stderr)
|
|
||||||
exit(1)
|
|
||||||
|
|
||||||
def validate_name(name):
|
def validate_name(name):
|
||||||
if len(name) == 0:
|
if len(name) == 0:
|
||||||
print("Name is required.", file=sys.stderr)
|
panic("Name is required.")
|
||||||
exit(1)
|
|
||||||
if len(name) > 25:
|
if len(name) > 25:
|
||||||
print("Name must be less than 25 characters.", file=sys.stderr)
|
panic("Name must be less than 25 characters.")
|
||||||
exit(1)
|
|
||||||
for char in name:
|
for char in name:
|
||||||
if not char.isalnum() and char != ' ':
|
if not char.isalnum() and char != " ":
|
||||||
print("Name must only contain letters, numbers, and spaces.", file=sys.stderr)
|
panic("Name must only contain letters, numbers, and spaces.")
|
||||||
exit(1)
|
|
||||||
|
|
||||||
def validate_description(description):
|
def validate_description(description):
|
||||||
if len(description) == 0:
|
if len(description) == 0:
|
||||||
print("Description is required.", file=sys.stderr)
|
panic("Description is required.")
|
||||||
exit(1)
|
|
||||||
if len(description) > 120:
|
if len(description) > 120:
|
||||||
print("Description must be less than 100 characters.", file=sys.stderr)
|
panic("Description must be less than 100 characters.")
|
||||||
exit(1)
|
|
||||||
|
|
||||||
def download_image(image_url, image_path):
|
def download_image(image_url, image_path):
|
||||||
response = requests.get(image_url, headers={'User-Agent': 'Epicture'})
|
response = requests.get(image_url, headers={"User-Agent": "Epicture"})
|
||||||
if response.status_code != 200:
|
if response.status_code != 200:
|
||||||
print("Image URL is invalid.", file=sys.stderr)
|
panic("Image URL is invalid.")
|
||||||
exit(1)
|
|
||||||
# Check if the image is valid and a PNG
|
# Check if the image is valid and a PNG
|
||||||
if response.headers['Content-Type'] != 'image/png':
|
if response.headers["Content-Type"] != "image/png":
|
||||||
print("Image must be a PNG.", file=sys.stderr)
|
panic("Image must be a PNG.")
|
||||||
exit(1)
|
with open(image_path, "wb") as f:
|
||||||
with open(image_path, 'wb') as f:
|
|
||||||
f.write(response.content)
|
f.write(response.content)
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
parser = argparse.ArgumentParser(description='Submit a theme to the theme repo.')
|
parser = argparse.ArgumentParser(description="Submit a theme to the theme repo.")
|
||||||
parser.add_argument('--name', type=str, help='The theme to submit.')
|
parser.add_argument("--name", type=str, help="The theme to submit.")
|
||||||
parser.add_argument('--description', type=str, help='The description of the theme.')
|
parser.add_argument("--description", type=str, help="The description of the theme.")
|
||||||
parser.add_argument('--homepage', type=str, help='The homepage of the theme.')
|
parser.add_argument("--homepage", type=str, help="The homepage of the theme.")
|
||||||
parser.add_argument('--author', type=str, help='The author of the theme.')
|
parser.add_argument("--author", type=str, help="The author of the theme.")
|
||||||
parser.add_argument('--image', type=str, help='The image of the theme.')
|
parser.add_argument("--image", type=str, help="The image of the theme.")
|
||||||
parser.add_argument('--is-color-theme', type=str, help='Whether the theme is a color theme.')
|
parser.add_argument(
|
||||||
|
"--is-color-theme", type=str, help="Whether the theme is a color theme."
|
||||||
|
)
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
name = args.name
|
name = args.name
|
||||||
|
@ -148,46 +291,47 @@ def main():
|
||||||
|
|
||||||
theme_id = create_theme_id()
|
theme_id = create_theme_id()
|
||||||
|
|
||||||
print("""
|
print(
|
||||||
|
"""
|
||||||
Welcome to the Zen Browser Theme Store!
|
Welcome to the Zen Browser Theme Store!
|
||||||
|
|
||||||
Please review the information below before submitting your theme. Also... Why are you here?
|
Please review the information below before submitting your theme. Also... Why are you here?
|
||||||
|
|
||||||
This action is only for theme reviewers. If you are a theme developer, please use the theme store.
|
This action is only for theme reviewers. If you are a theme developer, please use the theme store.
|
||||||
Just joking, you can do whatever you want. You're the boss.
|
Just joking, you can do whatever you want. You're the boss.
|
||||||
""")
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
theme = {
|
theme = {
|
||||||
'id': theme_id,
|
"id": theme_id,
|
||||||
'name': name,
|
"name": name,
|
||||||
'description': description,
|
"description": description,
|
||||||
'homepage': homepage,
|
"homepage": homepage,
|
||||||
'style': get_static_asset(theme_id, STYLES_FILE),
|
"style": get_static_asset(theme_id, STYLES_FILE),
|
||||||
'readme': get_static_asset(theme_id, README_FILE),
|
"readme": get_static_asset(theme_id, README_FILE),
|
||||||
'image': get_static_asset(theme_id, IMAGE_FILE),
|
"image": get_static_asset(theme_id, IMAGE_FILE),
|
||||||
'author': author,
|
"author": author,
|
||||||
'version': '1.0.0',
|
"version": "1.0.0",
|
||||||
}
|
}
|
||||||
|
|
||||||
os.makedirs(f"themes/{theme_id}")
|
os.makedirs(f"themes/{theme_id}")
|
||||||
|
|
||||||
with open(f"themes/{theme_id}/{STYLES_FILE}", 'w') as f:
|
with open(f"themes/{theme_id}/{STYLES_FILE}", "w") as f:
|
||||||
f.write(get_styles(is_color_theme, theme_id))
|
f.write(get_styles(is_color_theme, theme_id))
|
||||||
|
|
||||||
with open(f"themes/{theme_id}/{README_FILE}", 'w') as f:
|
with open(f"themes/{theme_id}/{README_FILE}", "w") as f:
|
||||||
f.write(get_readme())
|
f.write(get_readme())
|
||||||
|
|
||||||
if os.path.exists(TEMPLATE_PREFERENCES_FILE):
|
if os.path.exists(TEMPLATE_PREFERENCES_FILE):
|
||||||
if is_color_theme:
|
if is_color_theme:
|
||||||
print("Color themes do not support preferences.", file=sys.stderr)
|
panic("Color themes do not support preferences.")
|
||||||
exit(1)
|
|
||||||
prefs_file = f"themes/{theme_id}/{PREFERENCES_FILE}"
|
prefs_file = f"themes/{theme_id}/{PREFERENCES_FILE}"
|
||||||
with open(prefs_file, 'w') as f:
|
with open(prefs_file, "w") as f:
|
||||||
prefs = get_preferences()
|
prefs = get_preferences()
|
||||||
if len(prefs) > 0:
|
if len(prefs) > 0:
|
||||||
print("Detected preferences file. Please review the preferences below.")
|
print("Detected preferences file. Please review the preferences below.")
|
||||||
print(prefs)
|
print(prefs)
|
||||||
theme['preferences'] = get_static_asset(theme_id, PREFERENCES_FILE)
|
theme["preferences"] = get_static_asset(theme_id, PREFERENCES_FILE)
|
||||||
json.dump(prefs, f, indent=4)
|
json.dump(prefs, f, indent=4)
|
||||||
else:
|
else:
|
||||||
print("No preferences detected.")
|
print("No preferences detected.")
|
||||||
|
@ -195,12 +339,13 @@ Just joking, you can do whatever you want. You're the boss.
|
||||||
|
|
||||||
download_image(image, f"themes/{theme_id}/{IMAGE_FILE}")
|
download_image(image, f"themes/{theme_id}/{IMAGE_FILE}")
|
||||||
|
|
||||||
with open(f"themes/{theme_id}/theme.json", 'w') as f:
|
with open(f"themes/{theme_id}/theme.json", "w") as f:
|
||||||
json.dump(theme, f)
|
json.dump(theme, f)
|
||||||
|
|
||||||
print(f"Theme submitted with ID: {theme_id}")
|
print(f"Theme submitted with ID: {theme_id}")
|
||||||
for key, value in theme.items():
|
for key, value in theme.items():
|
||||||
print(f"\t{key}: {value}")
|
print(f"\t{key}: {value}")
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue