mirror of
https://github.com/Faraphel/Atlas-Install.git
synced 2025-07-03 03:08:29 +02:00
MKWColor is now a dataclass to improve readability
This commit is contained in:
parent
b035dcb6b7
commit
443d16b28c
8 changed files with 129 additions and 133 deletions
|
@ -1,14 +1,14 @@
|
|||
{
|
||||
"GAME_REGION": "str(extracted_game.original_game.wit_path.region)",
|
||||
|
||||
"TRACK_TEXT_SCORE": "bmg_color_text(['yellow', 'orange', 'dark red', 'azure'][getattr(track, 'warning', 0)], f'\\\\x{65296+track.score:x} ') if hasattr(track, 'score') else ''",
|
||||
"TRACK_TEXT_SCORE": "get_color(name=['yellow', 'orange', 'dark red', 'azure'][getattr(track, 'warning', 0)]).color_text(f'\\\\x{65296+track.score:x} ') if hasattr(track, 'score') else ''",
|
||||
"TRACK_TEXT_PREFIX": "f'{prefix} ' if (prefix := get_tag_template('prefix', '')) else ''",
|
||||
"TRACK_TEXT_SUFFIX": "f' ({suffix})' if (suffix := get_tag_template('suffix', '')) else ''",
|
||||
"TRACK_TEXT_NAME": "getattr(track, 'name', '')",
|
||||
"TRACK_TEXT_AUTHORS": "'\\\\n'.join(author) if isinstance(author := getattr(track, 'author', '/'), list) else author",
|
||||
"TRACK_TEXT_WARNING_IF_DISABLED": "bmg_color_text('red', '/') if getattr(track, 'warning', 0) != 0 else ''",
|
||||
"TRACK_TEXT_HIGHLIGHT_START": "bmg_color_raw('azure') if eval(highlight_if if (highlight_if := mod_config.specific_settings['highlight_if'].value) is not None else 'False', env={'track': track, 'mod_config': mod_config}) is True else ''",
|
||||
"TRACK_TEXT_HIGHLIGHT_END": "bmg_color_raw('off')",
|
||||
"TRACK_TEXT_WARNING_IF_DISABLED": "get_color(name='red').color_text('/') if getattr(track, 'warning', 0) != 0 else ''",
|
||||
"TRACK_TEXT_HIGHLIGHT_START": "get_color(name='azure').raw if eval(highlight_if if (highlight_if := mod_config.specific_settings['highlight_if'].value) is not None else 'False', env={'track': track, 'mod_config': mod_config}) is True else ''",
|
||||
"TRACK_TEXT_HIGHLIGHT_END": "get_color(name='off').raw",
|
||||
|
||||
"SETTINGS_MODE": "mod_config.specific_settings['mode'].value",
|
||||
"SETTINGS_BALANCING": "mod_config.specific_settings['balancing'].value",
|
||||
|
|
|
@ -55,59 +55,59 @@
|
|||
|
||||
"tags_templates": {
|
||||
"prefix": {
|
||||
"MSRDS": "{{ bmg_color_text('green', tag) }}",
|
||||
"CTR": "{{ bmg_color_text('orange', tag) }}",
|
||||
"CTTR": "{{ bmg_color_text('dark orange', tag) }}",
|
||||
"CNR": "{{ bmg_color_text('orange', tag) }}",
|
||||
"DKR": "{{ bmg_color_text('dark red', tag) }}",
|
||||
"LCP": "{{ bmg_color_text('green', tag) }}",
|
||||
"LEGO-R": "{{ bmg_color_text('light red', tag) }}",
|
||||
"MP9": "{{ bmg_color_text('neon yellow', tag) }}",
|
||||
"MSUSA": "{{ bmg_color_text('green', tag) }}",
|
||||
"FZMV": "{{ bmg_color_text('yellow', tag) }}",
|
||||
"KAR": "{{ bmg_color_text('green', tag) }}",
|
||||
"KO": "{{ bmg_color_text('dark orange', tag) }}",
|
||||
"FZ": "{{ bmg_color_text('yellow', tag) }}",
|
||||
"RV": "{{ bmg_color_text('white', tag) }}",
|
||||
"SADX": "{{ bmg_color_text('dark blue', tag) }}",
|
||||
"SCR": "{{ bmg_color_text('yellow', tag) }}",
|
||||
"SH": "{{ bmg_color_text('pink', tag) }}",
|
||||
"SM64": "{{ bmg_color_text('pink', tag) }}",
|
||||
"SMB1": "{{ bmg_color_text('light red', tag) }}",
|
||||
"SMB2": "{{ bmg_color_text('red', tag) }}",
|
||||
"SSBB": "{{ bmg_color_text('vivid red', tag) }}",
|
||||
"SMS": "{{ bmg_color_text('dark red', tag) }}",
|
||||
"SMO": "{{ bmg_color_text('apple red', tag) }}",
|
||||
"VVVVVV": "{{ bmg_color_text('azure', tag) }}",
|
||||
"WF": "{{ bmg_color_text('green', tag) }}",
|
||||
"WP": "{{ bmg_color_text('neon yellow 2', tag) }}",
|
||||
"Zelda OoT": "{{ bmg_color_text('green', tag) }}",
|
||||
"Zelda TP": "{{ bmg_color_text('green', tag) }}",
|
||||
"Zelda WW": "{{ bmg_color_text('green', tag) }}",
|
||||
"PMWR": "{{ bmg_color_text('neon yellow 2', tag) }}",
|
||||
"SHR": "{{ bmg_color_text('green', tag) }}",
|
||||
"SK64": "{{ bmg_color_text('green', tag) }}",
|
||||
"SMG": "{{ bmg_color_text('light red', tag) }}",
|
||||
"Spyro 1": "{{ bmg_color_text('azure', tag) }}",
|
||||
"Switch": "{{ bmg_color_text('vivid red', tag) }}",
|
||||
"Wii": "{{ bmg_color_text('azure', tag) }}",
|
||||
"3DS": "{{ bmg_color_text('light orange', tag) }}",
|
||||
"DS": "{{ bmg_color_text('white', tag) }}",
|
||||
"GCN": "{{ bmg_color_text('azure', tag) }}",
|
||||
"GBA": "{{ bmg_color_text('dark blue', tag) }}",
|
||||
"N64": "{{ bmg_color_text('pink', tag) }}",
|
||||
"SNES": "{{ bmg_color_text('green', tag) }}",
|
||||
"RMX": "{{ bmg_color_text('orange', tag) }}",
|
||||
"MKT": "{{ bmg_color_text('dark orange', tag) }}",
|
||||
"GP": "{{ bmg_color_text('dark red', tag) }}",
|
||||
"UT": "{{ bmg_color_text('red', tag) }}",
|
||||
"GK2": "{{ bmg_color_text('green', tag) }}",
|
||||
"GK3": "{{ bmg_color_text('green', tag) }}",
|
||||
"GK7": "{{ bmg_color_text('green', tag) }}",
|
||||
"FGKR": "{{ bmg_color_text('dark orange', tag) }}"
|
||||
"MSRDS": "{{ get_color(name='green').color_text(tag) }}",
|
||||
"CTR": "{{ get_color(name='orange').color_text(tag) }}",
|
||||
"CTTR": "{{ get_color(name='dark orange').color_text(tag) }}",
|
||||
"CNR": "{{ get_color(name='orange').color_text(tag) }}",
|
||||
"DKR": "{{ get_color(name='dark red').color_text(tag) }}",
|
||||
"LCP": "{{ get_color(name='green').color_text(tag) }}",
|
||||
"LEGO-R": "{{ get_color(name='light red').color_text(tag) }}",
|
||||
"MP9": "{{ get_color(name='neon yellow').color_text(tag) }}",
|
||||
"MSUSA": "{{ get_color(name='green').color_text(tag) }}",
|
||||
"FZMV": "{{ get_color(name='yellow').color_text(tag) }}",
|
||||
"KAR": "{{ get_color(name='green').color_text(tag) }}",
|
||||
"KO": "{{ get_color(name='dark orange').color_text(tag) }}",
|
||||
"FZ": "{{ get_color(name='yellow').color_text(tag) }}",
|
||||
"RV": "{{ get_color(name='white').color_text(tag) }}",
|
||||
"SADX": "{{ get_color(name='dark blue').color_text(tag) }}",
|
||||
"SCR": "{{ get_color(name='yellow').color_text(tag) }}",
|
||||
"SH": "{{ get_color(name='pink').color_text(tag) }}",
|
||||
"SM64": "{{ get_color(name='pink').color_text(tag) }}",
|
||||
"SMB1": "{{ get_color(name='light red').color_text(tag) }}",
|
||||
"SMB2": "{{ get_color(name='red').color_text(tag) }}",
|
||||
"SSBB": "{{ get_color(name='vivid red').color_text(tag) }}",
|
||||
"SMS": "{{ get_color(name='dark red').color_text(tag) }}",
|
||||
"SMO": "{{ get_color(name='apple red').color_text(tag) }}",
|
||||
"VVVVVV": "{{ get_color(name='azure').color_text(tag) }}",
|
||||
"WF": "{{ get_color(name='green').color_text(tag) }}",
|
||||
"WP": "{{ get_color(name='neon yellow 2').color_text(tag) }}",
|
||||
"Zelda OoT": "{{ get_color(name='green').color_text(tag) }}",
|
||||
"Zelda TP": "{{ get_color(name='green').color_text(tag) }}",
|
||||
"Zelda WW": "{{ get_color(name='green').color_text(tag) }}",
|
||||
"PMWR": "{{ get_color(name='neon yellow 2').color_text(tag) }}",
|
||||
"SHR": "{{ get_color(name='green').color_text(tag) }}",
|
||||
"SK64": "{{ get_color(name='green').color_text(tag) }}",
|
||||
"SMG": "{{ get_color(name='light red').color_text(tag) }}",
|
||||
"Spyro 1": "{{ get_color(name='azure').color_text(tag) }}",
|
||||
"Switch": "{{ get_color(name='vivid red').color_text(tag) }}",
|
||||
"Wii": "{{ get_color(name='azure').color_text(tag) }}",
|
||||
"3DS": "{{ get_color(name='light orange').color_text(tag) }}",
|
||||
"DS": "{{ get_color(name='white').color_text(tag) }}",
|
||||
"GCN": "{{ get_color(name='azure').color_text(tag) }}",
|
||||
"GBA": "{{ get_color(name='dark blue').color_text(tag) }}",
|
||||
"N64": "{{ get_color(name='pink').color_text(tag) }}",
|
||||
"SNES": "{{ get_color(name='green').color_text(tag) }}",
|
||||
"RMX": "{{ get_color(name='orange').color_text(tag) }}",
|
||||
"MKT": "{{ get_color(name='dark orange').color_text(tag) }}",
|
||||
"GP": "{{ get_color(name='dark red').color_text(tag) }}",
|
||||
"UT": "{{ get_color(name='red').color_text(tag) }}",
|
||||
"GK2": "{{ get_color(name='green').color_text(tag) }}",
|
||||
"GK3": "{{ get_color(name='green').color_text(tag) }}",
|
||||
"GK7": "{{ get_color(name='green').color_text(tag) }}",
|
||||
"FGKR": "{{ get_color(name='dark orange').color_text(tag) }}"
|
||||
},
|
||||
"suffix": {
|
||||
"Boost": "{{ bmg_color_text('orange', tag) }}"
|
||||
"Boost": "{{ get_color(name='orange').color_text(tag) }}"
|
||||
}
|
||||
},
|
||||
"tags_cups": [
|
||||
|
|
|
@ -3,7 +3,7 @@ from tkinter import ttk
|
|||
from typing import TYPE_CHECKING
|
||||
import re
|
||||
|
||||
from source.mkw.MKWColor import MKWColor
|
||||
from source.mkw import MKWColor
|
||||
from source.gui.preview import AbstractPreviewWindow
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
@ -28,7 +28,7 @@ class Window(AbstractPreviewWindow):
|
|||
self.entry_template_input.bind("<Return>", self.preview)
|
||||
|
||||
self.text_track_format = tkinter.Text(
|
||||
self, background="black", foreground=MKWColor("off").color_code, state=tkinter.DISABLED
|
||||
self, background="black", foreground=MKWColor.get(bmg="off").color_code, state=tkinter.DISABLED
|
||||
)
|
||||
self.text_track_format.grid(row=2, column=1, sticky="NEWS")
|
||||
|
||||
|
@ -37,7 +37,7 @@ class Window(AbstractPreviewWindow):
|
|||
|
||||
self.text_track_format.configure(yscrollcommand=self.scrollbar_track_preview.set)
|
||||
|
||||
for color in MKWColor.get_all_colors():
|
||||
for color in MKWColor.all_colors:
|
||||
self.text_track_format.tag_configure(color.bmg, foreground=color.color_code)
|
||||
self.text_track_format.tag_configure("error", background="red", foreground="white")
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@ import tkinter
|
|||
from tkinter import ttk
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from source.mkw.MKWColor import MKWColor
|
||||
from source.mkw import MKWColor
|
||||
from source.gui.preview import AbstractPreviewWindow
|
||||
from source.gui import better_gui_error
|
||||
|
||||
|
@ -27,7 +27,9 @@ class Window(AbstractPreviewWindow):
|
|||
self.entry_template_input.grid(row=1, column=1, columnspan=2, sticky="NEWS")
|
||||
self.entry_template_input.bind("<Return>", self.preview)
|
||||
|
||||
self.text_track_format = tkinter.Text(self, width=40, bg="black", fg=MKWColor("off").color_code, state=tkinter.DISABLED)
|
||||
self.text_track_format = tkinter.Text(
|
||||
self, width=40, bg="black", fg=MKWColor.get(bmg="off").color_code, state=tkinter.DISABLED
|
||||
)
|
||||
self.text_track_format.grid(row=2, column=1, sticky="NEWS")
|
||||
|
||||
self.text_track_select = tkinter.Text(self, width=20, bg="black", state=tkinter.DISABLED)
|
||||
|
|
|
@ -2,7 +2,7 @@ import tkinter
|
|||
from tkinter import ttk
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from source.mkw.MKWColor import MKWColor
|
||||
from source.mkw import MKWColor
|
||||
from source.gui.preview import AbstractPreviewWindow
|
||||
from source.gui import better_gui_error
|
||||
|
||||
|
@ -27,7 +27,7 @@ class Window(AbstractPreviewWindow):
|
|||
self.entry_template_input.bind("<Return>", self.preview)
|
||||
|
||||
self.text_track_format = tkinter.Text(
|
||||
self, background="black", foreground=MKWColor("off").color_code, state=tkinter.DISABLED
|
||||
self, background="black", foreground=MKWColor.get(bmg="off").color_code, state=tkinter.DISABLED
|
||||
)
|
||||
self.text_track_format.grid(row=2, column=1, sticky="NEWS")
|
||||
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
from dataclasses import dataclass
|
||||
|
||||
from source.translation import translate as _
|
||||
|
||||
|
||||
|
@ -6,71 +8,74 @@ class ColorNotFound(Exception):
|
|||
super().__init__(_("CANNOT_FIND_COLOR", ' "', color_data, '"'))
|
||||
|
||||
|
||||
@dataclass(init=True, slots=True)
|
||||
class MKWColor:
|
||||
"""
|
||||
Represent a color that can be used inside MKW files
|
||||
"""
|
||||
|
||||
_all_colors: list[dict] = [
|
||||
{"bmg": "yor7", "hex": 0xF5090B, "name": "apple red"},
|
||||
{"bmg": "yor6", "hex": 0xE82C09, "name": "dark red"},
|
||||
{"bmg": "yor5", "hex": 0xE65118, "name": "dark orange"}, # flame
|
||||
{"bmg": "yor4", "hex": 0xFF760E, "name": "orange"}, # pumpkin
|
||||
{"bmg": "yor3", "hex": 0xFFA61F, "name": "light orange"}, # bright yellow
|
||||
{"bmg": "yor2", "hex": 0xFEBC1F, "name": "yellow"}, # ripe mango
|
||||
{"bmg": "yor1", "hex": 0xFFE71F, "name": "light yellow"},
|
||||
{"bmg": "yor0", "hex": 0xFFFF22, "name": "neon yellow"},
|
||||
{"bmg": "blue2", "hex": 0x1170EC, "name": "dark blue"},
|
||||
{"bmg": "blue1", "hex": 0x75B5F6, "name": "azure"},
|
||||
{"bmg": "green", "hex": 0x0EB00A, "name": "green"},
|
||||
{"bmg": "yellow", "hex": 0xFFFD1E, "name": "neon yellow 2"},
|
||||
{"bmg": "red4", "hex": 0xEE0C10, "name": "vivid red"},
|
||||
{"bmg": "red3", "hex": 0xFF0308, "name": "red"},
|
||||
{"bmg": "red2", "hex": 0xF14A4E, "name": "light red"},
|
||||
{"bmg": "red1", "hex": 0xE46C74, "name": "pink"},
|
||||
{"bmg": "white", "hex": 0xFFFFFF, "name": "white"},
|
||||
{"bmg": "clear", "hex": 0x000000, "name": "clear"},
|
||||
{"bmg": "off", "hex": 0xDDDDDD, "name": "off"},
|
||||
]
|
||||
|
||||
__slots__ = ("bmg", "hex", "name")
|
||||
|
||||
def __init__(self, color_data: any, color_key: str = "name"):
|
||||
colors = list(filter(lambda color: color[color_key].upper() == color_data.upper(), self._all_colors))
|
||||
if len(colors) == 0: raise ColorNotFound(color_data)
|
||||
|
||||
for key, value in colors[0].items():
|
||||
setattr(self, key, value)
|
||||
|
||||
@classmethod
|
||||
def get_all_colors(cls):
|
||||
for color in cls._all_colors:
|
||||
yield cls(color["name"])
|
||||
bmg: str
|
||||
hexadecimal: hex
|
||||
name: str
|
||||
|
||||
@property
|
||||
def color_code(self) -> str:
|
||||
"""
|
||||
Return the color code that can be used in tkinter
|
||||
Return a color code that can be used in tkinter
|
||||
:return: the color code
|
||||
"""
|
||||
return f"#{self.hexadecimal:06X}"
|
||||
|
||||
return f"#{self.hex:06X}"
|
||||
@property
|
||||
def raw(self) -> str:
|
||||
"""
|
||||
return the special control character to start coloring a text
|
||||
:return: return the color control character
|
||||
"""
|
||||
return r"\c{" + self.bmg + "}"
|
||||
|
||||
def color_text(self, text: str) -> str:
|
||||
"""
|
||||
color a text, then reset the color
|
||||
:param text: text to color
|
||||
:return: return the formatted text with the color
|
||||
"""
|
||||
return f'{self.raw}{text}{get(bmg="off").raw}'
|
||||
|
||||
|
||||
def bmg_color_raw(color_name: str) -> str:
|
||||
all_colors: list[MKWColor] = [
|
||||
MKWColor(bmg="white", hexadecimal=0xFFFFFF, name="white"),
|
||||
MKWColor(bmg="clear", hexadecimal=0x000000, name="clear"),
|
||||
MKWColor(bmg="off", hexadecimal=0xDDDDDD, name="off"),
|
||||
|
||||
MKWColor(bmg="yor7", hexadecimal=0xF5090B, name="apple red"),
|
||||
MKWColor(bmg="yor6", hexadecimal=0xE82C09, name="dark red"),
|
||||
MKWColor(bmg="yor5", hexadecimal=0xE65118, name="dark orange"), # flame
|
||||
MKWColor(bmg="yor4", hexadecimal=0xFF760E, name="orange"), # pumpkin
|
||||
MKWColor(bmg="yor3", hexadecimal=0xFFA61F, name="light orange"), # bright yellow
|
||||
MKWColor(bmg="yor2", hexadecimal=0xFEBC1F, name="yellow"), # ripe mango
|
||||
MKWColor(bmg="yor1", hexadecimal=0xFFE71F, name="light yellow"),
|
||||
MKWColor(bmg="yor0", hexadecimal=0xFFFF22, name="neon yellow"),
|
||||
MKWColor(bmg="blue2", hexadecimal=0x1170EC, name="dark blue"),
|
||||
MKWColor(bmg="blue1", hexadecimal=0x75B5F6, name="azure"),
|
||||
MKWColor(bmg="green", hexadecimal=0x0EB00A, name="green"),
|
||||
MKWColor(bmg="yellow", hexadecimal=0xFFFD1E, name="neon yellow 2"),
|
||||
MKWColor(bmg="red4", hexadecimal=0xEE0C10, name="vivid red"),
|
||||
MKWColor(bmg="red3", hexadecimal=0xFF0308, name="red"),
|
||||
MKWColor(bmg="red2", hexadecimal=0xF14A4E, name="light red"),
|
||||
MKWColor(bmg="red1", hexadecimal=0xE46C74, name="pink"),
|
||||
]
|
||||
|
||||
|
||||
def get(**color_datas) -> MKWColor:
|
||||
"""
|
||||
Useful shortcut to place a color
|
||||
:param color_name: name of the color
|
||||
:return: return the color character
|
||||
Get a original track object from keys and its value
|
||||
:param color_datas: dictionary of track key and their value
|
||||
:return: the corresponding original track
|
||||
"""
|
||||
return r"\c{" + MKWColor(color_name).bmg + "}"
|
||||
|
||||
|
||||
def bmg_color_text(color_name: str, text: str) -> str:
|
||||
"""
|
||||
Useful shortcut to color a text
|
||||
:param color_name: name of the color
|
||||
:param text: text to color
|
||||
:return: return the formatted text with the color
|
||||
"""
|
||||
return f'{bmg_color_raw(color_name)}{text}{bmg_color_raw("off")}'
|
||||
try:
|
||||
return next(filter(
|
||||
lambda color: all(getattr(color, key) == value for key, value in color_datas.items()),
|
||||
all_colors
|
||||
))
|
||||
except StopIteration: raise ColorNotFound(color_datas)
|
||||
|
|
|
@ -8,10 +8,9 @@ from PIL import Image
|
|||
from source import threaded
|
||||
from source.mkw import Tag, Slot
|
||||
from source.mkw.Cup import Cup
|
||||
from source.mkw.MKWColor import bmg_color_text, bmg_color_raw
|
||||
from source.mkw import MKWColor
|
||||
from source.mkw.ModSettings import AbstractModSettings
|
||||
from source.mkw.Track import CustomTrack, DefaultTrack, Arena
|
||||
from source.mkw import OriginalTrack
|
||||
from source.progress import Progress
|
||||
from source.safe_eval import safe_eval, multiple_safe_eval
|
||||
from source.wt.szs import SZSPath
|
||||
|
@ -170,9 +169,8 @@ class ModConfig:
|
|||
:return: the modconfig environment
|
||||
"""
|
||||
return {
|
||||
"mod_config": self,
|
||||
"bmg_color_raw": bmg_color_raw,
|
||||
"bmg_color_text": bmg_color_text
|
||||
"mod_config": self,
|
||||
"get_color": MKWColor.get
|
||||
} | (
|
||||
base_env if base_env is not None else {}
|
||||
)
|
||||
|
|
|
@ -86,18 +86,6 @@ def safe_eval(template: "TemplateSafeEval", env: "Env" = None, macros: dict[str,
|
|||
elif node.id in args:
|
||||
raise SafeEvalException(_("CANNOT_SET_ARGUMENT", ' : "', node.id, '"'))
|
||||
|
||||
# when calling any function
|
||||
case ast.Call:
|
||||
# ban the function and method from the environment
|
||||
for callnode in ast.walk(node.func):
|
||||
if isinstance(callnode, ast.Attribute):
|
||||
for attrnode in ast.walk(callnode.value):
|
||||
if isinstance(attrnode, ast.Name):
|
||||
if attrnode.id in globals_ | locals_ or attrnode.id in args:
|
||||
raise SafeEvalException(
|
||||
_("CALLING_FUNCTION_NOT_ALLOWED", ' : "', callnode.attr, '"')
|
||||
)
|
||||
|
||||
# when assigning a value with ":="
|
||||
case ast.NamedExpr:
|
||||
# embed the value into a deepcopy, to avoid interaction with class attribute
|
||||
|
@ -139,3 +127,6 @@ def safe_eval(template: "TemplateSafeEval", env: "Env" = None, macros: dict[str,
|
|||
lambda_template = eval(compile(expression, "<safe_eval>", "eval"), globals_, locals_)
|
||||
self.safe_eval_cache[template_key] = lambda_template # cache the callable for potential latter call
|
||||
return lambda_template
|
||||
|
||||
|
||||
# TODO: disable some method and function call. for example, mod_config.path.unlink() is dangerous !
|
||||
|
|
Loading…
Reference in a new issue