diff --git a/__init__.py b/__init__.py index d0106f9..d271d7e 100644 --- a/__init__.py +++ b/__init__.py @@ -11,16 +11,21 @@ import sys import stat import mimetypes + mimetypes.add_type('application/javascript', '.js') + WEBROOT = Path(__file__).parent / "web" FLOWS_PATH = WEBROOT / "flows" CORE_PATH = WEBROOT / "core" FLOWER_PATH = WEBROOT / "flower" FLOW_PATH = WEBROOT / "flow" +CUSTOM_THEMES_DIR = WEBROOT / 'custom-themes' # Updated CUSTOM_NODES_DIR = Path(__file__).parent.parent EXTENSION_NODE_MAP_PATH = Path(__file__).parent.parent / "ComfyUI-Manager" / "extension-node-map.json" + logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') logger = logging.getLogger(__name__) + AppConfig = Dict[str, Any] Routes = web.RouteTableDef FLOWS_DOWNLOAD_PATH = 'https://github.com/diStyApps/flows_lib' @@ -32,7 +37,7 @@ PURPLE = "\033[38;5;129m" RESET = "\033[0m" FLOWMSG = f"{PURPLE}Flow{RESET}" - +ALLOWED_EXTENSIONS = {'css'} class RouteManager: @staticmethod @@ -79,6 +84,9 @@ def setup_app_routes(app: web.Application) -> None: logger.error(f"{FLOWMSG}: Failed to iterate over flows directory: {e}") if CORE_PATH.is_dir(): + # Add the specific handlers before the general static route + app.router.add_get('/core/css/themes/list', list_themes_handler) + app.router.add_get('/core/css/themes/{filename}', get_theme_css_handler) app.router.add_static('/core/', path=CORE_PATH, name='core') if FLOWER_PATH.is_dir(): @@ -174,7 +182,6 @@ async def update_package_handler(request: web.Request) -> web.Response: return web.json_response({'status': 'error', 'message': f"An error occurred while updating package '{package_name}': {e}"}, status=500) def remove_readonly(func, path, excinfo): - # This makes the file writable and retries the removal os.chmod(path, stat.S_IWRITE) func(path) @@ -195,7 +202,6 @@ async def uninstall_package_handler(request: web.Request) -> web.Response: try: logger.info(f"{FLOWMSG}: Uninstalling custom node '{package_name}'...") - # Attempt to remove the directory with retry and handling of access errors shutil.rmtree(install_path, onerror=remove_readonly) logger.info(f"{FLOWMSG}: Custom node '{package_name}' uninstalled successfully.") @@ -236,38 +242,75 @@ async def save_config_handler(request: web.Request) -> web.Response: logger.error(f"{FLOWMSG}: Error saving configuration: {e}") return web.Response(status=500, text=f"{FLOWMSG}: Error saving configuration: {str(e)}") +if not CUSTOM_THEMES_DIR.exists(): + try: + CUSTOM_THEMES_DIR.mkdir(parents=True, exist_ok=True) + logger.info(f"Created custom-themes directory at {CUSTOM_THEMES_DIR}") + except Exception as e: + logger.error(f"Failed to create custom-themes directory: {e}") + +def allowed_file(filename: str) -> bool: + return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS + +async def list_themes_handler(request: web.Request) -> web.Response: + themes_dir = CUSTOM_THEMES_DIR + try: + if not themes_dir.exists(): + logger.warning(f"Custom themes directory does not exist: {themes_dir}") + return web.json_response([], status=200) + + css_files = [file.name for file in themes_dir.iterdir() if file.is_file() and allowed_file(file.name)] + return web.json_response(css_files) + + except Exception as e: + logger.error(f"Error listing theme files: {e}") + return web.json_response({'error': 'Failed to list theme files.'}, status=500) + +async def get_theme_css_handler(request: web.Request) -> web.Response: + filename = request.match_info.get('filename') + + if not allowed_file(filename): + logger.warning(f"Attempt to access disallowed file type: {filename}") + raise web.HTTPNotFound() + + themes_dir = CUSTOM_THEMES_DIR + file_path = themes_dir / filename + + if not file_path.exists() or not file_path.is_file(): + logger.warning(f"CSS file not found: {file_path}") + raise web.HTTPNotFound() + + try: + return web.FileResponse(path=file_path) + except Exception as e: + logger.error(f"Error serving CSS file '{filename}': {e}") + raise web.HTTPInternalServerError(text="Internal Server Error") + def download_or_update_flows() -> None: try: with tempfile.TemporaryDirectory() as tmpdirname: temp_repo_path = Path(tmpdirname) / "Flows" - # logger.info(f"{FLOWMSG}:Cloning flows repository into temporary directory {temp_repo_path}") logger.info(f"{FLOWMSG}: Downloading Flows") result = subprocess.run(['git', 'clone', FLOWS_DOWNLOAD_PATH, str(temp_repo_path)], capture_output=True, text=True) if result.returncode != 0: logger.error(f"{FLOWMSG}: Failed to clone flows repository:\n{result.stderr}") - return else: - # logger.info(f"{FLOWMSG}: Flows repository cloned successfully into {temp_repo_path}") pass if not FLOWS_PATH.exists(): FLOWS_PATH.mkdir(parents=True) - # logger.info(f"{FLOWMSG}: Created flows directory at {FLOWS_PATH}") for item in temp_repo_path.iterdir(): - if item.name == '.git': + if item.name in ['.git', '.github']: continue dest_item = FLOWS_PATH / item.name if item.is_dir(): if dest_item.exists(): - # logger.info(f"{FLOWMSG}: Updating existing directory {dest_item}") _copy_directory(item, dest_item) else: - # logger.info(f"{FLOWMSG}: Copying new directory {item.name} to {dest_item}") shutil.copytree(item, dest_item) else: - # logger.info(f"{FLOWMSG}: Copying file {item.name} to {dest_item}") shutil.copy2(item, dest_item) logger.info(f"{FLOWMSG}: Flows have been updated successfully.") except Exception as e: @@ -275,7 +318,7 @@ def download_or_update_flows() -> None: def _copy_directory(src: Path, dest: Path) -> None: for item in src.iterdir(): - if item.name == '.git': + if item.name in ['.git', '.github']: continue dest_item = dest / item.name if item.is_dir(): diff --git a/web/core/content.html b/web/core/content.html index e13deef..5aec955 100644 --- a/web/core/content.html +++ b/web/core/content.html @@ -1,11 +1,11 @@
-
+
-
+
-
+
@@ -13,7 +13,7 @@
-
+

Recent

