|
| 1 | +import json, os, sys, logging as log |
| 2 | +from datetime import datetime as dt |
| 3 | +from tkinter import messagebox as msg, filedialog as prompt |
| 4 | + |
| 5 | +rootDir = os.path.dirname(os.path.abspath(__file__)) |
| 6 | +initDir = os.path.dirname(os.path.abspath(sys.argv[0])) |
| 7 | +appdataDir = os.path.join(os.getenv('APPDATA'), "Furglitch", "MO2SE") |
| 8 | +logDir = os.path.join(appdataDir, 'logs', f'{dt.now().strftime('%Y-%m-%d %H%M%S')}.log') |
| 9 | +resourceDir = os.path.join(rootDir, "resources") |
| 10 | +iconDir = os.path.join(resourceDir, "icon.ico") |
| 11 | + |
| 12 | +saved = True |
| 13 | +categories = {} |
| 14 | +examples = {} |
| 15 | +startColor = "#000000" |
| 16 | +endColor = "#ffffff" |
| 17 | +gradient = [] |
| 18 | +header = 'Bracket' |
| 19 | +theme = 'Nord' |
| 20 | +themeAccent = 'Blue' |
| 21 | +catCasing = 'Unchanged' |
| 22 | +subCasing = 'Unchanged' |
| 23 | + |
| 24 | +# Logging |
| 25 | +if not os.path.exists(os.path.join(appdataDir, 'logs')): os.makedirs(os.path.join(appdataDir, 'logs')) |
| 26 | +open(logDir, "w").close() |
| 27 | +log.basicConfig( |
| 28 | + filename=logDir, |
| 29 | + level=log.DEBUG, |
| 30 | + format='%(asctime)s - %(levelname)s - %(message)s', |
| 31 | + datefmt='%Y-%m-%d %H:%M:%S' |
| 32 | +) |
| 33 | +log.info("MO2SE Started") |
| 34 | + |
| 35 | +# Menu Functions |
| 36 | +def fileNew(): |
| 37 | + global startColor, endColor |
| 38 | + if msg.askyesno("Confirm", "Are you sure you want to create a new file? This will clear all current data."): |
| 39 | + categories.clear() |
| 40 | + startColor = "#000000" |
| 41 | + endColor = "#ffffff" |
| 42 | + log.info("Data cleared for new file") |
| 43 | + global saved; saved = True |
| 44 | + |
| 45 | +def fileSave(): |
| 46 | + global categories, startColor, endColor |
| 47 | + path = prompt.asksaveasfilename(initialdir=initDir, filetypes=[("JSON", "*.json")], defaultextension=".json") |
| 48 | + if path: |
| 49 | + data = {"categories": categories, "gradient": {"startColor": startColor, "endColor": endColor}} |
| 50 | + with open(path, "w") as f: json.dump(data, f, indent=4) |
| 51 | + global saved; saved = True |
| 52 | + log.info(f"File Saved: {path}") |
| 53 | + |
| 54 | +def fileOpen(path=None): |
| 55 | + global categories, startColor, endColor, saved |
| 56 | + if not saved: |
| 57 | + if not msg.askyesno("Changes Not Saved", "You've adjusted your separator list but haven't saved!\nAre you sure you want to open this file?"): |
| 58 | + return |
| 59 | + if path is None: path = prompt.askopenfilename(initialdir=initDir, filetypes=[("JSON", "*.json")], defaultextension=".json") |
| 60 | + if path: |
| 61 | + with open(path, "r") as f: |
| 62 | + data = json.load(f) |
| 63 | + categories.clear() |
| 64 | + categories.update(data["categories"]) |
| 65 | + startColor = data["gradient"]["startColor"] |
| 66 | + endColor = data["gradient"]["endColor"] |
| 67 | + saved = True |
| 68 | + log.info(f"File Opened: {path}") |
| 69 | + else: return |
| 70 | + |
| 71 | +def exampleGet(bar, list, subBox, startIndicator, endIndicator, startLabel, endLabel): |
| 72 | + global examples |
| 73 | + path = os.path.join(resourceDir, "examples") |
| 74 | + for file in os.listdir(path): |
| 75 | + filepath = os.path.join(path,file) |
| 76 | + if file.endswith('.json') and os.path.isfile(filepath): |
| 77 | + bar.add_command(label=file.removesuffix(".json"), command=lambda filepath=filepath: exampleOpen(filepath, list, subBox, startIndicator, endIndicator, startLabel, endLabel)) |
| 78 | + log.info(f"Example Found: {file.removesuffix('.json')}") |
| 79 | + |
| 80 | +def exampleOpen(path, tree, subBox, startIndicator, endIndicator, startLabel, endLabel): |
| 81 | + global startColor, endColor, categories, saved |
| 82 | + if not saved: |
| 83 | + if not msg.askyesno("Unsaved Changes", "You have unsaved changes. Are you sure you want to open an example?"): |
| 84 | + return |
| 85 | + saved = True |
| 86 | + log.info(f"Example Selected: {os.path.basename(path).removesuffix('.json')}") |
| 87 | + fileOpen(path) |
| 88 | + expanded_categories = set() |
| 89 | + for category_id in tree.get_children(): |
| 90 | + if tree.item(category_id, 'open'): expanded_categories.add(tree.item(category_id, 'values')[0].strip()) |
| 91 | + tree.delete(*tree.get_children()) |
| 92 | + for category, details in categories.items(): |
| 93 | + categoryID = tree.insert("", "end", text="", values=(category,)) |
| 94 | + categories[category]["id"] = categoryID |
| 95 | + for subcategory in details["sub"]: |
| 96 | + subcategoryID = tree.insert(categoryID, "end", text="", values=(f"\u00A0\u00A0\u00A0\u00A0{subcategory}",)) |
| 97 | + categories[category]["sub"][subcategory]["id"] = subcategoryID |
| 98 | + if category in expanded_categories: tree.item(categoryID, open=True) |
| 99 | + subBox.config(values=list(categories.keys())) |
| 100 | + startIndicator.config(bg=startColor) |
| 101 | + startLabel.config(text=f"Start Color: {startColor}") |
| 102 | + endIndicator.config(bg=endColor) |
| 103 | + endLabel.config(text=f"End Color: {endColor}") |
| 104 | + |
| 105 | +# Button Functions |
| 106 | +def sepAdd(type, name, parent_category=None): |
| 107 | + if type == "cat": |
| 108 | + if name not in categories: |
| 109 | + categories[name] = {"id": None, "sub": {}} |
| 110 | + log.info(f"Category Added: {name}") |
| 111 | + else: warn(1, name) |
| 112 | + elif type == "sub" and parent_category is not None: |
| 113 | + if parent_category in categories: |
| 114 | + if name not in categories[parent_category]["sub"]: |
| 115 | + categories[parent_category]["sub"][name] = {"id": None} |
| 116 | + log.info(f"Subcategory Added: {name} in Category {parent_category}") |
| 117 | + else: warn(2, name, parent_category) |
| 118 | + else: warn(3, parent_category) |
| 119 | + else: error(1, type) |
| 120 | + global saved; saved = False |
| 121 | + |
| 122 | +def sepRemove(type, name, children, parent): |
| 123 | + if type == "cat": |
| 124 | + if children: warn(4, name) |
| 125 | + else: |
| 126 | + if name in categories: |
| 127 | + del categories[name] |
| 128 | + log.info(f"Category Removed: {name}") |
| 129 | + else: warn(3, name) |
| 130 | + elif type == "sub": |
| 131 | + if parent in categories and name in categories[parent]["sub"]: |
| 132 | + del categories[parent]["sub"][name] |
| 133 | + log.info(f"Subcategory Removed: {name} in Category {parent}") |
| 134 | + else: warn(3, parent) |
| 135 | + else: error(1, type) |
| 136 | + global saved; saved = False |
| 137 | + |
| 138 | +def outputGen(): |
| 139 | + global gradient |
| 140 | + gradientGet() |
| 141 | + path = prompt.askdirectory() |
| 142 | + profilePath = os.path.join(path, 'profiles', 'default') |
| 143 | + modsPath = os.path.join(path, 'mods') |
| 144 | + if not os.path.exists(profilePath): os.makedirs(profilePath) |
| 145 | + f = open(profilePath + '/modlist.txt', "w") |
| 146 | + f.close() |
| 147 | + if not os.path.exists(modsPath): os.makedirs(modsPath) |
| 148 | + with open(os.path.join(profilePath + '/modlist.txt'), "r+") as l: |
| 149 | + lines = l.readlines() |
| 150 | + l.seek(0, 0) |
| 151 | + i = 0; j=0 |
| 152 | + for category, details in categories.items(): |
| 153 | + catSep = headerGet("start", header) + applyCasing("cat", category) + headerGet("end", header) + '_separator' |
| 154 | + os.mkdir(os.path.join(modsPath, catSep)) |
| 155 | + with open(os.path.join(modsPath, catSep, 'meta.ini'), 'w') as meta: |
| 156 | + meta.write(f"[General]\ncolor={gradient[j]}") |
| 157 | + lines.insert(0, "+"+catSep+'\n') |
| 158 | + j += 1 |
| 159 | + for subcategory in details["sub"]: |
| 160 | + i += 1 |
| 161 | + subSep = str(i)+'. ' + applyCasing("sub", subcategory) + '_separator' |
| 162 | + os.mkdir(os.path.join(modsPath, subSep)) |
| 163 | + lines.insert(0, "+"+subSep+'\n') |
| 164 | + l.writelines(lines) |
| 165 | + log.info(f"Output Generated at {path}") |
| 166 | + |
| 167 | + |
| 168 | +# Settings Functions |
| 169 | +def themeGet(type, theme=None): |
| 170 | + output = '' |
| 171 | + with open(os.path.join(resourceDir, 'themes.json')) as f: |
| 172 | + data = json.load(f) |
| 173 | + if type == "name": |
| 174 | + output = list(data.keys()) |
| 175 | + log.info(f"Themes Loaded: {output}") |
| 176 | + elif type == "theme": |
| 177 | + output = data[theme] |
| 178 | + log.info(f"Theme {theme} info loaded: {output}") |
| 179 | + elif type == "color": |
| 180 | + output = data[theme][type] |
| 181 | + log.info(f"Theme {theme} color loaded: {type} - {output}") |
| 182 | + return output |
| 183 | + |
| 184 | +def headerGet(type, header=None): |
| 185 | + output = '' |
| 186 | + with open(os.path.join(resourceDir, 'headers.json')) as f: |
| 187 | + data = json.load(f) |
| 188 | + if type == "name": |
| 189 | + output = list(data.keys()) |
| 190 | + log.info(f"Headers Loaded: {output}") |
| 191 | + elif type == "start" or type == "end": |
| 192 | + output = data[header][type] |
| 193 | + log.info(f"Headers {header} loaded: {type} - {output}") |
| 194 | + return output |
| 195 | + |
| 196 | +def applyCasing(type, text): |
| 197 | + global catCasing, subCasing |
| 198 | + if type == "cat": |
| 199 | + if catCasing == "Capitalize": return text.title() |
| 200 | + elif catCasing == "UPPER": return text.upper() |
| 201 | + elif catCasing == "lower": return text.lower() |
| 202 | + else: return text |
| 203 | + if type == "sub": |
| 204 | + if subCasing == "Capitalize": return text.title() |
| 205 | + elif subCasing == "UPPER": return text.upper() |
| 206 | + elif subCasing == "lower": return text.lower() |
| 207 | + else: return text |
| 208 | + |
| 209 | +def casingSet(type, case): |
| 210 | + global catCasing, subCasing |
| 211 | + if type == "cat": |
| 212 | + catCasing = case |
| 213 | + log.info(f"Category Casing Set: {case}") |
| 214 | + if type == "sub": |
| 215 | + subCasing = case |
| 216 | + log.info(f"Subcategory Casing Set: {case}") |
| 217 | + |
| 218 | +def settingsGet(): |
| 219 | + global theme, themeAccent, header, catCasing, subCasing |
| 220 | + if not os.path.exists(os.path.join(appdataDir, 'MO2SE.json')): |
| 221 | + data = {"theme": {"name": theme, "accent": themeAccent}, "header": header, "casing": {"cat": catCasing, "sub": subCasing}} |
| 222 | + f = open(os.path.join(appdataDir, 'MO2SE.json'), "w") |
| 223 | + json.dump(data, f, indent=4) |
| 224 | + f.close() |
| 225 | + log.info("Default Settings File Created") |
| 226 | + else: |
| 227 | + with open(os.path.join(appdataDir, 'MO2SE.json'), "r") as f: |
| 228 | + data = json.load(f) |
| 229 | + if data["theme"]["name"] in themeGet("name"): theme = data["theme"]["name"] |
| 230 | + else: theme = "Nord" |
| 231 | + if data["theme"]["accent"] in ['Red', 'Orange', 'Yellow', 'Green', 'Blue', 'Purple']: themeAccent = data["theme"]["accent"] |
| 232 | + else: themeAccent = "Blue" |
| 233 | + if data["header"] in headerGet("name"): header = data["header"] |
| 234 | + else: header = "Bracket" |
| 235 | + if data["casing"]["cat"] in ['Unchanged', 'Capitalize', 'UPPER', 'lower']: catCasing = data["casing"]["cat"] |
| 236 | + else: catCasing = "Unchanged" |
| 237 | + if data["casing"]["sub"] in ['Unchanged', 'Capitalize', 'UPPER', 'lower']: subCasing = data["casing"]["sub"] |
| 238 | + else: subCasing = "Unchanged" |
| 239 | + log.info(f"Settings Loaded: Theme {theme}, ThemeAccent {themeAccent}, Header {header}, Category Casing {catCasing}, Subcategory Casing {subCasing}") |
| 240 | + |
| 241 | +def settingsCheck(): |
| 242 | + global theme, themeAccent, header, catCasing, subCasing |
| 243 | + with open(os.path.join(appdataDir, 'MO2SE.json'), "r") as f: |
| 244 | + data = json.load(f) |
| 245 | + if theme == data["theme"]["name"] and themeAccent == data["theme"]["accent"] and header == data["header"] and catCasing == data["casing"]["cat"] and subCasing == data["casing"]["sub"]: |
| 246 | + log.info("Settings match saved settings") |
| 247 | + return True |
| 248 | + else: |
| 249 | + log.info("Settings do not match saved settings") |
| 250 | + return False |
| 251 | + |
| 252 | +def settingsSave(): |
| 253 | + global theme, header, themeAccent, casing |
| 254 | + with open(os.path.join(appdataDir + '/MO2SE.json'), "w") as f: |
| 255 | + data = {"theme": {"name": theme, "accent": themeAccent}, "header": header, "casing": {"cat": catCasing, "sub": subCasing}} |
| 256 | + json.dump(data, f, sort_keys=True, indent=4) |
| 257 | + log.info(f"Settings Saved: Theme {theme}, Theme Accent {themeAccent}, Header {header}, Category Casing {catCasing}, Subcategory Casing {subCasing}") |
| 258 | + |
| 259 | +# Gradient Processing |
| 260 | +def hexRGB(hex): |
| 261 | + hex = hex.lstrip('#') |
| 262 | + log.info(f"Hex Converted to RGB: {hex}") |
| 263 | + return tuple(int(hex[i:i+2], 16) for i in (0, 2, 4)) |
| 264 | + |
| 265 | +def stepRGB(start, end, steps): |
| 266 | + if steps == 1: return [start] |
| 267 | + gradient = [] |
| 268 | + for i in range(steps): |
| 269 | + t = i / (steps - 1) |
| 270 | + r = int(start[0] + (end[0] - start[0]) * t) |
| 271 | + g = int(start[1] + (end[1] - start[1]) * t) |
| 272 | + b = int(start[2] + (end[2] - start[2]) * t) |
| 273 | + gradient.append((r, g, b)) |
| 274 | + log.info(f"Generated RGB Gradient from {startColor} to {endColor} with {steps} steps") |
| 275 | + return gradient |
| 276 | + |
| 277 | +def rgbHex(rgb): |
| 278 | + log.info(f"RGB Converted to Hex: {rgb}") |
| 279 | + return '#{:02x}{:02x}{:02x}'.format(*rgb) |
| 280 | + |
| 281 | +def gradientGet(): |
| 282 | + global startColor, endColor, gradient |
| 283 | + if len(categories) == 0: return |
| 284 | + startRGB = hexRGB(startColor) |
| 285 | + endRGB = hexRGB(endColor) |
| 286 | + gradRGB = stepRGB(startRGB, endRGB, len(categories)) |
| 287 | + |
| 288 | + gradient = [rgbHex(rgb) for rgb in gradRGB] |
| 289 | + log.info(f"Gradient Generated: {gradient}") |
| 290 | + |
| 291 | +# Error Handling |
| 292 | +def warn(code, text, text2=None): |
| 293 | + match code: |
| 294 | + case 1: |
| 295 | + msg.showwarning("Warning", f"Category already exists: {text}") |
| 296 | + log.warning(f"Category Already Exists: {text}") |
| 297 | + case 2: |
| 298 | + msg.showwarning("Warning", f"Subcategory already exists: {text} in Category {text2}") |
| 299 | + log.warning(f"Subcategory Already Exists: {text} in Category {text2}") |
| 300 | + case 3: |
| 301 | + msg.showwarning("Warning", f"Category {text} does not exist") |
| 302 | + log.warning(f"Category Does Not Exist: {text}") |
| 303 | + case 4: |
| 304 | + msg.showwarning("Warning", f"Cannot remove Category with existing subcategories: {text}") |
| 305 | + log.warning(f"Cannot Remove Category with Existing Subcategories: {text}") |
| 306 | + |
| 307 | +def error(code, text=None): |
| 308 | + match code: |
| 309 | + case 1: |
| 310 | + msg.showerror("Error", "Invalid Type\n\nYou shouldn't be seeing this. Please submit an issue on GitHub.") |
| 311 | + log.error(f"Invalid Type: {text}") |
| 312 | + |
| 313 | +def settingsPrompt(): |
| 314 | + log.info("Prompt: Settings Not Saved") |
| 315 | + return msg.askyesno("Settings Not Saved", "You've adjusted your settings but haven't saved! Are you sure you want to exit?") |
| 316 | + |
| 317 | +def filePrompt(): |
| 318 | + log.info("Prompt: Changes Not Saved") |
| 319 | + return msg.askyesno("Changes Not Saved", "You've adjusted your separator list but haven't saved! Are you sure you want to exit?") |
0 commit comments