diff --git a/web/core/css/main.css b/web/core/css/main.css index a7c39ed..f6953b2 100644 --- a/web/core/css/main.css +++ b/web/core/css/main.css @@ -1,4 +1,3 @@ - * { box-sizing: border-box; margin: 0; @@ -7,26 +6,48 @@ } :root { - --background-color: #121213; - --border-color: #320848; - --text-color: #cba3e5; - --hover-background-color: #8621b7; - --hover-box-shadow: 0 0 8px rgba(134, 33, 183, 0.6); - --focus-border-color: #570d7b; - --focus-box-shadow: 0 0 8px rgba(134, 33, 183, 0.6); - --focus-outline-color: #8621b7; - --scrollbar-thumb-color: #570d7b; - --scrollbar-track-color: #1e1e1f; + --control-height: 36px; + --color-primary: #430474; + --color-secondary: #f4b6ff; + --color-accent: #8621b7; + --color-highlight: #7d0ab6; + --color-primary-text: #cba3e5; + --color-border: #570d7b; + --color-background: #181b1d; + --color-background-secondary: #131312; + --color-background-pattern: #4d0f7c; + --color-header-background: #131312; + --color-header-logo-text: #570d7b; + --color-header-text:#570d7b; + --color-social-icons: #570d7b; + --color-button-primary: #570d7b; + --color-button-primary-hover: #8621b7; + --color-button-primary-text: #f4b6ff; + --color-button-primary-text-hover: #131312; + --color-button-primary-active: #8621b7; + --color-button-primary-text-active: #8621b7; + --color-button-secondary: #131312; + --color-button-secondary-hover: #8621b7; + --color-button-secondary-text: #f4b6ff; + --color-button-secondary-text-hover: #131312; + --color-button-secondary-active: #8621b7; + --color-button-secondary-text-active: #f4b6ff; + --color-progress-background: #2c063f; + --color-progress-value:#7d0ab6; + --color-scrollbar-track: #1e1e1f; + --color-scrollbar-thumb: #570d7b; + --color-input-range-thumb: #f4b6ff; + --color-input-range-background: #570d7b; + --color-spinner:#570d7b; + --color-spinner-highlight:#7d0ab6; } html, body { height: 100%; - margin: 0; - padding: 0; - background-color: #181b1d; + background-color: var(--color-background); background-size: 32px 32px; - background-image: radial-gradient(circle, #430474 1px, transparent 1px); - color: #cba3e5; + background-image: radial-gradient(circle, var(--color-background-pattern) 1px, transparent 1px); + color: var(--color-primary-text); overflow-x: hidden; } @@ -40,16 +61,14 @@ header { display: flex; justify-content: space-between; align-items: center; - background-color: #121213; - color: #fff; + background-color: var(--color-header-background); padding-left: 50px; padding-right: 50px; padding-top: 10px; text-align: left; - color: #570d7b; - border-bottom: 1px dashed; - border-color: #570d7b #570d7b; + border-bottom: 1px dashed var(--color-border); } + #right-header { display: flex; justify-content: center; @@ -58,49 +77,40 @@ header { padding: 5px; } -#Support{ - /* padding: 10px; */ - color: #cba3e5; +#Support { + color: var(--color-header-text); margin: 10px; font-size: 12px; } + a { text-decoration: none; } -.appName{ - /* background-color: #570d7b; */ + +.appName { padding: 10px; - color: #cba3e5; - /* height: 100%; */ - /* border-radius: 10%; */ - border-top: 1px dashed; - border-left: 1px dashed; - border-right: 1px dashed; - - border-color: #570d7b #570d7b; + color: var(--color-header-text); + border-top: 1px dashed var(--color-border); + border-left: 1px dashed var(--color-border); + border-right: 1px dashed var(--color-border); font-size: 12px; } -#github{ - /* padding: 5px; */ - /* color: #570d7b; */ - /* border-radius: 5px; */ - /* margin-right: 10px; */ -} +/* Animations */ @keyframes animation-text { - 0% {opacity:0;} - 40% {opacity:0;} - 100% { opacity:1;} + 0% { opacity: 0; } + 40% { opacity: 0; } + 100% { opacity: 1; } } @keyframes animation-left { - from {left: 30px;} - to { left: -30px;} + from { left: 30px; } + to { left: -30px; } } @keyframes animation-right { - from {right: 30px;} - to { right: -30px;} + from { right: 30px; } + to { right: -30px; } } header #logo { @@ -115,10 +125,10 @@ header #img-logo { header .logo-text { display: inline-block; - position: relative; - font-family: Arial; + position: relative; + font-family: Arial; cursor: pointer; - color: #570d7b; + color: var(--color-header-text); } header .logo-text .text { @@ -154,11 +164,10 @@ header .right { display: flex; flex-direction: column; flex: 1; - /* background-color: #121213; */ - color: #cba3e5; - /* padding: 10px; */ + color: var(--color-primary-text); overflow: hidden; } + .content { display: flex; flex: 1; @@ -171,7 +180,9 @@ header .right { padding: 4px 6px; } -.content .mid-col, .content .left-col, .content .right-col { +.content .mid-col, +.content .left-col, +.content .right-col { display: flex; flex-direction: column; align-items: left; @@ -179,61 +190,56 @@ header .right { .content .left-col { flex: 1.1; - background-color: #181b1d; + background-color: var(--color-background); } .content .mid-col { flex: 2.5; - border-left: 1px dashed #570d7b; - border-right: 1px dashed #570d7b; - /* align-items: center; */ + border-left: 1px dashed var(--color-border); + border-right: 1px dashed var(--color-border); justify-content: space-around; min-height: 100%; } + .content .right-col { flex: 0.5; - background-color: #181b1d; + background-color: var(--color-background); } #control button { - padding: 5px; - background-color: #570d7b; - user-select: none; -} - -#control button:hover { - box-shadow: 0px 0px 20px var(--box-shadow-color); - background-color: #8621b7; -} - -#dropdownIdSearch { - padding: 8px; - border: 1px solid #ccc; - border-radius: 4px; - box-sizing: border-box; + padding: 5px; + background-color: var(--color-primary); + user-select: none; } -#dropdownIdSearch:focus { - outline: none; - border-color: #0056b3; +#control button:hover { + box-shadow: var(--shadow-hover); + background-color: var(--color-accent); } -input[type="text"], input[type="number"], textarea { - border: 1px solid #320848; - transition: border-color 0.3s ease-in-out; +input[type="text"], +input[type="number"], +textarea { + border:none; + border-bottom: 1px solid var(--color-border); + transition: border-color 0.3s ease-in-out; padding: 8px; - background-color: #121213; - color: #cba3e5; + background-color: var(--color-background-secondary); + color: var(--color-primary-text); + margin:0 10px; } -input[type="text"]:hover, input[type="number"]:hover, textarea:hover { - border-color: #570d7b; +input[type="text"]:hover, +input[type="number"]:hover, +textarea:hover { + border-color: var(--color-border); } -input[type="text"]:focus, input[type="number"]:focus, textarea:focus { - border-color: #8621b7; - outline: none; - box-shadow: 0 0 8px rgba(134, 33, 183, 0.6); +input[type="text"]:focus, +input[type="number"]:focus, +textarea:focus { + border-color: var(--color-primary-text); + outline: none; } #title { @@ -241,7 +247,7 @@ input[type="text"]:focus, input[type="number"]:focus, textarea:focus { flex-direction: row; align-items: center; margin: 3px 0; - background-color: #121213; + background-color: var(--color-background-secondary); width: 100%; text-align: left; } @@ -251,7 +257,8 @@ input[type="text"]:focus, input[type="number"]:focus, textarea:focus { user-select: none; } -#title input[type="text"], #title textarea { +#title input[type="text"], +#title textarea { flex: 3; } @@ -260,7 +267,7 @@ input[type="range"]::-webkit-slider-thumb { width: 10px; height: 10px; border-radius: 100%; - background: #ccb1d9; + background: var(--color-input-range-thumb); cursor: pointer; margin: 2px; } @@ -268,10 +275,31 @@ input[type="range"]::-webkit-slider-thumb { input[type="range"] { -webkit-appearance: none; width: 100%; - background-color: #570d7b; + background-color: var(--color-input-range-background); padding: 4px; } +#buttonsgen { + display: flex; + justify-content: right; +} + +#buttonsgen button { + display: flex; + justify-content: right; + background-color: var(--color-button-primary); + padding: 10px; + margin: 0px 10px; + color: var(--color-button-primary-text); + transition: background-color 0.3s ease, color 0.3s ease; +} + +#buttonsgen button:hover { + background-color: var(--color-button-primary-hover); + color: var(--color-button-primary-text-hover); + +} + .stepper-container { width: 100%; display: flex; @@ -279,7 +307,7 @@ input[type="range"] { flex-direction: row; justify-content: space-around; margin-bottom: 5px; - background-color: #121213; + background-color: var(--color-background-secondary); font-size: 15px; font-weight: bold; } @@ -292,57 +320,147 @@ input[type="range"] { width: 60px; text-align: center; border: none; - color: #cba3e5; -} - -.stepper-container #seed1Input { - flex: 2; + color: var(--color-primary-text); } .stepper-container span { - color: #cba3e5; - /* font-size: 1.1em; */ + color: var(--color-primary-text); flex: 1.8; } .stepper-container button { width: 30px; height: 30px; + font-weight: bold; + color: var(--color-button-secondary-text); + background-color: var(--color-button-secondary); + } + + button { - padding: 5px; + padding: 5px; border: none; - background-color: #121213; + background-color: var(--color-background-secondary); font-size: 1.2em; - color: #f4b6ff; - font-weight: bold; + color: var(--color-secondary); + font-weight: bold; cursor: pointer; - transition: transform 0.3s, box-shadow 0.3s, background-color 0.3s; - margin-left: 1px; + transition: background-color 0.3s ease, color 0.3s ease; + margin-left: 1px; user-select: none; -} +} button:last-child { - margin-right: 0; + margin-right: 0; +} + +button.active { + background-color: var(--color-button-secondary-active); + color: var(--color-button-secondary-text-active); + /* box-shadow: var(--shadow-hover); */ } button:hover { - box-shadow: 0px 0px 20px var(--box-shadow-color); - background-color: #8621b7; + background-color: var(--color-button-secondary-hover); + color: var(--color-button-secondary-text-hover); } -button:active { - transform: translateY(1px); - box-shadow: 0px 0px 10px var(--box-shadow-color); +input[type='number']::-webkit-inner-spin-button, +input[type='number']::-webkit-outer-spin-button { + -webkit-appearance: none; + margin: 0; } - -#canvas { - border: 1px dashed #570d7b; + +input[type='number'] { + -moz-appearance: textfield; +} + +.dimension-selector-container { display: flex; flex-direction: column; - align-items: center; - justify-content: center; - background-color: #121213; + background-color: var(--color-background-secondary); + color: var(--color-primary-text); + width: 100%; + margin-bottom: 4px; + padding: 6px 0; +} + +#dimension-selector { + display: flex; + width: 100%; +} + +.dimension-stepper { + display: flex; + flex-direction: column; + width: 100%; +} + +.dimension-stepper label { + margin-bottom: 5px; + text-align: center; + font-size: 0.9em; +} + +.stepper { + display: flex; +} + +.dimension-stepper .stepper__button { + background-color: var(--color-background-secondary); + color: var(--color-primary-text); + padding: 5px 10px; + cursor: pointer; + transition: background-color 0.3s ease, color 0.3s ease; + width: 100%; + border: none; +} + +.stepper__input { + border: 1px solid var(--color-background-secondary); + background-color: var(--color-background-secondary); + color: var(--color-primary-text); + padding: 5px; + width: 80px; + text-align: center; + transition: background-color 0.3s ease, color 0.3s ease; +} + +.swap-btn { + background-color: var(--color-background-secondary); + border: none; + color: var(--color-primary-text); + padding: 5px 10px; + cursor: pointer; + transition: background-color 0.3s ease, color 0.3s ease; +} + +.aspect-ratio-selector__select { + border-top: 1px solid var(--color-background-secondary); + background-color: var(--color-background-secondary); + color: var(--color-primary-text); + padding: 5px; + width: 100%; + outline: none; + +} + +.stepper__button:hover, +.swap-btn:hover { + /* background-color: var(--color-accent); */ + /* box-shadow: var(--shadow-hover); */ + background-color: var(--color-button-secondary-hover); + color: var(--color-button-secondary-text-hover); + border: none; + +} + +.dropdown-stepper-container, +.multi-stepper-container { + background-color: var(--color-background-secondary); + margin-bottom: 6px; + padding: 10px; } #main-progress { @@ -353,12 +471,12 @@ button:active { z-index: 3; border-radius: 0; height: 20px; - background-color: #2c063f; + background-color: var(--color-progress-background); border: 0; } #main-progress::-moz-progress-bar { - background: #7d0ab6; + background: var(--color-highlight); } #main-progress::-webkit-progress-bar { @@ -366,14 +484,14 @@ button:active { } #main-progress::-webkit-progress-value { - background: #7d0ab6; + background: var(--color-progress-value); } #control { display: flex; justify-content: center; - background-color: #121213; - border-top: 1px dashed #570d7b; + background-color: var(--color-background-secondary); + border-top: 1px dashed var(--color-primary); padding: 6px; } @@ -381,16 +499,16 @@ button:active { height: 20px; width: 20px; border-radius: 10%; - border: 4px dashed; - border-color: #570d7b #570d7b; + border: 4px dashed var(--color-spinner); margin-right: 10px; } - + + .spin { animation: spin 1s infinite ease-out; - border-color: #570d7b #7d0ab6 !important; + border-color: var(--color-spinner) var(--color-spinner-highlight) !important; } - + @keyframes spin { 0% { transform: rotate(0deg); @@ -402,43 +520,21 @@ button:active { #loading-area { display: none; - background-color: rgba(0, 0, 0, 0.5); + background-color: var(--color-background-secondary); z-index: 100; display: flex; justify-content: center; align-items: center; } -@media (max-width: 768px) { - .content { - flex-direction: column; - } -} - -#buttonsgen { - display: flex; - justify-content: right; -} - -#buttonsgen button { - display: flex; - justify-content: right; - background-color: #570d7b; - padding: 10px; - margin: 0px 10px; -} - -#buttonsgen button:hover { - background-color: #8621b7; -} footer { - background-color: #121213; - color: #570d7b; + background-color: var(--color-background-secondary); + color: var(--color-primary-text); padding: 15px 50px; text-align: center; font-family: 'Gill Sans', 'Gill Sans MT', Calibri, 'Trebuchet MS', sans-serif; - border-top: 1px dashed #570d7b; + border-top: 1px dashed var(--color-border); margin-top: auto; } @@ -448,13 +544,13 @@ footer { align-items: center; margin-bottom: 4px; justify-content: space-around; - background-color: #121213; - box-shadow: 0 4px 8px rgba(0,0,0,0.1); + background-color: var(--color-background-secondary); + box-shadow: 0 4px 8px var(--color-background-secondary); } .loader label { - width: 50%; - color: #cba3e5; + width: 50%; + color: var(--color-primary-text); font-size: 15px; font-weight: bold; } @@ -462,370 +558,679 @@ footer { select { width: 100%; padding: 8px; - background-color: #121213; - color: #cba3e5; + background-color: var(--color-background-secondary); + color: var(--color-primary-text); border: none; } -select::-webkit-scrollbar { - width: 12px; - background-color: #121213; +select:focus { + border-color: var(--color-accent); } -select::-webkit-scrollbar-thumb { - background-color: #cba3e5; - border: 3px solid #121213; +select option:hover { + background-color: var(--color-primary-text); + color: var(--color-background-secondary); } -select::-webkit-scrollbar-track { - box-shadow: inset 0 0 5px grey; -} -select:focus { - border-color: #ff85c0; +#display-media-main { + display: flex; + flex-direction: row; + justify-content: flex-start; + align-items: stretch; + width: 100%; + height: 550px; + overflow: hidden; } -select option:hover { - background-color: #cba3e5; - color: #121213; +#load-image-container { + display: flex; + flex-direction: column; + justify-content: flex-start; + gap: 10px; + width: 300px; + height: 100%; + overflow-y: auto; } -select option:checked, -select option:hover { - background-color: #9b4db4; - color: white; +.image-loader { + flex: 0 1 auto; + width: 100%; + min-height: 250px; + max-height: 50%; + position: relative; + border-radius: 5px; + border: 2px dashed var(--color-border); + /* background: var(--color-background-secondary); */ } -input[type='number']::-webkit-inner-spin-button, -input[type='number']::-webkit-outer-spin-button { - -webkit-appearance: none; - margin: 0; -} +#load-image-container img { + max-width: 100%; + max-height: 100%; + width: auto; + height: auto; + position: absolute; + object-fit: contain; + border-radius: 5px; + padding: 5px; + object-fit: cover; -input[type='number'] { - -moz-appearance: textfield; } -button.active { - background-color: #8621b7; +#image-container { + flex: 1; + height: 100%; + overflow: hidden; + position: relative; + align-items: center; + margin: 4px; } -.dimension-selector-container { - display: flex; - flex-direction: column; - background-color: var(--background-color); - color: var(--text-color); +#image-container img, +#image-container video { width: 100%; - margin-bottom: 4px; - padding: 6px 0; + height: 100%; + position: relative; + object-fit: contain; + border: 2px dashed var(--color-border); + border-radius: 5px; } -#dimension-selector { - display: flex; +#batch-images-container { width: 100%; - border-bottom: 2px solid var(--border-color); + height: 100px; + border: 1px solid var(--color-primary); + position: relative; } -.dimension-stepper { +#batch-images { + padding: 10px; + background-color: var(--color-background-secondary); + border: 1px solid var(--color-border); + border-radius: 5px; + height: 100%; + scrollbar-width: thin; + scrollbar-color: var(--color-primary) var(--color-scrollbar-track); display: flex; - flex-direction: column; - width: 100%; + flex-direction: row; } -.dimension-stepper label { - margin-bottom: 5px; +#history h2 { + color: var(--color-primary-text); text-align: center; - font-size: 0.9em; + margin-bottom: 10px; + font-size: 1.2em; } -.stepper { - display: flex; +.history-thumbnail { + border: 1px dashed var(--color-border); + /* border-radius: 5px; */ + overflow: hidden; + cursor: pointer; + position: relative; } -.stepper__button { - background-color: var(--background-color); - color: var(--text-color); - padding: 5px 10px; +.history-thumbnail img { + width: 100%; + height: 100%; + object-fit: cover; + transition: transform 0.3s; +} + +#history, +#side-workflow-controls, +#load-image-container, +#custom-theme-modal, +.aspect-ratio-selector, +.content { + scrollbar-width: thin; + scrollbar-color: var(--color-scrollbar-thumb) var(--color-scrollbar-track); + overflow-y: auto; +} + +.custom-scrollbar::-webkit-scrollbar { + width: 2px; +} + +.custom-scrollbar::-webkit-scrollbar-track { + background: var(--color-scrollbar-track); + border-radius: 2px; +} + +.custom-scrollbar::-webkit-scrollbar-thumb { + background-color: var(--color-scrollbar-thumb); + border-radius: 2px; + border: 2px solid var(--color-scrollbar-track); +} + +.custom-scrollbar::-webkit-scrollbar-thumb:hover { + background-color: var(--color-accent); +} + +.images-thumbnail { + border: 2px solid var(--color-border); + /* border-radius: 5px; */ + overflow: hidden; cursor: pointer; - transition: background-color 0.3s, box-shadow 0.3s; + position: relative; +} + +.images-thumbnail img { width: 100%; - border: none; + height: 100%; + object-fit: cover; + transition: transform 0.3s; } -.stepper__input { - border: 1px solid var(--background-color); - background-color: var(--background-color); - color: var(--text-color); - padding: 5px; - width: 80px; - text-align: center; - transition: border-color 0.3s ease-in-out; +@media (max-width: 768px) { + .content { + flex-direction: column; + } } -.swap-btn { - background-color: var(--background-color); +#theme-selector { + display: flex; + justify-content: center; + align-items: center; + border: none; + scrollbar-width: thin; + scrollbar-color: #570d7b #1e1e1f; + overflow-y: auto; + +} + +#theme-selector-dropdown { + appearance: none; + -webkit-appearance: none; + -moz-appearance: none; + background-color: var(--color-header-background); + color: var(--color-header-text); border: none; - color: var(--text-color); - padding: 5px 10px; cursor: pointer; - transition: background-color 0.3s, box-shadow 0.3s; + background-repeat: no-repeat; + background-position: right 1rem center; + background-size: 12px 7px; + transition: background-color 0.3s ease, color 0.3s ease; + padding-right: 5rem; } -.aspect-ratio-selector__select { - border: 1px solid var(--background-color); - background-color: var(--background-color); - color: var(--text-color); - padding: 5px; - width: 100%; +#theme-selector-dropdown optgroup { + color: var(--color-header-text); + + font-weight: bold; + /* font-style: italic; */ + /* background-color: #1e1e1f; */ + } -.stepper__button:hover, .swap-btn:hover { - background-color: var(--hover-background-color); - box-shadow: var(--hover-box-shadow); +#theme-selector-dropdown:focus { + outline: none; + } -input[type="number"]::-webkit-inner-spin-button, -input[type="number"]::-webkit-outer-spin-button { - -webkit-appearance: none; + +#theme-selector-dropdown:hover { + border-color: var(--color-accent); } -input[type="number"] { - -moz-appearance: textfield; +#theme-selector-dropdown option { + color: var(--color-primary-text); + + font-style: none; + background-color: var(--color-background); + } -.dropdown-stepper-container, .multi-stepper-container { - background-color: #121213; - margin-bottom: 6px; - padding: 10px; +#theme-selector-dropdown option:disabled { + color: var(--color-primary-text); + background-color: var(--color-background); + font-weight: bold; + font-size: 14px; + + } -input:hover, input:focus, select:hover, select:focus { + +#theme-option-create-custom { + color: var(--color-primary-text) !important; + font-weight:bolder !important; + font-size: 15px; + background-color: var(--color-background-secondary) !important; + } -input:focus, select:focus { - outline: none; - border: 1px solid var(--focus-outline-color); +.custom-theme-modal { + display: none; + position: fixed; + z-index: 1000; + left: 40%; + top: 8%; + max-height: 80%; + background-color: #1e1e1f !important; + padding: 0; + + color: #bababa; + border: 1px dashed #320848; + } -*:focus { - outline: none; +.custom-theme-modal button { + background-color: #1e1e1f; + color: #bababa ; + font-size: 14px; + font-weight: normal; + } -#display-media-main { - display: flex; - flex-direction: row; - justify-content: flex-start; - align-items: stretch; - width: 100%; - height: 550px; - overflow: hidden; + +.custom-theme-modal button:hover{ + background-color: #7d0ab6; + /* color: #1e1e1f; */ } -#load-image-container { - display: flex; - flex-direction: column; - justify-content: flex-start; - gap: 10px; - width: 300px; - height: 100%; - overflow-y: auto; +.modal-content { + background-color: #fff; + /* width: 98%; */ + /* max-width: 900px; */ + overflow: hidden; + display: flex; + flex-direction: column; + background-color: #1e1e1f !important; + } -.image-loader { - flex: 0 1 auto; - width: 100%; - min-height: 250px; - max-height: 50%; - position: relative; - border-radius: 5px; - border: 2px dashed #570d7b; - background: #121213; +.modal-header { + padding: 16px 24px; + background-color: #121212; + display: flex; + justify-content: space-between; + align-items: center; + width: 100%; } -#load-image-container img { - max-width: 100%; - max-height: 100%; - width: auto; - height: auto; - position: absolute; - object-fit: contain; - border-radius: 5px; - padding: 5px - /* border: 2px dashed #570d7b; */ +.modal-header h2 { + margin: 0; + font-size: 1.5rem; } -#image-container { - flex: 1; - height: 100%; - overflow: hidden; - position: relative; - /* border: 1px dashed #570d7b; */ - align-items: center; +.close-button { + background: none; + border: none; + font-size: 1.5rem; + cursor: pointer; +} - margin: 4px; +.modal-body { + /* padding: 24px; */ + display: flex; + flex-direction: column; + padding: 10px; } -#image-container img , #image-container video { - width: 100%; - height: 100%; - position: relative; - object-fit: contain; - border: 2px dashed #570d7b; - border-radius: 5px; - /* margin: 10px; */ +.notification { + /* margin-bottom: 16px; */ + /* padding: 12px; */ + border-radius: 4px; + /* background-color: #e0f7fa; */ + /* color: #006064; */ +} +.modal-main { + display: flex; + flex-direction: row; + gap: 24px; } -#load-image-container::-webkit-scrollbar { - width: 8px; +.left-panel { + flex: 2; + display: flex; + flex-direction: column; } -#load-image-container::-webkit-scrollbar-track { - background: var(--scrollbar-track-color); - border-radius: 4px; + + .theme-form fieldset { + border: none; + margin-bottom: 16px; } -#load-image-container::-webkit-scrollbar-thumb { - background-color: var(--scrollbar-thumb-color); - border-radius: 4px; - border: 2px solid var(--scrollbar-track-color); + +.theme-form legend { + font-weight: bold; + margin-bottom: 8px; } -#load-image-container::-webkit-scrollbar-thumb:hover { - background-color: var(--hover-background-color); + +.form-group { + display: flex; + flex-direction: column; + margin-bottom: 12px; } -#batch-images-container{ - width: 100%; - height: 100px; - border: 1px solid #9303d6; - position: relative; - /* display: flex; */ - /* flex-direction: row; */ - gap: 10px; - /* overflow: hidden; */ - /* padding: 10px; - background-color: var(--background-color); - border: 1px solid var(--border-color); - border-radius: 5px; - height: 100%; - overflow-y: auto; - scrollbar-width: thin; - scrollbar-color: var(--scrollbar-thumb-color) var(--scrollbar-track-color); */ +.form-group label { + margin-bottom: 4px; + font-size: 0.9rem; } -#batch-images { - padding: 10px; - background-color: var(--background-color); - border: 1px solid var(--border-color); - border-radius: 5px; - height: 100%; - /* overflow-y: auto; */ - scrollbar-width: thin; - scrollbar-color: var(--scrollbar-thumb-color) var(--scrollbar-track-color); +.form-group input[type="text"], +.form-group select { + padding: 8px; + font-size: 1rem; + border: none; + /* border-radius: 4px; */ + color: #bababa !important; + background-color: #131312; + border: none; + border-bottom: 1px solid #7a7a7a; + margin-bottom: 10px; +} + +.form-group input[type="text"]:focus, +.form-group select:focus { + border: none; + border-bottom: 1px solid #acabab; +} + +.new-theme-set { display: flex; - flex-direction: row; + /* flex-direction: row; */ + /* gap: 10px; */ + padding: 10px; + background-color: #131312; + /* border-radius: 5px; */ + /* margin: 10px; */ + color: #bababa !important; } -#history { - padding: 10px; - background-color: var(--background-color); - border: 1px solid var(--border-color); - border-radius: 5px; - height: 100%; - overflow-y: auto; - scrollbar-width: thin; - scrollbar-color: var(--scrollbar-thumb-color) var(--scrollbar-track-color); + + +.variables-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); + gap: 16px; + padding: 8px 0; + height: 200px; + overflow: auto; + margin: 10px; + } -#history::-webkit-scrollbar { - width: 8px; +.color-field { + display: flex; + flex-direction: column; + align-items: center; + background-color: #131312; + padding: 8px; + } -#history::-webkit-scrollbar-track { - background: var(--scrollbar-track-color); - border-radius: 4px; +.color-field label { + margin-bottom: 8px; + font-size: 0.85rem; + text-align: center; } -#history::-webkit-scrollbar-thumb { - background-color: var(--scrollbar-thumb-color); - border-radius: 4px; - border: 2px solid var(--scrollbar-track-color); +.color-field input[type="color"] { + width: 50px; + height: 50px; + padding: 0; + border: none; + background: none; + cursor: pointer; + border-radius: 0px; + transition: transform 0.2s ease; } -#history::-webkit-scrollbar-thumb:hover { - background-color: var(--hover-background-color); +.color-field input[type="color"]:hover { + transform: scale(1.1); } -#side-workflow-controls::-webkit-scrollbar { - width: 8px; +.form-actions { + display: flex; + justify-content: flex-end; + gap: 12px; } -#side-workflow-controls::-webkit-scrollbar-track { - background: var(--scrollbar-track-color); - border-radius: 4px; + +.right-panel { + flex: 1; + border-left: 1px solid #ddd; + padding-left: 16px; } -#side-workflow-controls::-webkit-scrollbar-thumb { - background-color: var(--scrollbar-thumb-color); - border-radius: 4px; - border: 2px solid var(--scrollbar-track-color); +.right-panel h3 { + margin-top: 0; + font-size: 1.2rem; + margin-bottom: 12px; } -#side-workflow-controls::-webkit-scrollbar-thumb:hover { - background-color: var(--hover-background-color); +#custom-themes-listing{ + display: flex; + justify-content: space-between; + flex-direction: row; + + gap: 16px; + width: 900px; + /* height: 200px; */ + overflow: auto; + + } +.form-section.css-variables { + padding: 16px; +} + +.custom-themes-section { + margin-bottom: 24px; + /* height: 250px; */ + overflow: auto; + flex: 4; + /* background-color: #1976d2; */ -#history h2 { - color: var(--text-color); - text-align: center; - margin-bottom: 10px; - font-size: 1.2em; } -/* #images-container { - display: flex; +#custom-themes-input { + padding: 8px; + font-size: 1rem; + /* border: 1px solid #ccc; */ + border-radius: 4px; + /* width: 100px; */ + flex: 1; + +} + +#custom-themes-list-container { + height: 250px; + overflow: auto; + +} + +#custom-themes-list { + list-style: none; + padding: 0; + margin: 0; + font-size: 14px; + +} + +.custom-themes-list li { + padding: 8px 0; + /* border-bottom: 1px solid #f0f0f0; */ + background-color: #222222; + margin: 4px 0; + +} + +@media (max-width: 768px) { + .modal-main { flex-direction: column; - gap: 10px; - overflow: hidden; -} */ + } + + .right-panel { + border-left: none; + padding-left: 0; + margin-top: 24px; + } + + .variables-grid { + grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); + } +} -.images-thumbnail { - /* width: 100%; */ - /* max-width: 400px; */ - /* height: 200px; */ - border: 2px solid var(--border-color); - border-radius: 5px; - overflow: hidden; + +.new-theme-set input[type="text"] { + flex: 1; + color: #bababa !important; + + } + +#custom-theme-modal { + scrollbar-width: thin; + scrollbar-color: #570d7b #1e1e1f; + overflow-y: auto; +} + +#custom-theme-modal::-webkit-scrollbar { + width: 1px; +} + +#custom-theme-modal::-webkit-scrollbar-track { + background: #1e1e1f; + border-radius: 1px ; +} + +#custom-theme-modal::-webkit-scrollbar-thumb { + background-color: #1e1e1f; + border-radius: 1px ; + border: 1px solid 570d7b; +} + +#custom-theme-modal::-webkit-scrollbar-thumb:hover { + background-color: #1e1e1f; +} + +#save-theme-button { + position: absolute; + background-color: #570d7b; + color: #bababa; + padding: 10px; + margin: 0 10px; + border: none; cursor: pointer; - position: relative; + transition: background-color 0.3s ease, color 0.3s ease; + align-self: flex-end; } -.images-thumbnail img { - width: 100%; - height: 100%; - object-fit: cover; - transition: transform 0.3s; +#save-and-continue-theme-button { + position: absolute; + right: 200px; + background-color: #570d7b; + color: #bababa; + padding: 10px; + margin: 0 10px; + border: none; + cursor: pointer; + transition: background-color 0.3s ease, color 0.3s ease; + align-self: flex-end; } -.history-thumbnail { - /* width: 100%; */ - /* max-width: 400px; */ - /* height: 200px; */ - border: 2px solid var(--border-color); - border-radius: 5px; - overflow: hidden; + +#save-theme-set-button { + background-color: #570d7b; + color: #bababa; + padding: 10px; + margin: 0 10px; + border: none; cursor: pointer; - position: relative; + transition: background-color 0.3s ease, color 0.3s ease; } -.history-thumbnail img { +#save-theme-button:hover, #save-and-continue-theme-button:hover ,#save-theme-set-button:hover { + background-color: #8621b7; + color: #bababa; +} + +#theme-name { + flex: 1; + transition: border-color 0.3s ease-in-out; + padding: 8px; + background-color: #131312; + margin: 0; width: 100%; - height: 100%; - object-fit: cover; - transition: transform 0.3s; } -.history-thumbnail:hover img { - /* transform: scale(1.05); */ +#theme-set-select { + border: none; + outline: none; + +} + +@keyframes fadeOut { + from { opacity: 1; } + to { opacity: 0; } +} + +@keyframes fadeIn { + from { opacity: 0; } + to { opacity: 1; } +} +.fade-out { + animation: fadeOut 0.3s forwards; +} + +.fade-in { + animation: fadeIn 0.3s forwards; } +@keyframes fadeSlideDown { + from { + opacity: 0; + transform: translateY(-30px); + } + to { + opacity: 1; + transform: translateY(0); + } +} +body { + transition: transform 0.3s ease-out, opacity 0.3s ease-out; + will-change: transform, opacity; +} +html.css-loading body { + opacity: 0; + transform: translateY(-30px); +} + +html:not(.css-loading) body { + opacity: 1; + transform: translateY(0); + animation: fadeSlideDown 0.3s forwards; +} + +#css-loading-overlay { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: #ffffff; + opacity: 1; + transition: opacity 0.3s ease-out; + will-change: opacity; + z-index: 9999; +} + +#css-loading-overlay.fade-out { + opacity: 0; + pointer-events: none; +} + +@media (prefers-reduced-motion: reduce) { + body { + opacity: 1; + transform: translateY(0); + transition: none; + } + #css-loading-overlay { + opacity: 0; + transition: none; + } +} diff --git a/web/core/css/themes.css b/web/core/css/themes.css new file mode 100644 index 0000000..d1251b8 --- /dev/null +++ b/web/core/css/themes.css @@ -0,0 +1,301 @@ + +[data-theme="flow-dark"] { + --color-primary: #430474; + --color-secondary: #f4b6ff; + --color-accent: #8621b7; + --color-highlight: #7d0ab6; + --color-primary-text: #cba3e5; + --color-border: #570d7b; + --color-background: #181b1d; + --color-background-secondary: #131312; + --color-background-pattern: #4d0f7c; + --color-header-background: #131312; + --color-header-logo-text: #570d7b; + --color-header-text:#570d7b; + --color-social-icons: #570d7b; + --color-button-primary: #570d7b; + --color-button-primary-hover: #8621b7; + --color-button-primary-text: #f4b6ff; + --color-button-primary-text-hover: #131312; + --color-button-primary-active: #8621b7; + --color-button-primary-text-active: #8621b7; + --color-button-secondary: #131312; + --color-button-secondary-hover: #8621b7; + --color-button-secondary-text: #f4b6ff; + --color-button-secondary-text-hover: #131312; + --color-button-secondary-active: #8621b7; + --color-button-secondary-text-active: #f4b6ff; + --color-progress-background: #2c063f; + --color-progress-value:#7d0ab6; + --color-scrollbar-track: #1e1e1f; + --color-scrollbar-thumb: #570d7b; + --color-input-range-thumb: #f4b6ff; + --color-input-range-background: #570d7b; + --color-spinner:#570d7b; + --color-spinner-highlight:#7d0ab6; +} +[data-theme="monochrome-noir"] { + --color-background: #000000; + --color-primary-text: #888888; + --color-background-pattern: #424242; + --color-border: #7a7a7a; + --color-header-background: #131312; + --color-header-text: #888888; + --color-background-secondary: #0d0d0d; + --color-button-primary: #000000; + --color-button-primary-text: #888888; + --color-button-primary-hover: #707070; + --color-button-primary-text-hover: #000000; + --color-button-primary-active: #d9d8d8; + --color-button-primary-text-active: #707070; + --color-button-secondary: #0d0d0d; + --color-button-secondary-text: #888888; + --color-button-secondary-hover: #707070; + --color-button-secondary-text-hover: #000000; + --color-button-secondary-active: #707070; + --color-button-secondary-text-active: #000000; + --color-progress-background: #000000; + --color-progress-value: #908989; + --color-scrollbar-track: #1e1e1f; + --color-scrollbar-thumb: #7a7a7a; + --color-input-range-thumb: #0d0d0d; + --color-input-range-background: #707070; + --color-spinner: #908989; + --color-spinner-highlight: #908989; +} + +[data-theme="monochrome-blanc"] { + --color-background: #ffffff; + --color-primary-text: #5a5a5a; + --color-background-pattern: #e5e5e5; + --color-border: #d0d0d0; + --color-header-background: #f7f7f7; + --color-header-text: #5a5a5a; + --color-background-secondary: #f9f9f9; + --color-button-primary: #c0c0c0; + --color-button-primary-text: #5a5a5a; + --color-button-primary-hover: #5a5a5a; + --color-button-primary-text-hover: #ffffff; + --color-button-primary-active: #f9f9f9; + --color-button-primary-text-active: #c0c0c0; + --color-button-secondary: #f9f9f9; + --color-button-secondary-text: #5a5a5a; + --color-button-secondary-hover: #c0c0c0; + --color-button-secondary-text-hover: #ffffff; + --color-button-secondary-active: #c0c0c0; + --color-button-secondary-text-active: #5a5a5a; + --color-progress-background: #f9f9f9; + --color-progress-value: #b5b5b5; + --color-scrollbar-track: #f2f2f2; + --color-scrollbar-thumb: #c0c0c0; + --color-input-range-thumb: #5a5a5a; + --color-input-range-background: #c0c0c0; + --color-spinner: #b5b5b5; + --color-spinner-highlight: #ffffff; +} +[data-theme="shadow-realm"] { + --color-header-background: #131312; + --color-header-text: #888888; + --color-background: #212121; + --color-background-pattern: #424242; + --color-primary-text: #bababa; + --color-border: #454545; + --color-background-secondary: #171717; + --color-button-primary: #707070; + --color-button-primary-hover: #1f1f1f; + --color-button-primary-text: #171717; + --color-button-primary-text-hover: #707070; + --color-button-primary-active: #000000; + --color-button-primary-text-active: #707070; + --color-button-secondary: #171717; + --color-button-secondary-hover: #707070; + --color-button-secondary-text: #888888; + --color-button-secondary-text-hover: #000000; + --color-button-secondary-active: #707070; + --color-button-secondary-text-active: #000000; + --color-progress-background: #1f1f1f; + --color-progress-value: #707070; + --color-scrollbar-track: #1e1e1f; + --color-scrollbar-thumb: #7a7a7a; + --color-input-range-thumb: #000000; + --color-input-range-background: #707070; + --color-spinner: #908989; + --color-spinner-highlight: #908989; +} +[data-theme="dawn-realm"] { + --color-background: #707070; + --color-primary-text: #131312; + --color-background-pattern: #424242; + --color-border: #454545; + --color-header-background: #707070; + --color-header-text: #131312; + --color-background-secondary: #888888; + --color-button-primary: #707070; + --color-button-primary-text: #171717; + --color-button-primary-hover: #1f1f1f; + --color-button-primary-text-hover: #707070; + --color-button-primary-active: #000000; + --color-button-primary-text-active: #707070; + --color-button-secondary: #888888; + --color-button-secondary-text: #1f1f1f; + --color-button-secondary-hover: #707070; + --color-button-secondary-text-hover: #000000; + --color-button-secondary-active: #707070; + --color-button-secondary-text-active: #000000; + --color-progress-background: #1f1f1f; + --color-progress-value: #707070; + --color-scrollbar-track: #1e1e1f; + --color-scrollbar-thumb: #7a7a7a; + --color-input-range-thumb: #000000; + --color-input-range-background: #707070; + --color-spinner: #171717; + --color-spinner-highlight: #171717; +} +[data-theme="terminal-dark"] { + --color-background: #000000; + --color-primary-text: #00ff00; + --color-background-pattern: #003300; + --color-border: #00ff00; + --color-header-background: #000000; + --color-header-text: #00ff00; + --color-background-secondary: #0a0b0a; + --color-button-primary: #00ff00; + --color-button-primary-text: #000000; + --color-button-primary-hover: #003300; + --color-button-primary-text-hover: #00ff00; + --color-button-primary-active: #003300; + --color-button-primary-text-active: #00ff00; + --color-button-secondary: #000000; + --color-button-secondary-text: #00ff00; + --color-button-secondary-hover: #00ff00; + --color-button-secondary-text-hover: #000000; + --color-button-secondary-active: #00ff00; + --color-button-secondary-text-active: #000000; + --color-progress-background: #001a00; + --color-progress-value: #00ff00; + --color-scrollbar-track: #001a00; + --color-scrollbar-thumb: #00ff00; + --color-input-range-thumb: #00ff00; + --color-input-range-background: #001a00; + --color-spinner: #00ff00; + --color-spinner-highlight: #00ff00; +} +[data-theme="terminal-soft-dark"] { + --color-background: #0a0a0a; + --color-primary-text: #82e082; + --color-background-pattern: #0f1e0f; + --color-border: #369136; + --color-header-background: #0a0a0a; + --color-header-text: #82e082; + --color-background-secondary: #1a1a1a; + --color-button-primary: #4ec94e; + --color-button-primary-text: #0a0a0a; + --color-button-primary-hover: #5aff5a; + --color-button-primary-text-hover: #101010; + --color-button-primary-active: #7aff7a; + --color-button-primary-text-active: #0a0a0a; + --color-button-secondary: #1a1a1a; + --color-button-secondary-text: #82e082; + --color-button-secondary-hover: #4ec94e; + --color-button-secondary-text-hover: #0a0a0a; + --color-button-secondary-active: #4ec94e; + --color-button-secondary-text-active: #1a1a1a; + --color-progress-background: #1a2b1a; + --color-progress-value: #7aff7a; + --color-scrollbar-track: #151515; + --color-scrollbar-thumb: #4ec94e; + --color-input-range-thumb: #1a1a1a; + --color-input-range-background: #4ec94e; + --color-spinner: #1a2b1a; + --color-spinner-highlight: #7aff7a; +} +[data-theme="ink-dark"] { + --color-background: #000000; + --color-primary-text: #ffffff; + --color-background-pattern: #727272; + --color-border: #ffffff; + --color-header-background: #000000; + --color-header-text: #ffffff; + --color-background-secondary: #000000; + --color-button-primary: #ffffff; + --color-button-primary-text: #000000; + --color-button-primary-hover: #000000; + --color-button-primary-text-hover: #ffffff; + --color-button-primary-active: #ffffff; + --color-button-primary-text-active: #000000; + --color-button-secondary: #000000; + --color-button-secondary-text: #ffffff; + --color-button-secondary-hover: #ffffff; + --color-button-secondary-text-hover: #000000; + --color-button-secondary-active: #ffffff; + --color-button-secondary-text-active: #000000; + --color-progress-background: #0f0f0f; + --color-progress-value: #ffffff; + --color-scrollbar-track: #000000; + --color-scrollbar-thumb: #ffffff; + --color-input-range-thumb: #141414; + --color-input-range-background: #ffffff; + --color-spinner: #0f0f0f; + --color-spinner-highlight: #0f0f0f; +} +[data-theme="paper-light"] { + --color-background: #ffffff; + --color-primary-text: #0d0d0d; + --color-background-pattern: #808080; + --color-border: #d0d0d0; + --color-header-background: #ffffff; + --color-header-text: #5a5a5a; + --color-background-secondary: #ffffff; + --color-button-primary: #171717; + --color-button-primary-text: #f9f9f9; + --color-button-primary-hover: #dbdbdb; + --color-button-primary-text-hover: #5a5a5a; + --color-button-primary-active: #fcfcfc; + --color-button-primary-text-active: #fcfcfc; + --color-button-secondary: #ffffff; + --color-button-secondary-text: #5a5a5a; + --color-button-secondary-hover: #121212; + --color-button-secondary-text-hover: #ffffff; + --color-button-secondary-active: #121212; + --color-button-secondary-text-active: #ffffff; + --color-progress-background: #f9f9f9; + --color-progress-value: #b5b5b5; + --color-scrollbar-track: #f2f2f2; + --color-scrollbar-thumb: #c0c0c0; + --color-input-range-thumb: #ffffff; + --color-input-range-background: #121212; + --color-spinner: #121212; + --color-spinner-highlight: #121212; +} + +[data-theme="glimpse-of-life"] { + --color-primary-text: #bababa; + --color-border: #454545; + --color-background: #212121; + --color-background-secondary: #171717; + --color-background-pattern: #424242; + --color-header-background: #131312; + --color-header-logo-text: #888888; + --color-header-text: #888888; + --color-social-icons: #888888; + --color-button-primary: #457434; + --color-button-primary-hover: #1f1f1f; + --color-button-primary-text: #171717; + --color-button-primary-text-hover: #707070; + --color-button-primary-active: #000000; + --color-button-primary-text-active: #707070; + --color-button-secondary: #171717; + --color-button-secondary-hover: #707070; + --color-button-secondary-text: #888888; + --color-button-secondary-text-hover: #000000; + --color-button-secondary-active: #707070; + --color-button-secondary-text-active: #000000; + --color-progress-background: #1f1f1f; + --color-progress-value: #707070; + --color-scrollbar-track: #1e1e1f; + --color-scrollbar-thumb: #7a7a7a; + --color-input-range-thumb: #000000; + --color-input-range-background: #457434; + --color-spinner: #908989; + --color-spinner-highlight: #908989; +} \ No newline at end of file diff --git a/web/core/js/common/components/ImageLoader.js b/web/core/js/common/components/ImageLoader.js index 90dfd17..4d32e94 100644 --- a/web/core/js/common/components/ImageLoader.js +++ b/web/core/js/common/components/ImageLoader.js @@ -3,7 +3,7 @@ import { showSpinner, hideSpinner } from './utils.js'; export default class ImageLoader { static DEFAULT_CONFIG = { allowedFileType: 'video', - defaultImageSrc: '../core/media/ui/drop_image_rect_no_border.png', + defaultImageSrc: '../core/media/ui/drop_image_rect_no_border_trans.png', showIndicator: false, }; diff --git a/web/core/js/common/components/header.js b/web/core/js/common/components/header.js index 43bf776..5a2a5d5 100644 --- a/web/core/js/common/components/header.js +++ b/web/core/js/common/components/header.js @@ -15,12 +15,14 @@ const headerHTML = `

${appName}s

+
+
Support in keeping the flow.
- + @@ -28,13 +30,13 @@ const headerHTML = `
- + diff --git a/web/core/js/common/components/imageLoaderComp.js b/web/core/js/common/components/imageLoaderComp.js index ab0bf82..32b84db 100644 --- a/web/core/js/common/components/imageLoaderComp.js +++ b/web/core/js/common/components/imageLoaderComp.js @@ -35,7 +35,7 @@ export default function imageLoaderComp(flowConfig, workflow) { const containerId = loadImageContainer.id; const imageLoader = new ImageLoader(containerId, { allowedFileType: 'image', - defaultImageSrc: '../core/media/ui/drop_image_rect_no_border.png', + defaultImageSrc: '../core/media/ui/drop_image_rect_no_border_trans.png', showIndicator: true, }, (localSrc, serverResult) => { console.log(`Image ${index + 1} loaded:`, serverResult); diff --git a/web/core/js/common/scripts/ThemeManager.js b/web/core/js/common/scripts/ThemeManager.js new file mode 100644 index 0000000..c0a5623 --- /dev/null +++ b/web/core/js/common/scripts/ThemeManager.js @@ -0,0 +1,1157 @@ + +class ThemeManager { + constructor(preferencesManager, cssPath = '/core/css/themes.css') { + this.preferencesManager = preferencesManager; + this.themes = []; + this.customThemes = []; + this.externalCustomThemes = []; + this.targetElementId = 'theme-selector'; + this.cssPath = cssPath; + this.customThemeModalId = 'custom-theme-modal'; + this.isDragging = false; + this.dragOffset = { x: 0, y: 0 }; + this.animationFrame = null; + this.currentEditingThemeId = null; + this.themeCache = new Map(); + this.themeSets = []; + this.customLabels = { + '--color-background': 'Background', + '--color-primary-text': 'Text', + '--color-background-pattern': 'Background Pattern', + '--color-border': 'Border', + '--color-header-background': 'Header Background', + '--color-header-text': 'Header Text', + '--color-background-secondary': 'Widget Background', + '--color-button-primary': 'Button', + '--color-button-primary-text': 'Button Text', + '--color-button-primary-hover': 'Button Hover', + '--color-button-primary-text-hover': 'Button Text Hover', + '--color-button-primary-active': 'Button Active', + '--color-button-primary-text-active': 'Button Text Active', + '--color-button-secondary': 'Widget Button', + '--color-button-secondary-text': 'Widget Button Text', + '--color-button-secondary-hover': 'Widget Button Hover', + '--color-button-secondary-text-hover': 'Widget Button Text Hover', + '--color-button-secondary-active': 'Widget Button Active', + '--color-button-secondary-text-active': 'Widget Button Text Active', + '--color-progress-background': 'Progress Background', + '--color-progress-value': 'Progress Value', + '--color-scrollbar-track': 'Scrollbar Track', + '--color-scrollbar-thumb': 'Scrollbar Thumb', + '--color-input-range-thumb': 'Widget Range Thumb', + '--color-input-range-background': 'Widget Range Background', + '--color-spinner': 'Spinner', + '--color-spinner-highlight': 'Spinner Highlight', + }; + this.cssVariables = Object.keys(this.customLabels); + this.onMouseMove = this.onMouseMove.bind(this); + this.onMouseUp = this.onMouseUp.bind(this); + } + + static applyInitialTheme(preferencesManager) { + let themeToApply = preferencesManager.get('selectedTheme'); + if (!themeToApply) { + const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches; + themeToApply = prefersDark ? 'flow-dark' : 'flow-light'; + } + document.documentElement.setAttribute('data-theme', themeToApply); + } + + async init() { + try { + document.documentElement.classList.add('css-loading'); + this.loadCustomThemesFromStorage(); + this.applySavedTheme(); + await this.loadThemesFromCSS(); + await this.loadExternalCustomThemes(); + this.loadThemeSetsFromStorage(); + this.addMenu(); + this.setupCustomThemeModal(); + } catch (error) { + console.error('Failed to initialize ThemeManager:', error); + } finally { + document.documentElement.classList.remove('css-loading'); + + const overlay = document.getElementById('css-loading-overlay'); + if (overlay) { + overlay.addEventListener('transitionend', () => { + if (overlay.parentNode) { + overlay.parentNode.removeChild(overlay); + } + }); + setTimeout(() => { + if (overlay.parentNode) { + overlay.parentNode.removeChild(overlay); + } + }, 50); + } + }} + + getTheme(themeValue) { + if (!this.themeCache.has(themeValue)) { + const theme = this.themes.find(t => t.value === themeValue) || + this.customThemes.find(t => t.value === themeValue) || + this.externalCustomThemes.flatMap(s => s.themes).find(t => t.value === themeValue); + if (theme) { + this.themeCache.set(themeValue, theme); + } + } + return this.themeCache.get(themeValue); + } + + async loadThemesFromCSS() { + try { + const response = await fetch(this.cssPath); + const cssText = await response.text(); + this.parseThemesFromCSS(cssText); + } catch (error) { + console.error('Failed to load themes:', error); + } + } + + parseThemesFromCSS(cssText) { + const themeRegex = /\[data-theme="([^"]+)"\]\s*\{([^}]+)\}/g; + let match; + + while ((match = themeRegex.exec(cssText)) !== null) { + const themeValue = match[1]; + const themeName = this.formatThemeName(themeValue); + const variablesBlock = match[2]; + const variables = this.parseCSSVariables(variablesBlock); + + if (!this.themes.some(theme => theme.value === themeValue)) { + this.themes.push({ name: themeName, value: themeValue, variables: variables }); + } + } + + if (this.themes.length === 0) { + console.warn('No themes found in the CSS file.'); + } else { + console.info(`Loaded ${this.themes.length} themes from CSS.`); + } + + if (!this.themes.some(t => t.value === 'create-custom')) { + this.themes.push({ name: 'Create Custom Theme', value: 'create-custom', variables: {} }); + } + } + + parseCSSVariables(cssBlock) { + const variableRegex = /(--[\w-]+)\s*:\s*([^;]+);/g; + let match; + const variables = {}; + + while ((match = variableRegex.exec(cssBlock)) !== null) { + const variable = match[1]; + const value = match[2].trim(); + variables[variable] = value; + } + + return variables; + } + + async loadExternalCustomThemes() { + try { + const response = await fetch('/core/css/themes/list'); + if (!response.ok) { + throw new Error('Failed to fetch the list of custom theme CSS files.'); + } + const cssFiles = await response.json(); + + for (const fileName of cssFiles) { + if (!fileName.endsWith('.css')) continue; + const styleName = this.formatStyleName(fileName); + + try { + const cssResponse = await fetch(`/core/css/themes/${fileName}`); + if (!cssResponse.ok) { + console.warn(`Failed to load CSS file: ${fileName}`); + continue; + } + const cssText = await cssResponse.text(); + + const themes = this.extractThemesFromCSS(cssText).map(theme => ({ + ...theme, + themesSetName: styleName, + })); + if (themes.length === 0) { + console.warn(`No valid themes found in CSS file: ${fileName}`); + continue; + } + + this.externalCustomThemes.push({ + styleName: styleName, + themes: themes + }); + + this.appendExternalCSS(fileName, cssText); + } catch (error) { + console.error(`Error loading custom theme from file ${fileName}:`, error); + } + } + + if (this.externalCustomThemes.length === 0) { + console.info('No external custom themes loaded.'); + } else { + console.info(`Loaded ${this.externalCustomThemes.length} external custom theme styles.`); + } + } catch (error) { + console.error('Error loading external custom themes:', error); + } + } + + extractThemesFromCSS(cssText) { + const themeRegex = /\[data-theme="([^"]+)"\]\s*\{([^}]+)\}/g; + let match; + const themes = []; + + while ((match = themeRegex.exec(cssText)) !== null) { + const themeValue = match[1]; + const themeName = this.formatThemeName(themeValue); + const variablesBlock = match[2]; + const variables = this.parseCSSVariables(variablesBlock); + + if (!themes.some(theme => theme.value === themeValue)) { + themes.push({ name: themeName, value: themeValue, variables: variables }); + } + } + + return themes; + } + + formatStyleName(fileName) { + const nameWithoutExt = fileName.replace('.css', ''); + const nameWithSpaces = nameWithoutExt.replace(/[_-]+/g, ' '); + return nameWithSpaces + .split(' ') + .map(word => word.charAt(0).toUpperCase() + word.slice(1)) + .join(' '); + } + + formatThemeName(themeValue) { + if (this.customLabels[themeValue]) { + return this.customLabels[themeValue]; + } + return themeValue + .split('-') + .map(word => word.charAt(0).toUpperCase() + word.slice(1)) + .join(' '); + } + + applySavedTheme() { + const savedTheme = this.preferencesManager.get('selectedTheme'); + if (savedTheme) { + document.documentElement.setAttribute('data-theme', savedTheme); + } + } + + applyTheme(theme) { + document.documentElement.setAttribute('data-theme', theme); + this.preferencesManager.set('selectedTheme', theme); + } + + addMenu() { + const targetElement = document.getElementById(this.targetElementId); + console.debug(`Attempting to add/populate theme selector in element: ${this.targetElementId}`); + + if (!targetElement) { + console.error(`Element with ID "${this.targetElementId}" not found. Theme selector will not be injected.`); + return; + } + + let selector = targetElement.querySelector('#theme-selector-dropdown'); + + if (!selector) { + selector = document.createElement('select'); + selector.id = 'theme-selector-dropdown'; + selector.style.cursor = 'pointer'; + selector.setAttribute('aria-label', 'Select Theme'); + + targetElement.appendChild(selector); + console.info('Theme selector dropdown created and injected.'); + } else { + console.info('Theme selector dropdown already exists. Populating with themes.'); + selector.innerHTML = ''; + } + + const defaultOption = document.createElement('option'); + defaultOption.value = ''; + defaultOption.textContent = 'Set Theme'; + defaultOption.disabled = true; + defaultOption.selected = true; + selector.appendChild(defaultOption); + + this.themes.forEach(theme => { + if (theme.value !== 'create-custom') { + const option = document.createElement('option'); + option.value = theme.value; + option.textContent = theme.name; + selector.appendChild(option); + } + }); + + const createCustomOption = document.createElement('option'); + createCustomOption.value = 'create-custom'; + createCustomOption.textContent = 'Create Custom Theme'; + createCustomOption.id = `theme-option-create-custom`; + + selector.appendChild(createCustomOption); + + if (this.externalCustomThemes.length + this.customThemes.length > 0) { + const customThemesHeader = document.createElement('option'); + customThemesHeader.textContent = 'Custom Themes'; + customThemesHeader.disabled = true; + selector.appendChild(customThemesHeader); + + const allCustomThemes = [ + ...this.externalCustomThemes.flatMap(style => style.themes), + ...this.customThemes + ]; + + const groupedThemes = this.groupThemesBySet(allCustomThemes); + + for (const [setName, themes] of groupedThemes) { + const optgroup = document.createElement('optgroup'); + optgroup.label = setName; + + themes.forEach(theme => { + const option = document.createElement('option'); + option.value = theme.value; + option.textContent = theme.name; + optgroup.appendChild(option); + }); + + selector.appendChild(optgroup); + } + } + + const currentTheme = this.preferencesManager.get('selectedTheme') || this.getDefaultTheme(); + selector.value = currentTheme; + + selector.removeEventListener('change', this.handleThemeChange); + selector.addEventListener('change', this.handleThemeChange.bind(this)); + + console.info('Theme selector dropdown populated with themes.'); + } + + groupThemesBySet(themes) { + const map = new Map(); + themes.forEach(theme => { + const setName = theme.themesSetName || 'Default Set'; + if (!map.has(setName)) { + map.set(setName, []); + } + map.get(setName).push(theme); + }); + return map; + } + + getDefaultTheme() { + const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches; + const preferredTheme = this.themes.find(theme => + theme.value.toLowerCase().includes(prefersDark ? 'dark' : 'light') + ); + return preferredTheme ? preferredTheme.value : this.themes[0]?.value || 'flow-dark'; + } + + handleThemeChange(e) { + const selectedTheme = e.target.value; + if (selectedTheme === 'create-custom') { + this.openCustomThemeModal(); + } else { + this.applyTheme(selectedTheme); + } + } + + setupCustomThemeModal() { + if (document.getElementById(this.customThemeModalId)) { + console.warn('Custom Theme Modal already exists.'); + return; + } + + const modalHTML = ` + + `; + + document.body.insertAdjacentHTML('beforeend', modalHTML); + const style = document.createElement('style'); + style.textContent = ` + CSS + `; + + // document.head.appendChild(style); + this.attachModalEventListeners(); + } + + attachModalEventListeners() { + const modal = document.getElementById(this.customThemeModalId); + if (!modal) return; + + const closeButton = modal.querySelector('.close-button'); + if (closeButton) { + closeButton.addEventListener('click', this.closeCustomThemeModal.bind(this)); + } + + const modalHeader = modal.querySelector('.modal-header'); + if (modalHeader) { + modalHeader.addEventListener('mousedown', this.onMouseDown.bind(this)); + } + + const saveThemeSetButton = modal.querySelector('#save-theme-set-button'); + if (saveThemeSetButton) { + saveThemeSetButton.addEventListener('click', () => this.addThemeSet()); + } + + const form = modal.querySelector('#custom-theme-form'); + if (form) { + form.addEventListener('submit', (event) => { + event.preventDefault(); + this.saveCustomTheme(true); + }); + + const saveAndContinueButton = modal.querySelector('#save-and-continue-theme-button'); + if (saveAndContinueButton) { + saveAndContinueButton.addEventListener('click', () => this.saveCustomTheme(false)); + } + } + + this.cssVariables.forEach(variable => { + const colorInput = modal.querySelector(`#${this.escapeHTML(variable)}`); + if (colorInput) { + colorInput.addEventListener('input', (e) => { + const colorValue = e.target.value; + this.applyInlineCustomCSS(variable, colorValue); + }); + } + }); + + this.populateCustomThemesList(); + } + + escapeHTML(str) { + return str.replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """) + .replace(/'/g, "'"); + } + + addThemeSet() { + const input = document.getElementById('new-theme-set-name'); + if (!input) { + this.displayNotification('Theme Set input field not found.', 'error'); + return; + } + + const setName = input.value.trim(); + if (!setName) { + this.displayNotification('Theme Set name cannot be empty.', 'error'); + return; + } + + if (this.themeSets.includes(setName)) { + this.displayNotification('A Theme Set with this name already exists.', 'error'); + return; + } + + this.themeSets.push(setName); + this.saveThemeSetsToStorage(); + this.updateThemeSetDropdown(); + this.updateCustomThemesListUI(); + input.value = ''; + this.displayNotification(`Theme Set "${setName}" has been added.`, 'success'); + } + + deleteThemeSet(setName) { + const confirmDelete = confirm(`Are you sure you want to delete the Theme Set "${setName}" and all its themes?`); + if (!confirmDelete) return; + + this.themeSets = this.themeSets.filter(set => set !== setName); + this.saveThemeSetsToStorage(); + + const themesToRemove = this.customThemes.filter(theme => theme.themesSetName === setName); + themesToRemove.forEach(theme => this.deleteCustomTheme(theme.id, false)); // false to prevent confirmation + + const externalThemesToRemove = this.externalCustomThemes.filter(style => style.styleName === setName); + externalThemesToRemove.forEach(style => { + const styleElement = document.getElementById(`external-${style.styleName}.css`); + if (styleElement) { + styleElement.parentNode.removeChild(styleElement); + } + this.externalCustomThemes = this.externalCustomThemes.filter(s => s.styleName !== setName); + }); + + this.addMenu(); + this.populateCustomThemesList(); + + this.displayNotification(`Theme Set "${setName}" and all its themes have been deleted.`, 'success'); + } + + updateThemeSetDropdown() { + const select = document.getElementById('theme-set-select'); + if (!select) return; + + select.innerHTML = ''; + + if (this.themeSets.length === 0) { + const noSetOption = document.createElement('option'); + noSetOption.value = ''; + noSetOption.textContent = 'No Theme Sets Available'; + noSetOption.disabled = true; + noSetOption.selected = true; + select.appendChild(noSetOption); + select.disabled = true; + return; + } + + this.themeSets.forEach(setName => { + const option = document.createElement('option'); + option.value = setName; + option.textContent = setName; + select.appendChild(option); + }); + + select.disabled = false; + } + + formatLabel(variable) { + return this.customLabels[variable] || this.formatVariableName(variable); + } + + formatVariableName(variable) { + return variable + .replace('--', '') + .replace(/-/g, ' ') + .replace(/\b\w/g, char => char.toUpperCase()); + } + + getCurrentCSSVariable(variable) { + return getComputedStyle(document.documentElement).getPropertyValue(variable).trim() || '#000000'; + } + + applyInlineCustomCSS(variable, value) { + document.documentElement.style.setProperty(variable, value); + } + + openCustomThemeModal() { + const modal = document.getElementById(this.customThemeModalId); + if (modal) { + const currentTheme = this.preferencesManager.get('selectedTheme'); + const activeThemeSet = this.findThemeSetByThemeValue(currentTheme); + const themeSetSelect = modal.querySelector('#theme-set-select'); + if (themeSetSelect && activeThemeSet) { + themeSetSelect.value = activeThemeSet; + } else if (themeSetSelect && this.themeSets.length > 0) { + themeSetSelect.selectedIndex = 0; + } + + modal.style.display = 'block'; + this.resetCustomThemeForm(); + this.populateCustomThemesList(); + } else { + console.error('Custom Theme Modal not found.'); + } + } + + findThemeSetByThemeValue(themeValue) { + for (const theme of this.themes) { + if (theme.value === themeValue) { + return theme.themesSetName || 'Default Set'; + } + } + + for (const style of this.externalCustomThemes) { + if (style.themes.some(t => t.value === themeValue)) { + return style.styleName; + } + } + + const customTheme = this.customThemes.find(t => t.value === themeValue); + if (customTheme) { + return customTheme.themesSetName; + } + + return null; + } + + closeCustomThemeModal() { + const modal = document.getElementById(this.customThemeModalId); + if (modal) { + modal.style.display = 'none'; + const currentTheme = this.preferencesManager.get('selectedTheme') || this.getDefaultTheme(); + document.documentElement.removeAttribute('style'); + this.applyTheme(currentTheme); + const selector = document.getElementById('theme-selector-dropdown'); + if (selector) { + selector.value = currentTheme; + } + this.clearNotification(); + } + } + + resetCustomThemeForm() { + const form = document.getElementById('custom-theme-form'); + if (form) { + form.reset(); + const modal = document.getElementById(this.customThemeModalId); + const themeSetSelect = modal.querySelector('#theme-set-select'); + if (themeSetSelect && this.themeSets.length > 0) { + + const activeTheme = this.preferencesManager.get('selectedTheme'); + const activeSet = this.findThemeSetByThemeValue(activeTheme); + if (activeSet) { + themeSetSelect.value = activeSet; + } + } + this.cssVariables.forEach(variable => { + const input = document.getElementById(variable); + if (input) { + input.value = this.getCurrentCSSVariable(variable) || '#000000'; + this.applyInlineCustomCSS(variable, input.value); + } + }); + this.currentEditingThemeId = null; + const modalTitle = modal.querySelector('h2'); + if (modalTitle) { + modalTitle.textContent = 'Create Custom Theme'; + } + const saveThemeButton = modal.querySelector('#save-theme-button'); + if (saveThemeButton) { + saveThemeButton.textContent = 'Save Theme & Close'; + } + const saveAndContinueButton = modal.querySelector('#save-and-continue-theme-button'); + if (saveAndContinueButton) { + saveAndContinueButton.textContent = 'Save Theme'; + } + this.clearNotification(); + } + } + + populateCustomThemesList() { + const customThemesList = document.getElementById('custom-themes-list'); + if (!customThemesList) return; + + customThemesList.innerHTML = ''; + + const allCustomThemes = [ + ...this.externalCustomThemes.flatMap(style => style.themes), + ...this.customThemes + ]; + + if (allCustomThemes.length === 0) { + const noThemesItem = document.createElement('li'); + noThemesItem.textContent = 'No custom themes created yet.'; + customThemesList.appendChild(noThemesItem); + return; + } + + const groupedThemes = this.groupThemesBySet(allCustomThemes); + + for (const [setName, themes] of groupedThemes) { + const themesSetItem = document.createElement('li'); + themesSetItem.style.display = 'flex'; + themesSetItem.style.justifyContent = 'space-between'; + themesSetItem.style.alignItems = 'center'; + themesSetItem.style.padding = '5px 0'; + themesSetItem.style.marginTop = '10px'; + themesSetItem.style.fontWeight = 'bold'; + + const setNameSpan = document.createElement('span'); + setNameSpan.textContent = setName; + + const buttonsContainer = document.createElement('div'); + + const deleteSetButton = document.createElement('button'); + deleteSetButton.type = 'button'; + deleteSetButton.textContent = 'Delete Set'; + deleteSetButton.style.padding = '4px 8px'; + deleteSetButton.style.cursor = 'pointer'; + deleteSetButton.style.marginRight = '5px'; + deleteSetButton.addEventListener('click', () => this.deleteThemeSet(setName)); + + const downloadSetButton = document.createElement('button'); + downloadSetButton.type = 'button'; + downloadSetButton.textContent = 'Download Set'; + downloadSetButton.style.padding = '4px 8px'; + downloadSetButton.style.cursor = 'pointer'; + downloadSetButton.addEventListener('click', () => this.downloadThemeSet(setName)); + + buttonsContainer.appendChild(downloadSetButton); + buttonsContainer.appendChild(deleteSetButton); + + themesSetItem.appendChild(setNameSpan); + themesSetItem.appendChild(buttonsContainer); + customThemesList.appendChild(themesSetItem); + + themes.forEach(theme => { + const listItem = document.createElement('li'); + listItem.style.display = 'flex'; + listItem.style.justifyContent = 'space-between'; + listItem.style.alignItems = 'center'; + listItem.style.padding = '5px 0'; + listItem.style.marginLeft = '20px'; + + const themeNameSpan = document.createElement('span'); + themeNameSpan.textContent = `- ${theme.name}`; + + const themeButtonsContainer = document.createElement('div'); + + if (this.customThemes.some(t => t.id === theme.id)) { + const editButton = document.createElement('button'); + editButton.type = 'button'; + editButton.textContent = 'Edit'; + editButton.style.marginRight = '10px'; + editButton.style.padding = '4px 8px'; + editButton.style.cursor = 'pointer'; + editButton.addEventListener('click', () => this.editCustomTheme(theme.id)); + themeButtonsContainer.appendChild(editButton); + } + + if (this.customThemes.some(t => t.id === theme.id)) { + const deleteButton = document.createElement('button'); + deleteButton.type = 'button'; + deleteButton.textContent = 'Delete'; + deleteButton.style.padding = '4px 8px'; + deleteButton.style.cursor = 'pointer'; + deleteButton.addEventListener('click', () => this.deleteCustomTheme(theme.id)); + themeButtonsContainer.appendChild(deleteButton); + } + + listItem.appendChild(themeNameSpan); + listItem.appendChild(themeButtonsContainer); + + customThemesList.appendChild(listItem); + }); + } + } + + downloadThemeSet(setName) { + const externalThemes = this.externalCustomThemes + .filter(style => style.styleName === setName) + .flatMap(style => style.themes); + + const customThemes = this.customThemes + .filter(theme => theme.themesSetName === setName); + + const allThemes = [...externalThemes, ...customThemes]; + + if (allThemes.length === 0) { + this.displayNotification(`No themes found in the set "${setName}".`, 'error'); + return; + } + + let cssContent = ''; + allThemes.forEach(theme => { + cssContent += `[data-theme="${theme.value}"] {\n`; + if (this.customThemes.some(t => t.id === theme.id)) { + for (const [key, value] of Object.entries(theme.variables)) { + cssContent += ` ${key}: ${value};\n`; + } + } else { + for (const [key, value] of Object.entries(theme.variables)) { + cssContent += ` ${key}: ${value};\n`; + } + } + cssContent += '}\n\n'; + }); + + const blob = new Blob([cssContent], { type: 'text/css' }); + const fileName = this.generateFileName(setName); + const link = document.createElement('a'); + link.href = URL.createObjectURL(blob); + link.download = fileName; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + URL.revokeObjectURL(link.href); + + this.displayNotification(`Theme Set "${setName}" has been downloaded as "${fileName}".`, 'success'); + } + + generateFileName(setName) { + const fileName = setName.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '') || 'custom-theme'; + return `${fileName}.css`; + } + + editCustomTheme(themeId) { + const theme = this.customThemes.find(t => t.id === themeId); + if (!theme) { + this.displayNotification(`Theme with ID "${themeId}" not found or cannot be edited (external themes).`, 'error'); + return; + } + + const form = document.getElementById('custom-theme-form'); + if (form) { + const modal = document.getElementById(this.customThemeModalId); + const nameInput = modal.querySelector('#theme-name'); + const themeSetSelect = modal.querySelector('#theme-set-select'); + + if (nameInput) { + nameInput.value = theme.name; + } + if (themeSetSelect) { + themeSetSelect.value = theme.themesSetName; + } + this.cssVariables.forEach(variable => { + const input = document.getElementById(variable); + if (input) { + input.value = theme.variables[variable] || '#000000'; + this.applyInlineCustomCSS(variable, input.value); + } + }); + this.currentEditingThemeId = themeId; + const modalTitle = modal.querySelector('h2'); + if (modalTitle) { + modalTitle.textContent = 'Edit Custom Theme'; + } + const saveThemeButton = modal.querySelector('#save-theme-button'); + if (saveThemeButton) { + saveThemeButton.textContent = 'Update Theme & Close'; + } + const saveAndContinueButton = modal.querySelector('#save-and-continue-theme-button'); + if (saveAndContinueButton) { + saveAndContinueButton.textContent = 'Update Theme'; + } + this.displayNotification(`Editing theme "${theme.name}". Make your changes and click "Update Theme".`, 'info'); + } + } + + deleteCustomTheme(themeId, confirmDelete = true) { + const theme = this.customThemes.find(t => t.id === themeId); + if (!theme) { + this.displayNotification(`Theme with ID "${themeId}" not found or cannot be deleted (external themes).`, 'error'); + return; + } + + if (confirmDelete) { + const userConfirmed = confirm(`Are you sure you want to delete the theme "${theme.name}"?`); + if (!userConfirmed) return; + } + + const styleId = `custom-theme-${theme.value}`; + const styleElement = document.getElementById(styleId); + if (styleElement) { + styleElement.parentNode.removeChild(styleElement); + } + + this.customThemes = this.customThemes.filter(t => t.id !== themeId); + + this.saveCustomThemesToStorage(); + + this.addMenu(); + this.populateCustomThemesList(); + + if (confirmDelete) { + this.displayNotification('Theme deleted successfully.', 'success'); + } + } + + saveCustomTheme(closeAfterSave = true) { + const form = document.getElementById('custom-theme-form'); + if (!form) return; + + const formData = new FormData(form); + const themeName = formData.get('themeName')?.trim() || `Theme ${Date.now()}`; + const themesSetName = formData.get('themeSet') || 'Default Set'; + const themeValue = this.generateThemeValue(themeName); + + const themeVariables = {}; + this.cssVariables.forEach(variable => { + themeVariables[variable] = formData.get(variable) || '#000000'; + }); + + if (this.currentEditingThemeId) { + const index = this.customThemes.findIndex(t => t.id === this.currentEditingThemeId); + if (index !== -1) { + this.customThemes[index] = { + ...this.customThemes[index], + name: themeName, + value: themeValue, + themesSetName, + variables: themeVariables + }; + } + } else { + this.customThemes.push({ + id: `custom-${Date.now()}`, + name: themeName, + value: themeValue, + themesSetName, + variables: themeVariables + }); + } + + this.appendCustomCSSToDocument(themeValue, themeVariables); + localStorage.setItem('customThemes', JSON.stringify(this.customThemes)); + this.addMenu(); + this.applyTheme(themeValue); + + if (closeAfterSave) { + this.closeCustomThemeModal(); + } + } + + generateThemeValue(themeName) { + return themeName.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '') || `custom-theme-${Date.now()}`; + } + + loadCustomThemesFromStorage() { + const storedThemes = localStorage.getItem('customThemes'); + if (storedThemes) { + this.customThemes = JSON.parse(storedThemes); + this.customThemes.forEach(theme => { + this.appendCustomCSSToDocument(theme.value, theme.variables); + }); + } + } + + saveCustomThemesToStorage() { + localStorage.setItem('customThemes', JSON.stringify(this.customThemes)); + } + + loadThemeSetsFromStorage() { + const storedSets = localStorage.getItem('themeSets'); + if (storedSets) { + this.themeSets = JSON.parse(storedSets); + } else { + this.themeSets = []; + this.saveThemeSetsToStorage(); + } + } + + saveThemeSetsToStorage() { + localStorage.setItem('themeSets', JSON.stringify(this.themeSets)); + } + + generateCustomCSS(themeValue, variables) { + let css = `[data-theme="${themeValue}"] {\n`; + for (const [key, value] of Object.entries(variables)) { + css += ` ${key}: ${value};\n`; + } + css += '}'; + return css; + } + + appendCustomCSSToDocument(themeValue, variables) { + const styleId = `custom-theme-${themeValue}`; + let styleElement = document.getElementById(styleId); + + if (styleElement) { + styleElement.parentNode.removeChild(styleElement); + } + + styleElement = document.createElement('style'); + styleElement.type = 'text/css'; + styleElement.id = styleId; + let css = `[data-theme="${themeValue}"] {\n`; + for (const [key, value] of Object.entries(variables)) { + css += ` ${key}: ${value};\n`; + } + css += '}'; + styleElement.appendChild(document.createTextNode(css)); + document.head.appendChild(styleElement); + } + + updateCustomCSSInDocument(themeValue, variables) { + this.appendCustomCSSToDocument(themeValue, variables); + } + + appendExternalCSS(fileName, cssText) { + const style = document.createElement('style'); + style.id = `external-${fileName}`; + style.appendChild(document.createTextNode(cssText)); + document.head.appendChild(style); + } + + static applyTheme(theme, preferencesManager) { + if (!theme || !preferencesManager) return; + document.documentElement.setAttribute('data-theme', theme); + preferencesManager.set('selectedTheme', theme); + } + + onMouseDown(e) { + e.preventDefault(); + + const modal = document.getElementById(this.customThemeModalId); + if (!modal) return; + + this.isDragging = true; + + const rect = modal.getBoundingClientRect(); + this.dragOffset.x = e.clientX - rect.left; + this.dragOffset.y = e.clientY - rect.top; + + document.addEventListener('mousemove', this.onMouseMove); + document.addEventListener('mouseup', this.onMouseUp); + } + + onMouseMove(e) { + if (!this.isDragging) return; + + const newLeft = e.clientX - this.dragOffset.x; + const newTop = e.clientY - this.dragOffset.y; + + const modal = document.getElementById(this.customThemeModalId); + if (!modal) return; + + const modalWidth = modal.offsetWidth; + const modalHeight = modal.offsetHeight; + const windowWidth = window.innerWidth; + const windowHeight = window.innerHeight; + + const clampedLeft = Math.max(0, Math.min(newLeft, windowWidth - modalWidth)); + const clampedTop = Math.max(0, Math.min(newTop, windowHeight - modalHeight)); + + if (this.animationFrame) { + cancelAnimationFrame(this.animationFrame); + } + + this.animationFrame = requestAnimationFrame(() => { + modal.style.left = `${clampedLeft}px`; + modal.style.top = `${clampedTop}px`; + modal.style.transform = `translate(0, 0)`; + }); + } + + onMouseUp() { + if (!this.isDragging) return; + this.isDragging = false; + + document.removeEventListener('mousemove', this.onMouseMove); + document.removeEventListener('mouseup', this.onMouseUp); + + if (this.animationFrame) { + cancelAnimationFrame(this.animationFrame); + this.animationFrame = null; + } + } + + displayNotification(message, type) { + const notification = document.getElementById('theme-notification'); + if (!notification) return; + + notification.textContent = message; + notification.style.display = 'block'; + notification.hidden = false; + + switch (type) { + case 'success': + // notification.style.backgroundColor = '#d4edda'; + notification.style.color = '#50B452'; + // notification.style.border = '1px solid #c3e6cb'; + break; + case 'error': + // notification.style.backgroundColor = '#f8d7da'; + notification.style.color = '#D24141'; + // notification.style.border = '1px solid #f5c6cb'; + break; + case 'info': + // notification.style.backgroundColor = '#1e1e1f'; + notification.style.color = '#628BC0'; + // notification.style.border = '1px solid #bee5eb'; + break; + default: + notification.style.backgroundColor = '#1e1e1f'; + notification.style.color = '#bababa'; + } + + setTimeout(() => { + this.clearNotification(); + }, 5000); + } + + + clearNotification() { + const notification = document.getElementById('theme-notification'); + if (!notification) return; + + notification.textContent = ''; + notification.style.display = 'none'; + notification.hidden = true; + } + + updateCustomThemesListUI() { + this.populateCustomThemesList(); + } +} + +export default ThemeManager; diff --git a/web/core/js/common/scripts/injectStylesheet.js b/web/core/js/common/scripts/injectStylesheet.js new file mode 100644 index 0000000..475ec6d --- /dev/null +++ b/web/core/js/common/scripts/injectStylesheet.js @@ -0,0 +1,29 @@ +function injectStylesheet(href, id = null) { + if (id && document.getElementById(id)) { + console.warn(`Stylesheet with id "${id}" is already injected.`); + return; + } + + const link = document.createElement('link'); + link.rel = 'preload'; + link.as = 'style'; + link.href = href; + + if (id) { + link.id = id; + } + + link.onload = function() { + this.onload = null; + this.rel = 'stylesheet'; + console.log(`Stylesheet "${href}" has been loaded and applied.`); + }; + + link.onerror = function() { + console.error(`Failed to load stylesheet "${href}".`); + }; + + document.head.appendChild(link); +} + +export default injectStylesheet; diff --git a/web/core/js/common/scripts/preferences.js b/web/core/js/common/scripts/preferences.js new file mode 100644 index 0000000..220aa0d --- /dev/null +++ b/web/core/js/common/scripts/preferences.js @@ -0,0 +1,46 @@ +const PREFS_KEY = 'FlowMenuPref'; + +class PreferencesManager { + constructor(defaultPrefs) { + this.preferences = { ...defaultPrefs }; + this.loadPreferences(); + } + + loadPreferences() { + const storedPrefs = localStorage.getItem(PREFS_KEY); + if (storedPrefs) { + try { + const parsedPrefs = JSON.parse(storedPrefs); + this.preferences = { ...this.preferences, ...parsedPrefs }; + } catch (e) { + console.error('Error parsing preferences from localStorage:', e); + } + } + } + + savePreferences() { + try { + localStorage.setItem(PREFS_KEY, JSON.stringify(this.preferences)); + } catch (e) { + console.error('Error saving preferences to localStorage:', e); + } + } + + get(prefKey) { + return this.preferences[prefKey]; + } + + set(prefKey, value) { + this.preferences[prefKey] = value; + this.savePreferences(); + } + + addPreference(prefKey, defaultValue) { + if (!(prefKey in this.preferences)) { + this.preferences[prefKey] = defaultValue; + this.savePreferences(); + } + } +} + +export { PreferencesManager, }; diff --git a/web/core/main.js b/web/core/main.js index daf580b..5c62a82 100644 --- a/web/core/main.js +++ b/web/core/main.js @@ -1,3 +1,4 @@ + import Seeder from "./js/common/components/Seeder.js" import Stepper from "./js/common/components/Stepper.js" import MultiStepper from "./js/common/components/MultiStepper.js" @@ -12,6 +13,9 @@ import { processWorkflowNodes } from './js/common/scripts/nodesscanner.js'; import { fetchWorkflow } from './js/common/scripts/fetchWorkflow.js'; import { fetchflowConfig } from './js/common/scripts/fetchflowConfig.js'; import { setFaviconStatus } from './js/common/scripts/favicon.js'; +import { PreferencesManager } from './js/common/scripts/preferences.js'; +import ThemeManager from './js/common/scripts/ThemeManager.js'; +import injectStylesheet from './js/common/scripts/injectStylesheet.js'; import { checkAndShowMissingPackagesDialog } from './js/common/components/missingPackagesDialog.js'; @@ -26,10 +30,28 @@ import { checkAndShowMissingPackagesDialog } from './js/common/components/missin let isProcessing = false; initializeWebSocket(client_id); setFaviconStatus.Default(); - + injectStylesheet('/core/css/main.css', 'main'); + injectStylesheet('/core/css/themes.css', 'themes-stylesheet'); + console.log("flowConfig",flowConfig) console.log("workflow",workflow) + + const defaultPreferences = { + selectedCategories: [], + favoritesFilterActive: false, + hideDescriptions: false, + hideTitles: false, + sortValue: 'nameAsc', + selectedTheme: null + }; + + const preferencesManager = new PreferencesManager(defaultPreferences); + + ThemeManager.applyInitialTheme(preferencesManager); + const themeManager = new ThemeManager(preferencesManager); + themeManager.init(); + function generateWorkflowControls(config) { const container = document.getElementById('side-workflow-controls'); @@ -76,7 +98,6 @@ import { checkAndShowMissingPackagesDialog } from './js/common/components/missin }); } - function generateWorkflowInputs(config, options = { clearInputs: false }) { const promptsContainer = document.getElementById('prompts'); config.workflowInputs.forEach((input, index) => { @@ -141,6 +162,7 @@ import { checkAndShowMissingPackagesDialog } from './js/common/components/missin flowConfig.multiSteppers.forEach(config => { new MultiStepper(config, workflow); }); + flowConfig.dropdownSteppers.forEach(config => { new DropdownStepper(config, workflow); }); @@ -255,6 +277,13 @@ import { checkAndShowMissingPackagesDialog } from './js/common/components/missin interrupt(); }); - - + document.addEventListener('DOMContentLoaded', () => { + const overlay = document.getElementById('css-loading-overlay'); + overlay.classList.add('fade-out'); + + overlay.addEventListener('transitionend', () => { + overlay.style.display = 'none'; + }); + }); + })(window, document, undefined); \ No newline at end of file diff --git a/web/core/media/ui/drop_image_rect_no_border_trans.png b/web/core/media/ui/drop_image_rect_no_border_trans.png new file mode 100644 index 0000000..1b14f04 Binary files /dev/null and b/web/core/media/ui/drop_image_rect_no_border_trans.png differ diff --git a/web/flow/index.html b/web/flow/index.html index 1cf2fa0..a92dbba 100644 --- a/web/flow/index.html +++ b/web/flow/index.html @@ -5,435 +5,472 @@ Flow - + +} + +.mid-col::-webkit-scrollbar { + width: 8px; +} + +.mid-col::-webkit-scrollbar-track { + background: var(--color-scrollbar-track); + border-radius: var(--border-radius); +} + +.mid-col::-webkit-scrollbar-thumb { + background-color: var(--color-primary); + border-radius: var(--border-radius); + border: 2px solid var(--color-scrollbar-track); +} + +.mid-col::-webkit-scrollbar-thumb:hover { + background-color: var(--color-highlight); +} + +@keyframes animation-text { + 0% { opacity: 0; } + 40% { opacity: 0; } + 100% { opacity: 1; } +} + +@keyframes animation-left { + from { left: 30px; } + to { left: -30px; } +} + +@keyframes animation-right { + from { right: 30px; } + to { right: -30px; } +} + +.appName { + padding: 10px; + color: var(--color-header-logo-text); + border-top: 1px dashed var(--color-border); + border-left: 1px dashed var(--color-border); + border-right: 1px dashed var(--color-border); + font-size: 12px; +} + +
@@ -539,6 +607,8 @@ +
+
@@ -571,31 +641,12 @@
- \ No newline at end of file diff --git a/web/flow/js/main.js b/web/flow/js/main.js index 9f23362..be7db9f 100644 --- a/web/flow/js/main.js +++ b/web/flow/js/main.js @@ -1,3 +1,7 @@ +import { PreferencesManager } from '/core/js/common/scripts/preferences.js'; +import ThemeManager from '/core/js/common/scripts/ThemeManager.js'; +import injectStylesheet from '/core/js/common/scripts/injectStylesheet.js'; + let allFlows = []; let categories = []; let selectedCategories = new Set(); @@ -6,8 +10,16 @@ let hideTitles = false; let favorites = new Set(); let favoritesFilterActive = false; const FAVORITES_KEY = 'FlowFavorites'; -const PREFS_KEY = 'FlowMenuPref'; const openInNewTab = false; +const priorityFlowIds = [ + 'flupdate', + 'fltuts', + 'flbsdt2i', + 'flbsdi2i', + 'flbfxdt2i', + 'flbfxdi2i', + 'flbfxst2i', +]; const categoryKeywords = [ 'Base', 'Stable Diffusion', @@ -22,59 +34,20 @@ const categoryKeywords = [ 'Pulid', 'CogVideoX', ]; - -class PreferencesManager { - constructor(defaultPrefs) { - this.preferences = { ...defaultPrefs }; - this.loadPreferences(); - } - - loadPreferences() { - const storedPrefs = localStorage.getItem(PREFS_KEY); - if (storedPrefs) { - try { - const parsedPrefs = JSON.parse(storedPrefs); - this.preferences = { ...this.preferences, ...parsedPrefs }; - } catch (e) { - console.error('Error parsing preferences from localStorage:', e); - } - } - } - - savePreferences() { - try { - localStorage.setItem(PREFS_KEY, JSON.stringify(this.preferences)); - } catch (e) { - console.error('Error saving preferences to localStorage:', e); - } - } - - get(prefKey) { - return this.preferences[prefKey]; - } - - set(prefKey, value) { - this.preferences[prefKey] = value; - this.savePreferences(); - } - - addPreference(prefKey, defaultValue) { - if (!(prefKey in this.preferences)) { - this.preferences[prefKey] = defaultValue; - this.savePreferences(); - } - } -} - const defaultPreferences = { selectedCategories: [], favoritesFilterActive: false, hideDescriptions: false, hideTitles: false, - sortValue: 'nameAsc' + sortValue: 'nameAsc', + selectedTheme: null // Will be set by ThemeManager }; - +// injectStylesheet('/flow/css/main.css', 'main'); +// injectStylesheet('/core/css/themes.css', 'themes-stylesheet'); const preferencesManager = new PreferencesManager(defaultPreferences); +// const themeManager = new ThemeManager(preferencesManager); +// themeManager.init(); +// themeManager.addMenu(); function loadFavorites() { const storedFavorites = localStorage.getItem(FAVORITES_KEY); @@ -101,16 +74,14 @@ function toggleFavorite(flowId, button) { if (favorites.has(flowId)) { favorites.delete(flowId); button.classList.remove('favorited'); - button.innerHTML = ''; + button.innerHTML = ''; } else { favorites.add(flowId); button.classList.add('favorited'); button.innerHTML = ''; } saveFavorites(); - if (preferencesManager.get('favoritesFilterActive')) { - renderFlows(filterCurrentFlows()); - } + animateFlowReorder(); } const createElement = (type, className, textContent = '') => { @@ -140,8 +111,8 @@ function createFlowCard(flow) { } favoriteButton.addEventListener('click', (event) => { - event.preventDefault(); - event.stopPropagation(); + event.preventDefault(); + event.stopPropagation(); toggleFavorite(flow.id, favoriteButton); }); @@ -155,6 +126,9 @@ function createFlowCard(flow) { card.appendChild(favoriteButton); + // Attach a unique identifier to the card for animation purposes + card.dataset.flowId = flow.id; + return card; } @@ -261,14 +235,11 @@ function createToggleButtons() { favoritesToggle.title = favoritesFilterActive ? 'Show All Flows' : 'Show Favorites'; } - function toggleFavoritesFilter(button) { favoritesFilterActive = !favoritesFilterActive; preferencesManager.set('favoritesFilterActive', favoritesFilterActive); button.classList.toggle('active', favoritesFilterActive); - button.title = favoritesFilterActive ? 'Show All Flows' : 'Show Favorites'; - renderFlows(filterCurrentFlows()); } @@ -321,6 +292,7 @@ function filterCurrentFlows() { const searchTerm = document.getElementById('searchInput').value.toLowerCase(); const sortValue = preferencesManager.get('sortValue') || 'nameAsc'; let filteredFlows = filterFlows(allFlows, searchTerm, selectedCategories); + if (favoritesFilterActive) { filteredFlows = filteredFlows.filter(flow => isFavorited(flow.id)); } @@ -338,7 +310,37 @@ function renderFlows(flows) { flowGrid.appendChild(flowCard); } }); - updateFlowCardVisibility(); + updateFlowCardVisibility(); +} + +function animateFlowReorder() { + const flowGrid = document.getElementById('flowGrid'); + const oldPositions = new Map(); + Array.from(flowGrid.children).forEach(card => { + const rect = card.getBoundingClientRect(); + oldPositions.set(card.dataset.flowId, rect); + }); + + renderFlows(filterCurrentFlows()); + + Array.from(flowGrid.children).forEach(card => { + const oldRect = oldPositions.get(card.dataset.flowId); + const newRect = card.getBoundingClientRect(); + + const deltaX = oldRect.left - newRect.left; + const deltaY = oldRect.top - newRect.top; + card.style.transform = `translate(${deltaX}px, ${deltaY}px)`; + card.offsetHeight; + card.style.transition = 'transform 0.5s ease'; + card.style.transform = ''; + }); + + setTimeout(() => { + Array.from(flowGrid.children).forEach(card => { + card.style.transition = ''; + card.style.transform = ''; + }); + }, 500); } export async function showVersion() { @@ -380,7 +382,6 @@ function initializeSearch() { function initializeSorting() { const sortSelect = document.getElementById('sortSelect'); - const savedSortValue = preferencesManager.get('sortValue') || 'nameAsc'; sortSelect.value = savedSortValue; @@ -422,12 +423,46 @@ function sortFlows(flows, sortValue) { break; } - // Ensure the 'flupdate' Flow Card is always first if it exists in the filtered flows - const flupdateIndex = sortedFlows.findIndex(flow => flow.id === 'flupdate'); - if (flupdateIndex > -1) { - const [flupdateFlow] = sortedFlows.splice(flupdateIndex, 1); - sortedFlows.unshift(flupdateFlow); - } + const topPriorityIds = ['flupdate', 'fltuts']; + const topPriorityFlows = []; + const remainingFlows = []; + + sortedFlows.forEach(flow => { + if (topPriorityIds.includes(flow.id)) { + topPriorityFlows.push(flow); + } else { + remainingFlows.push(flow); + } + }); + + const favoriteFlows = []; + const nonFavoriteFlows = []; + + remainingFlows.forEach(flow => { + if (isFavorited(flow.id)) { + favoriteFlows.push(flow); + } else { + nonFavoriteFlows.push(flow); + } + }); + + const otherPriorityIds = priorityFlowIds.filter(id => !topPriorityIds.includes(id)); + const otherPriorityFlows = []; + const restFlows = []; + + nonFavoriteFlows.forEach(flow => { + if (otherPriorityIds.includes(flow.id)) { + otherPriorityFlows.push(flow); + } else { + restFlows.push(flow); + } + }); + + otherPriorityFlows.sort((a, b) => { + return otherPriorityIds.indexOf(a.id) - otherPriorityIds.indexOf(b.id); + }); + + sortedFlows = [...topPriorityFlows, ...favoriteFlows, ...otherPriorityFlows, ...restFlows]; return sortedFlows; } @@ -507,15 +542,20 @@ function initializeFilterMenu() { } export function initializeUI() { - loadFavorites(); + loadFavorites(); initializeMenu(); initializeSearch(); initializeSorting(); initializeFilterMenu(); - createToggleButtons(); + createToggleButtons(); loadFlows(); showVersion(); } const styleElement = document.createElement('style'); +styleElement.textContent = ` +.flow-card { + transition: transform 0.5s ease; +} +`; document.head.appendChild(styleElement);