Compare commits
	
		
			2 Commits
		
	
	
		
			973cd977ba
			...
			2353362c04
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 2353362c04 | |||
| ab6f27524d | 
							
								
								
									
										207
									
								
								app.py
									
									
									
									
									
								
							
							
						
						
									
										207
									
								
								app.py
									
									
									
									
									
								
							| @@ -2,128 +2,146 @@ from flask import Flask, render_template | |||||||
| import re | import re | ||||||
| import docker | import docker | ||||||
| import requests | import requests | ||||||
|  | import yaml | ||||||
|  | import os | ||||||
|  | from waitress import serve | ||||||
|  |  | ||||||
| app = Flask(__name__) | app = Flask(__name__) | ||||||
|  |  | ||||||
| TRAEFIK_API_URL = "https://norman.lan/api/http/routers" | TRAEFIK_API_URL = os.getenv('TRAEFIK_API_URL') | ||||||
| REGEX_PATTERNS = [r".*\.gederico\.dynu\.net"] | # Check if TRAEFIK_API_URL is set | ||||||
| DEFAULT_GROUP_PRIORITY = 100 | if not TRAEFIK_API_URL: | ||||||
| DEFAULT_GROUP = 'Applications' |     raise ValueError("TRAEFIK_API_URL environment variable is required and not set.") | ||||||
| DEFAULT_GROUP_ICON = 'fas fa-box' | USER_COLORS_FILE = os.getenv('USER_COLORS_FILE', False) | ||||||
| DEFAULT_TITLE = 'Traefik Routers' | USER_CONFIG_FILE = os.getenv('USER_CONFIG_FILE', False) | ||||||
| DEFAULT_ICON = 'fas fa-bars' | router_configs, group_configs, global_configs = {}, {}, {} | ||||||
|  |  | ||||||
| client = docker.from_env() | client = docker.from_env() | ||||||
|  |  | ||||||
| def get_routers(containers): | def get_defaults(global_configs): | ||||||
|  |     return { | ||||||
|  |         'REGEX_PATTERNS': global_configs.get('url_regex', [r".*"]), | ||||||
|  |         'DEFAULT_GROUP_PRIORITY': global_configs.get('group_priority', 100), | ||||||
|  |         'DEFAULT_GROUP': global_configs.get('default_group', 'Applications'), | ||||||
|  |         'DEFAULT_GROUP_ICON': global_configs.get('group_icon', 'fas fa-box'), | ||||||
|  |         'DEFAULT_GROUP_COLLAPSED': global_configs.get('group_collapsed', 'false'), | ||||||
|  |         'DEFAULT_TITLE': global_configs.get('title', 'Traefik Routers'), | ||||||
|  |         'DEFAULT_ROUTER_ICON': global_configs.get('router_icon', 'fas fa-bars'), | ||||||
|  |         'DEFAULT_PROTOCOL': global_configs.get('entrypoint', 'http'), | ||||||
|  |         'DEFAULT_TARGET': global_configs.get('target', '_blank'), | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def load_yaml(file_path): | ||||||
|  |     with open(file_path, 'r') as file: | ||||||
|  |         return yaml.safe_load(file) | ||||||
|  |  | ||||||
|  | def merge_colors(default_colors, user_colors): | ||||||
|  |     for mode in default_colors: | ||||||
|  |         if mode in user_colors: | ||||||
|  |             default_colors[mode].update(user_colors[mode]) | ||||||
|  |     return default_colors | ||||||
|  |  | ||||||
|  | def categorize_configs(config): | ||||||
|  |     for key, value in config.items(): | ||||||
|  |         if key == "routers": | ||||||
|  |             router_configs.update(value) | ||||||
|  |         elif key == "groups": | ||||||
|  |             group_configs.update(value) | ||||||
|  |         elif key == "global": | ||||||
|  |             global_configs.update(value) | ||||||
|  |  | ||||||
|  | def fetch_containers(): | ||||||
|  |     containers = client.containers.list(all=True) | ||||||
|  |     for container in containers: | ||||||
|  |         labels = container.attrs.get('Config', {}).get('Labels', {}) | ||||||
|  |         for label, value in labels.items(): | ||||||
|  |             if label.startswith('traefik-frontend.http.routers.'): | ||||||
|  |                 router_name = label.split('.')[3] | ||||||
|  |                 if router_name not in router_configs: | ||||||
|  |                     router_configs[router_name] = {} | ||||||
|  |                 key = label.split('.')[4] | ||||||
|  |                 router_configs[router_name][key] = value | ||||||
|  |             elif label.startswith('traefik-frontend.groups.'): | ||||||
|  |                 group_name = label.split('.')[2] | ||||||
|  |                 if group_name not in group_configs: | ||||||
|  |                     group_configs[group_name] = {} | ||||||
|  |                 key = label.split('.')[3] | ||||||
|  |                 group_configs[group_name][key] = value | ||||||
|  |             elif label.startswith('traefik-frontend.'): | ||||||
|  |                 key = label.split('.')[1] | ||||||
|  |                 global_configs[key] = value | ||||||
|  |  | ||||||
|  | def get_routers(defaults): | ||||||
|     response = requests.get(TRAEFIK_API_URL, verify=False)  # Ignore SSL certificate verification |     response = requests.get(TRAEFIK_API_URL, verify=False)  # Ignore SSL certificate verification | ||||||
|     if response.status_code == 200: |     if response.status_code == 200: | ||||||
|         routers = response.json() |         routers = response.json() | ||||||
|         filtered_routers = filter_routers(routers, containers) |         filtered_routers = filter_routers(routers, defaults) | ||||||
|         return filtered_routers |         return filtered_routers | ||||||
|     return [] |     return [] | ||||||
|  |  | ||||||
| def filter_routers(routers, containers): | def filter_routers(routers, defaults): | ||||||
|     filtered_routers = [] |     filtered_routers = [] | ||||||
|     for router in routers: |     for router in routers: | ||||||
|         for pattern in REGEX_PATTERNS: |         for pattern in defaults["REGEX_PATTERNS"]: | ||||||
|             if re.match(pattern, router['rule'].split('`')[1]): |             if re.match(pattern, router['rule'].split('`')[1]): | ||||||
|                 if not is_router_hidden(router['name'], containers): |                 if not is_router_hidden(router['name']): | ||||||
|                     router['description'] = get_router_description(router['name'], containers) |                     router_name = router['name'].split('@')[0] | ||||||
|                     router['display_name'] = get_router_display_name(router['name'], containers) |                     router['description'] = router_configs.get(router_name, {}).get('description', False) | ||||||
|                     router['icon'] = get_router_icon(router['name'], containers) |                     router['display_name'] = router_configs.get(router_name, {}).get('router_name', router_name).upper() | ||||||
|                     router['group'] = get_router_group(router['name'], containers) |                     router['icon'] = router_configs.get(router_name, {}).get('icon', defaults['DEFAULT_ROUTER_ICON']) | ||||||
|  |                     router['group'] = router_configs.get(router_name, {}).get('group', defaults['DEFAULT_GROUP']) | ||||||
|  |                     router['protocol'] = router_configs.get(router_name, {}).get('entrypoint', defaults['DEFAULT_PROTOCOL']) | ||||||
|  |                     router['target'] = router_configs.get(router_name, {}).get('target', defaults['DEFAULT_TARGET']) | ||||||
|                     filtered_routers.append(router) |                     filtered_routers.append(router) | ||||||
|                 break |                 break | ||||||
|     return filtered_routers |     return filtered_routers | ||||||
|  |  | ||||||
| def is_router_hidden(router_name, containers): | def is_router_hidden(router_name): | ||||||
|     service_name = router_name.split('@')[0] |     service_name = router_name.split('@')[0] | ||||||
|     for container in containers: |     return router_configs.get(service_name, {}).get('hidden') == 'true' | ||||||
|         labels = container.attrs.get('Config', {}).get('Labels', {}) |  | ||||||
|         hidden_label = f'traefik-frontend.http.routers.{service_name}.hidden' |  | ||||||
|         if labels.get(hidden_label) == 'true': |  | ||||||
|             return True |  | ||||||
|     return False |  | ||||||
|  |  | ||||||
| def get_router_description(router_name, containers): | def get_groups(defaults): | ||||||
|     service_name = router_name.split('@')[0] |     groups = { | ||||||
|     for container in containers: |         defaults['DEFAULT_GROUP']: { | ||||||
|         labels = container.attrs.get('Config', {}).get('Labels', {}) |             'priority': int(defaults['DEFAULT_GROUP_PRIORITY']), | ||||||
|         description_label = f'traefik-frontend.http.routers.{service_name}.description' |             'collapsed': defaults['DEFAULT_GROUP_COLLAPSED'].lower() == 'true' if isinstance(defaults['DEFAULT_GROUP_COLLAPSED'], str) else defaults['DEFAULT_GROUP_COLLAPSED'], | ||||||
|         if description_label in labels: |  | ||||||
|             return labels[description_label] |  | ||||||
|     return 'No description available' |  | ||||||
|  |  | ||||||
| def get_router_display_name(router_name, containers): |  | ||||||
|     service_name = router_name.split('@')[0] |  | ||||||
|     for container in containers: |  | ||||||
|         labels = container.attrs.get('Config', {}).get('Labels', {}) |  | ||||||
|         display_name_label = f'traefik-frontend.http.routers.{service_name}.router_name' |  | ||||||
|         if display_name_label in labels: |  | ||||||
|             return labels[display_name_label].upper() |  | ||||||
|     return service_name.upper() |  | ||||||
|  |  | ||||||
| def get_router_group(router_name, containers): |  | ||||||
|     service_name = router_name.split('@')[0] |  | ||||||
|     for container in containers: |  | ||||||
|         labels = container.attrs.get('Config', {}).get('Labels', {}) |  | ||||||
|         group_label = f'traefik-frontend.http.routers.{service_name}.group' |  | ||||||
|         if group_label in labels: |  | ||||||
|             return labels[group_label] |  | ||||||
|     return DEFAULT_GROUP |  | ||||||
|  |  | ||||||
| def get_router_icon(router_name, containers): |  | ||||||
|     service_name = router_name.split('@')[0] |  | ||||||
|     for container in containers: |  | ||||||
|         labels = container.attrs.get('Config', {}).get('Labels', {}) |  | ||||||
|         icon_label = f'traefik-frontend.http.routers.{service_name}.icon' |  | ||||||
|         if icon_label in labels: |  | ||||||
|             return labels[icon_label] |  | ||||||
|     return DEFAULT_ICON |  | ||||||
|  |  | ||||||
| def get_groups(containers): |  | ||||||
|     groups = {DEFAULT_GROUP: {'priority': DEFAULT_GROUP_PRIORITY, 'collapsed': False, 'routers': [], 'icon': DEFAULT_GROUP_ICON}} |  | ||||||
|     for container in containers: |  | ||||||
|         labels = container.attrs.get('Config', {}).get('Labels', {}) |  | ||||||
|         for label, value in labels.items(): |  | ||||||
|             if label.startswith('traefik-frontend.groups.'): |  | ||||||
|                 group_name = label.split('.')[2] |  | ||||||
|                 priority_label = f'traefik-frontend.groups.{group_name}.priority' |  | ||||||
|                 collapsed_label = f'traefik-frontend.groups.{group_name}.collapsed' |  | ||||||
|                 icon_label = f'traefik-frontend.groups.{group_name}.icon' |  | ||||||
|  |  | ||||||
|                 if group_name not in groups: |  | ||||||
|                     groups[group_name] = { |  | ||||||
|                         'priority': DEFAULT_GROUP_PRIORITY, |  | ||||||
|                         'collapsed': False, |  | ||||||
|             'routers': [], |             'routers': [], | ||||||
|                         'icon': DEFAULT_GROUP_ICON |             'icon': defaults['DEFAULT_GROUP_ICON'] | ||||||
|  |         } | ||||||
|     } |     } | ||||||
|      |      | ||||||
|                 if priority_label in labels: |     for group_name, config in group_configs.items(): | ||||||
|                     groups[group_name]['priority'] = int(labels[priority_label]) |         groups[group_name] = { | ||||||
|                 if collapsed_label in labels: |             'priority': int(config.get('priority', defaults['DEFAULT_GROUP_PRIORITY'])), | ||||||
|                     groups[group_name]['collapsed'] = labels[collapsed_label].lower() == 'true' |             'collapsed': config.get('collapsed', defaults["DEFAULT_GROUP_COLLAPSED"]).lower() == 'true' if isinstance(config.get('collapsed', defaults["DEFAULT_GROUP_COLLAPSED"]), str) else config.get('collapsed', defaults["DEFAULT_GROUP_COLLAPSED"]), | ||||||
|                 if icon_label in labels: |             'routers': [], | ||||||
|                     groups[group_name]['icon'] = labels[icon_label] |             'icon': config.get('icon', defaults['DEFAULT_GROUP_ICON']) | ||||||
|  |         } | ||||||
|      |      | ||||||
|     return groups |     return groups | ||||||
|  |  | ||||||
| def get_title(containers): | @app.route('/static/css/colors.css') | ||||||
|     for container in containers: | def colors_css(): | ||||||
|         labels = container.attrs.get('Config', {}).get('Labels', {}) |     DEFAULT_COLORS_FILE = '/app/colors.yml' | ||||||
|         title_label = 'traefik-frontend.title' |     default_colors = load_yaml(DEFAULT_COLORS_FILE) | ||||||
|         if title_label in labels: |     if os.path.exists(USER_COLORS_FILE) and USER_COLORS_FILE: | ||||||
|             return labels[title_label] |         user_colors = load_yaml(USER_COLORS_FILE) | ||||||
|     return DEFAULT_TITLE |         merged_colors = merge_colors(default_colors["colors"], user_colors["colors"]) | ||||||
|  |     else: | ||||||
|  |         merged_colors = default_colors["colors"] | ||||||
|  |     return render_template('colors.css', colors=merged_colors), {'Content-Type': 'text/css'} | ||||||
|  |  | ||||||
|  |  | ||||||
| @app.route('/') | @app.route('/') | ||||||
| def index(): | def index(): | ||||||
|     containers = client.containers.list(all=True)  # Get all containers once |     if os.path.exists(USER_CONFIG_FILE) and USER_CONFIG_FILE: | ||||||
|     title = get_title(containers) |         user_config = load_yaml(USER_CONFIG_FILE) | ||||||
|     routers = get_routers(containers) |         categorize_configs(user_config.get("config", {})) | ||||||
|     groups = get_groups(containers) |     fetch_containers() | ||||||
|  |     defaults = get_defaults(global_configs) | ||||||
|  |     title = defaults["DEFAULT_TITLE"] | ||||||
|  |     routers = get_routers(defaults) | ||||||
|  |     groups = get_groups(defaults) | ||||||
|     for router in routers: |     for router in routers: | ||||||
|         group_name = router['group'] |         group_name = router['group'] | ||||||
|         if group_name not in groups: |         if group_name not in groups: | ||||||
| @@ -133,5 +151,4 @@ def index(): | |||||||
|     return render_template('index.html', title=title, groups=sorted_groups) |     return render_template('index.html', title=title, groups=sorted_groups) | ||||||
|  |  | ||||||
| if __name__ == '__main__': | if __name__ == '__main__': | ||||||
|     app.run(debug=True, host='0.0.0.0') |     serve(app, host='0.0.0.0', port=5000) | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										26
									
								
								colors.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								colors.yml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,26 @@ | |||||||
|  | colors: | ||||||
|  |   light: | ||||||
|  |     background: "#f0f0f0" | ||||||
|  |     title-text: "#e0e0e0" | ||||||
|  |     icons: "#333" | ||||||
|  |     search-bar-bg: "#f0f0f0" | ||||||
|  |     icon-bg: "#f0f0f033" | ||||||
|  |     icon-hover-bg: "#f0f0f066" | ||||||
|  |     router-bg: "#ffffff" | ||||||
|  |     router-hover-shadow: "#00000033" | ||||||
|  |     router-text: "#666666" | ||||||
|  |     accent: "#3f51b5" | ||||||
|  |  | ||||||
|  |   dark: | ||||||
|  |     background: "#121212" | ||||||
|  |     title-text: "#e0e0e0" | ||||||
|  |     icons: "#e0e0e0" | ||||||
|  |     header-bg: "#1e1e1e" | ||||||
|  |     search-bar-bg: "#121212" | ||||||
|  |     icon-bg: "#e0e0e033" | ||||||
|  |     icon-hover-bg: "#e0e0e066" | ||||||
|  |     router-bg: "#1e1e1e" | ||||||
|  |     router-hover-shadow: "#ffffff33" | ||||||
|  |     router-text: "#e0e0e0" | ||||||
|  |     footer-bg: "#1e1e1e" | ||||||
|  |     accent: "#bb86fc" | ||||||
| @@ -7,10 +7,14 @@ services: | |||||||
|       dockerfile: Dockerfile |       dockerfile: Dockerfile | ||||||
|     volumes: |     volumes: | ||||||
|       - /var/run/docker.sock:/var/run/docker.sock:ro |       - /var/run/docker.sock:/var/run/docker.sock:ro | ||||||
|  |       - ./data:/data | ||||||
|     environment: |     environment: | ||||||
|       - FLASK_ENV=development |       - FLASK_ENV=development | ||||||
|       - PUID=1000 |       - PUID=1000 | ||||||
|       - PGID=1000 |       - PGID=1000 | ||||||
|  |       - TRAEFIK_API_URL=http://norman.lan/api/http/routers | ||||||
|  |       - USER_CONFIG_FILE=/data/config.yml | ||||||
|  |       #- USER_COLORS_FILE=/data/accent.yml | ||||||
|     networks: |     networks: | ||||||
|       - web |       - web | ||||||
|     labels: |     labels: | ||||||
| @@ -43,6 +47,7 @@ services: | |||||||
|       - "traefik-frontend.groups.Security.priority=6" |       - "traefik-frontend.groups.Security.priority=6" | ||||||
|       - "traefik-frontend.groups.Security.icon=fas fa-lock" |       - "traefik-frontend.groups.Security.icon=fas fa-lock" | ||||||
|       - "traefik-frontend.title=GEDERICO'S HOME" |       - "traefik-frontend.title=GEDERICO'S HOME" | ||||||
|  |       - "traefik-frontend.entrypoint=https" | ||||||
|  |  | ||||||
| networks: | networks: | ||||||
|   web: |   web: | ||||||
|   | |||||||
| @@ -1,3 +1,5 @@ | |||||||
| docker==7.1.0 | docker==7.1.0 | ||||||
| Flask==2.3.2 | Flask==2.3.2 | ||||||
|  | PyYAML==6.0.1 | ||||||
| Requests==2.32.3 | Requests==2.32.3 | ||||||
|  | waitress==2.1.2 | ||||||
|   | |||||||
| @@ -1,122 +0,0 @@ | |||||||
| :root { |  | ||||||
|     --light-background: #f0f0f0; |  | ||||||
|     --dark-background: #121212; |  | ||||||
|     --light-title-text: #e0e0e0; |  | ||||||
|     --light-icons: #333; |  | ||||||
|     --dark-title-text: #e0e0e0; |  | ||||||
|     --dark-icons: #e0e0e0; |  | ||||||
|     --light-header: #3f51b5; |  | ||||||
|     --light-group-text: #3f51b5; |  | ||||||
|     --light-router-title: #3f51b5; |  | ||||||
|     --dark-header: #1e1e1e; |  | ||||||
|     --light-border: #3f51b5; |  | ||||||
|     --dark-border: #bb86fc; |  | ||||||
|     --dark-group-text: #bb86fc; |  | ||||||
|     --dark-router-title: #bb86fc; |  | ||||||
|     --light-icon-bg: rgba(240, 240, 240, 0.2); |  | ||||||
|     --dark-icon-bg: rgba(224, 224, 224, 0.2); |  | ||||||
|     --light-icon-hover-bg: rgba(240, 240, 240, 0.4); |  | ||||||
|     --dark-icon-hover-bg: rgba(224, 224, 224, 0.4); |  | ||||||
|     --light-router-bg: white; |  | ||||||
|     --dark-router-bg: #1e1e1e; |  | ||||||
|     --light-router-hover-shadow: rgba(0, 0, 0, 0.2); |  | ||||||
|     --light-router-text: #666; |  | ||||||
|     --dark-router-text: #e0e0e0; |  | ||||||
|     --dark-router-hover-shadow: rgba(255, 255, 255, 0.2); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| body { |  | ||||||
|     background-color: var(--light-background); |  | ||||||
|     color: var(--light-title-text); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .dark-mode { |  | ||||||
|     background-color: var(--dark-background); |  | ||||||
|     color: var(--dark-title-text); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .header { |  | ||||||
|     background-color: var(--light-header); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .dark-mode .header { |  | ||||||
|     background-color: var(--dark-header); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .icon-button { |  | ||||||
|     background-color: var(--light-icon-bg); |  | ||||||
|     border: 1px solid var(--light-border); |  | ||||||
|     color: var(--light-icons); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .icon-button:hover { |  | ||||||
|     background-color: var(--light-icon-hover-bg); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .dark-mode .icon-button { |  | ||||||
|     background-color: var(--dark-icon-bg); |  | ||||||
|     border: 1px solid var(--dark-border); |  | ||||||
|     color: var(--dark-icons); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .dark-mode .icon-button:hover { |  | ||||||
|     background-color: var(--dark-icon-hover-bg); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .group h2 { |  | ||||||
|     color: var(--light-group-text); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .dark-mode .group h2 { |  | ||||||
|     color: var(--dark-group-text); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .router { |  | ||||||
|     background-color: var(--light-router-bg); |  | ||||||
|     color: var(--light-router-text); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .router:hover { |  | ||||||
|     box-shadow: 0 4px 8px var(--light-router-hover-shadow); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .router h2 { |  | ||||||
|     color: var(--light-router-title); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .dark-mode .router h2 { |  | ||||||
|     color: var(--dark-router-title); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .dark-mode .router { |  | ||||||
|     background-color: var(--dark-router-bg); |  | ||||||
|     color: var(--dark-router-text); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .dark-mode .router:hover { |  | ||||||
|     box-shadow: 0 4px 8px var(--dark-router-hover-shadow); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .group-title .fa { |  | ||||||
|     color: var(--light-group-text); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .dark-mode .group-title .fa { |  | ||||||
|     color: var(--dark-group-text); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .settings-menu .fa { |  | ||||||
|     color: var(--light-icons); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .dark-mode .settings-menu .fa { |  | ||||||
|     color: var(--dark-icons); |  | ||||||
| } |  | ||||||
| .router svg { |  | ||||||
|     font-size: 26px; |  | ||||||
|     color: var(--light-router-title); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .dark-mode .router svg { |  | ||||||
|     color: var(--dark-router-title); |  | ||||||
| } |  | ||||||
| @@ -9,6 +9,7 @@ | |||||||
|     margin: 0; |     margin: 0; | ||||||
|     flex-grow: 1; |     flex-grow: 1; | ||||||
|     text-align: center; |     text-align: center; | ||||||
|  |     font-size: 2em; | ||||||
| } | } | ||||||
|  |  | ||||||
| .settings { | .settings { | ||||||
| @@ -41,13 +42,14 @@ | |||||||
| } | } | ||||||
|  |  | ||||||
| .group { | .group { | ||||||
|     margin-bottom: 20px;  /* Reduced margin between groups */ |     margin-bottom: 15px;  /* Reduced margin between groups */ | ||||||
| } | } | ||||||
|  |  | ||||||
| .group-title { | .group-title { | ||||||
|     display: inline-block; |     display: inline-block; | ||||||
|     cursor: pointer; |     cursor: pointer; | ||||||
|     margin-top: 10px;  /* Reduced margin above group title */ |     margin-top: 10px;  /* Reduced margin above group title */ | ||||||
|  |     margin-left: 15px; | ||||||
| } | } | ||||||
|  |  | ||||||
| .group-icon { | .group-icon { | ||||||
| @@ -89,34 +91,139 @@ | |||||||
|     margin: 0; |     margin: 0; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | .router div { | ||||||
|  |     overflow: hidden; | ||||||
|  | } | ||||||
|  |  | ||||||
| .router p { | .router p { | ||||||
|     font-size: 14px; |     font-size: 14px; | ||||||
|     margin: 5px 0 0; |     margin: 5px 0 0; | ||||||
|  |     white-space: nowrap; | ||||||
|  |     overflow: hidden; | ||||||
|  |     text-overflow: ellipsis; | ||||||
| } | } | ||||||
|  |  | ||||||
| .content { | .content { | ||||||
|     padding: 20px;  /* Added padding to the left and right of the webpage */ |     padding: 20px;  /* Added padding to the left and right of the webpage */ | ||||||
|  |     padding-bottom: 60px | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /* List mode styles */ | ||||||
|  | .list-mode .content { | ||||||
|  |     display: flex; | ||||||
|  |     flex-wrap: wrap; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .list-mode .group { | ||||||
|  |     display: flex; | ||||||
|  |     flex-direction: column; | ||||||
|  |     flex: 1 1 calc(33.333% - 40px);  /* Adjusted for new margin */ | ||||||
|  |     max-width: calc(33.333% - 40px);  /* Adjusted for new margin */ | ||||||
|  |     margin: 0 15px 15px 15px; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .list-mode .group-title { | ||||||
|  |     margin-left: 0; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .list-mode .router-container { | ||||||
|  |     flex-direction: column; | ||||||
|  |     flex-wrap: nowrap; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .list-mode .router { | ||||||
|  |     max-width: 100%; | ||||||
|  |     margin: 1px 0;  /* Adjusted margin between routers */ | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .router:first-child { | ||||||
|  |     margin-top: 15px;  /* Adjusted margin between routers */ | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .footer { | ||||||
|  |     position: fixed; | ||||||
|  |     bottom: 0; | ||||||
|  |     width: 100%; | ||||||
|  |     padding: 10px 0; | ||||||
|  |     box-shadow: 0 -2px 4px rgba(0, 0, 0, 0.1); | ||||||
|  |     display: flex; | ||||||
|  |     justify-content: center; | ||||||
|  |     align-items: center; | ||||||
|  |     z-index: 1000; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .search-bar { | ||||||
|  |     display: flex; | ||||||
|  |     align-items: center; | ||||||
|  |     width: 100%; | ||||||
|  |     max-width: 600px; | ||||||
|  |     border-radius: 20px; | ||||||
|  |     padding: 5px 10px; | ||||||
|  |     box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); | ||||||
|  |     margin: 0 20px; | ||||||
|  | } | ||||||
|  |  | ||||||
|  |  | ||||||
|  | .search-bar i { | ||||||
|  |     margin-right: 10px; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #clear-search, #start-search { | ||||||
|  |     cursor: pointer; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .search-bar input { | ||||||
|  |     width: 100%; | ||||||
|  |     border: none; | ||||||
|  |     background: none; | ||||||
|  |     outline: none; | ||||||
|  |     text-align: center; | ||||||
|  |     font-weight: bold; | ||||||
| } | } | ||||||
|  |  | ||||||
| /* Responsive Design */ | /* Responsive Design */ | ||||||
| @media (max-width: 1024px) { | @media (max-width: 1024px) { | ||||||
|     .router { |     body:not(.list-mode) .router { | ||||||
|  |         flex: 1 1 calc(50% - 40px);  /* Adjusted for new margin */ | ||||||
|  |         max-width: calc(50% - 40px);  /* Adjusted for new margin */ | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     .list-mode .group { | ||||||
|         flex: 1 1 calc(50% - 40px);  /* Adjusted for new margin */ |         flex: 1 1 calc(50% - 40px);  /* Adjusted for new margin */ | ||||||
|         max-width: calc(50% - 40px);  /* Adjusted for new margin */ |         max-width: calc(50% - 40px);  /* Adjusted for new margin */ | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
| @media (max-width: 768px) { | @media (max-width: 768px) { | ||||||
|     .router { |     body:not(.list-mode) .router { | ||||||
|         flex: 1 1 calc(50% - 40px);  /* Adjusted for new margin */ |         flex: 1 1 calc(50% - 40px);  /* Adjusted for new margin */ | ||||||
|         max-width: calc(50% - 40px);  /* Adjusted for new margin */ |         max-width: calc(50% - 40px);  /* Adjusted for new margin */ | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     .list-mode .group { | ||||||
|  |         flex: 1 1 calc(50% - 40px);  /* Adjusted for new margin */ | ||||||
|  |         max-width: calc(50% - 40px);  /* Adjusted for new margin */ | ||||||
|  |     } | ||||||
|  |     .header h1 { | ||||||
|  |         font-size: 1.8em; | ||||||
|  |     } | ||||||
| } | } | ||||||
|  |  | ||||||
| @media (max-width: 480px) { | @media (max-width: 480px) { | ||||||
|     .router { |     body:not(.list-mode) .router { | ||||||
|         flex: 1 1 100%; |         flex: 1 1 100%; | ||||||
|         max-width: 100%; |         max-width: 100%; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     .list-mode .group { | ||||||
|  |         flex: 1 1 100%; | ||||||
|  |         max-width: 100%; | ||||||
|  |     } | ||||||
|  |     .router:first-child { | ||||||
|  |         margin-top: 1px;  /* Adjusted margin between routers */ | ||||||
|  |     } | ||||||
|  |     .header h1 { | ||||||
|  |         font-size: 1.5em; | ||||||
|  |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,5 +1,5 @@ | |||||||
| body { | body { | ||||||
|     font-family: Arial, sans-serif; |     font-family: 'Helvetica', 'Arial', sans-serif; | ||||||
|     margin: 0; |     margin: 0; | ||||||
|     transition: background-color 0.3s, color 0.3s; |     transition: background-color 0.3s, color 0.3s; | ||||||
| } | } | ||||||
| @@ -34,4 +34,10 @@ body { | |||||||
|     transition: box-shadow 0.3s ease; |     transition: box-shadow 0.3s ease; | ||||||
|     box-sizing: border-box; |     box-sizing: border-box; | ||||||
| } | } | ||||||
|  | /* Custom scrollbar for webkit browsers */ | ||||||
|  | body::-webkit-scrollbar { | ||||||
|  |     width: 8px; | ||||||
|  |     height: 8px; | ||||||
|  |     overflow: visible; | ||||||
|  |     display: none; | ||||||
|  | } | ||||||
|   | |||||||
| @@ -1,4 +1,8 @@ | |||||||
|  | let initialWindowHeight; | ||||||
|  |  | ||||||
| document.addEventListener("DOMContentLoaded", function() { | document.addEventListener("DOMContentLoaded", function() { | ||||||
|  |     initialWindowHeight = window.innerHeight; | ||||||
|  |  | ||||||
|     if (localStorage.getItem("dark-mode") === "true") { |     if (localStorage.getItem("dark-mode") === "true") { | ||||||
|         document.body.classList.add("dark-mode"); |         document.body.classList.add("dark-mode"); | ||||||
|         document.getElementById('theme-icon').classList.remove('fa-moon'); |         document.getElementById('theme-icon').classList.remove('fa-moon'); | ||||||
| @@ -8,6 +12,15 @@ document.addEventListener("DOMContentLoaded", function() { | |||||||
|         document.getElementById('theme-icon').classList.add('fa-moon'); |         document.getElementById('theme-icon').classList.add('fa-moon'); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     if (localStorage.getItem("view-mode") === "list") { | ||||||
|  |         document.body.classList.add("list-mode"); | ||||||
|  |         document.getElementById('view-mode-icon').classList.remove('fa-list'); | ||||||
|  |         document.getElementById('view-mode-icon').classList.add('fa-table'); | ||||||
|  |     } else { | ||||||
|  |         document.getElementById('view-mode-icon').classList.remove('fa-table'); | ||||||
|  |         document.getElementById('view-mode-icon').classList.add('fa-list'); | ||||||
|  |     } | ||||||
|  |  | ||||||
|     document.querySelectorAll('.group').forEach(group => { |     document.querySelectorAll('.group').forEach(group => { | ||||||
|         const groupName = group.id; |         const groupName = group.id; | ||||||
|         const collapsed = group.dataset.collapsed === 'true'; |         const collapsed = group.dataset.collapsed === 'true'; | ||||||
| @@ -24,8 +37,30 @@ document.addEventListener("DOMContentLoaded", function() { | |||||||
|             settingsMenu.classList.remove('visible'); |             settingsMenu.classList.remove('visible'); | ||||||
|         } |         } | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
|  |     const searchInput = document.getElementById('search-input'); | ||||||
|  |     searchInput.addEventListener('input', handleSearch); | ||||||
|  |     searchInput.addEventListener('keydown', handleKeyDown); | ||||||
|  |  | ||||||
|  |     document.addEventListener('keydown', function(event) { | ||||||
|  |         if (event.key === '/') { | ||||||
|  |             event.preventDefault(); | ||||||
|  |             searchInput.focus(); | ||||||
|  |         } | ||||||
|  |         if (event.key === 'Escape' || event.key === 'Esc') { | ||||||
|  |             event.preventDefault(); | ||||||
|  |             clearSearch(); | ||||||
|  |             searchInput.blur(); | ||||||
|  |         } | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     window.visualViewport.addEventListener('resize', adjustFooterPosition); | ||||||
| }); | }); | ||||||
|  |  | ||||||
|  | function searchFocus() { | ||||||
|  |     document.getElementById('search-input').focus() | ||||||
|  | } | ||||||
|  |  | ||||||
| function toggleDarkMode() { | function toggleDarkMode() { | ||||||
|     document.body.classList.toggle("dark-mode"); |     document.body.classList.toggle("dark-mode"); | ||||||
|     const themeIcon = document.getElementById('theme-icon'); |     const themeIcon = document.getElementById('theme-icon'); | ||||||
| @@ -56,3 +91,171 @@ function refreshData() { | |||||||
|     location.reload(); |     location.reload(); | ||||||
| } | } | ||||||
|  |  | ||||||
|  | function toggleViewMode() { | ||||||
|  |     document.body.classList.toggle("list-mode"); | ||||||
|  |     const viewModeIcon = document.getElementById('view-mode-icon'); | ||||||
|  |     if (document.body.classList.contains("list-mode")) { | ||||||
|  |         localStorage.setItem("view-mode", "list"); | ||||||
|  |         viewModeIcon.classList.remove('fa-list'); | ||||||
|  |         viewModeIcon.classList.add('fa-table'); | ||||||
|  |     } else { | ||||||
|  |         localStorage.setItem("view-mode", "box"); | ||||||
|  |         viewModeIcon.classList.remove('fa-table'); | ||||||
|  |         viewModeIcon.classList.add('fa-list'); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | let selectedRouterIndex = -1; | ||||||
|  |  | ||||||
|  | function handleSearch(event) { | ||||||
|  |     const query = event.target.value.toLowerCase(); | ||||||
|  |     const groups = document.querySelectorAll('.group'); | ||||||
|  |     const content = document.getElementById('content'); | ||||||
|  |  | ||||||
|  |     // Remove the search group if it exists | ||||||
|  |     let searchGroup = document.getElementById('search-group'); | ||||||
|  |     if (searchGroup) { | ||||||
|  |         searchGroup.remove(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     if (query === '') { | ||||||
|  |         // Show all original groups | ||||||
|  |         groups.forEach(group => group.style.display = ''); | ||||||
|  |         selectedRouterIndex = -1; | ||||||
|  |     } else { | ||||||
|  |         // Hide all original groups | ||||||
|  |         groups.forEach(group => group.style.display = 'none'); | ||||||
|  |  | ||||||
|  |         // Create and show the search group | ||||||
|  |         searchGroup = document.createElement('div'); | ||||||
|  |         searchGroup.id = 'search-group'; | ||||||
|  |         searchGroup.className = 'group'; | ||||||
|  |         content.appendChild(searchGroup); | ||||||
|  |  | ||||||
|  |         searchGroup.innerHTML = ` | ||||||
|  |             <h2 class="group-title"><i class="fas fa-search"></i> ${query}</h2> | ||||||
|  |             <div class="router-container"></div> | ||||||
|  |         `; | ||||||
|  |  | ||||||
|  |         const routerContainer = searchGroup.querySelector('.router-container'); | ||||||
|  |         const matchingRouters = []; | ||||||
|  |  | ||||||
|  |         document.querySelectorAll('.router').forEach(router => { | ||||||
|  |             const displayName = router.querySelector('h2').textContent.toLowerCase(); | ||||||
|  |             if (displayName.includes(query)) { | ||||||
|  |                 const clonedRouter = router.cloneNode(true); | ||||||
|  |                 matchingRouters.push(clonedRouter); | ||||||
|  |                 routerContainer.appendChild(clonedRouter); | ||||||
|  |             } | ||||||
|  |         }); | ||||||
|  |  | ||||||
|  |         if (matchingRouters.length > 0) { | ||||||
|  |             selectedRouterIndex = 0; | ||||||
|  |             matchingRouters[selectedRouterIndex].classList.add('selected'); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // Scroll to search group | ||||||
|  |         document.getElementById("header").scrollIntoView({ behavior: "instant" }); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function handleKeyDown(event) { | ||||||
|  |     const searchGroup = document.getElementById('search-group'); | ||||||
|  |     if (!searchGroup) return; | ||||||
|  |  | ||||||
|  |     const matchingRouters = searchGroup.querySelectorAll('.router'); | ||||||
|  |     if (matchingRouters.length === 0) return; | ||||||
|  |  | ||||||
|  |     const isListMode = document.body.classList.contains('list-mode'); | ||||||
|  |     const itemsPerRow = getItemsPerRow(); | ||||||
|  |  | ||||||
|  |     switch (event.key) { | ||||||
|  |         case 'ArrowDown': | ||||||
|  |             event.preventDefault(); | ||||||
|  |             if (isListMode) { | ||||||
|  |                 if (selectedRouterIndex < matchingRouters.length - 1) { | ||||||
|  |                     matchingRouters[selectedRouterIndex].classList.remove('selected'); | ||||||
|  |                     selectedRouterIndex++; | ||||||
|  |                     matchingRouters[selectedRouterIndex].classList.add('selected'); | ||||||
|  |                     matchingRouters[selectedRouterIndex].scrollIntoView({ behavior: 'smooth', block: 'center' }); | ||||||
|  |                 } | ||||||
|  |             } else { | ||||||
|  |                 if (selectedRouterIndex + itemsPerRow < matchingRouters.length) { | ||||||
|  |                     matchingRouters[selectedRouterIndex].classList.remove('selected'); | ||||||
|  |                     selectedRouterIndex += itemsPerRow; | ||||||
|  |                     matchingRouters[selectedRouterIndex].classList.add('selected'); | ||||||
|  |                     matchingRouters[selectedRouterIndex].scrollIntoView({ behavior: 'smooth', block: 'center' }); | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |             break; | ||||||
|  |         case 'ArrowUp': | ||||||
|  |             event.preventDefault(); | ||||||
|  |             if (isListMode) { | ||||||
|  |                 if (selectedRouterIndex > 0) { | ||||||
|  |                     matchingRouters[selectedRouterIndex].classList.remove('selected'); | ||||||
|  |                     selectedRouterIndex--; | ||||||
|  |                     matchingRouters[selectedRouterIndex].classList.add('selected'); | ||||||
|  |                     matchingRouters[selectedRouterIndex].scrollIntoView({ behavior: 'smooth', block: 'center' }); | ||||||
|  |                 } | ||||||
|  |             } else { | ||||||
|  |                 if (selectedRouterIndex - itemsPerRow >= 0) { | ||||||
|  |                     matchingRouters[selectedRouterIndex].classList.remove('selected'); | ||||||
|  |                     selectedRouterIndex -= itemsPerRow; | ||||||
|  |                     matchingRouters[selectedRouterIndex].classList.add('selected'); | ||||||
|  |                     matchingRouters[selectedRouterIndex].scrollIntoView({ behavior: 'smooth', block: 'center' }); | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |             break; | ||||||
|  |         case 'ArrowRight': | ||||||
|  |             if (!isListMode) { | ||||||
|  |                 event.preventDefault(); | ||||||
|  |                 if (selectedRouterIndex < matchingRouters.length - 1) { | ||||||
|  |                     matchingRouters[selectedRouterIndex].classList.remove('selected'); | ||||||
|  |                     selectedRouterIndex++; | ||||||
|  |                     matchingRouters[selectedRouterIndex].classList.add('selected'); | ||||||
|  |                     matchingRouters[selectedRouterIndex].scrollIntoView({ behavior: 'smooth', block: 'center' }); | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |             break; | ||||||
|  |         case 'ArrowLeft': | ||||||
|  |             if (!isListMode) { | ||||||
|  |                 event.preventDefault(); | ||||||
|  |                 if (selectedRouterIndex > 0) { | ||||||
|  |                     matchingRouters[selectedRouterIndex].classList.remove('selected'); | ||||||
|  |                     selectedRouterIndex--; | ||||||
|  |                     matchingRouters[selectedRouterIndex].classList.add('selected'); | ||||||
|  |                     matchingRouters[selectedRouterIndex].scrollIntoView({ behavior: 'smooth', block: 'center' }); | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |             break; | ||||||
|  |         case 'Enter': | ||||||
|  |             event.preventDefault(); | ||||||
|  |             if (selectedRouterIndex >= 0 && selectedRouterIndex < matchingRouters.length) { | ||||||
|  |                 matchingRouters[selectedRouterIndex].click(); | ||||||
|  |             } | ||||||
|  |             break; | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function getItemsPerRow() { | ||||||
|  |     const width = window.innerWidth; | ||||||
|  |     if (width <= 480) return 1; | ||||||
|  |     if (width <= 1024) return 2; | ||||||
|  |     return 3; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function clearSearch() { | ||||||
|  |     const searchInput = document.getElementById('search-input'); | ||||||
|  |     searchInput.value = ''; | ||||||
|  |     handleSearch({ target: searchInput }); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function adjustFooterPosition() { | ||||||
|  |     const footer = document.querySelector('.footer'); | ||||||
|  |     const visualViewportHeight = window.visualViewport.height; | ||||||
|  |     const bottomOffset = window.innerHeight - visualViewportHeight; | ||||||
|  |  | ||||||
|  |     // Set the bottom position | ||||||
|  |     footer.style.bottom = `${bottomOffset}px`; | ||||||
|  | } | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										
											BIN
										
									
								
								static/tf.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								static/tf.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 394 KiB | 
							
								
								
									
										163
									
								
								templates/colors.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										163
									
								
								templates/colors.css
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,163 @@ | |||||||
|  | :root { | ||||||
|  |     {% for mode, mode_colors in colors.items() %} | ||||||
|  |         {% for name, value in mode_colors.items() %} | ||||||
|  |             --{{ mode }}-{{ name }}: {{ value }}; | ||||||
|  |         {% endfor %} | ||||||
|  |     {% endfor %} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /* Body */ | ||||||
|  | body { | ||||||
|  |     background-color: var(--light-background); | ||||||
|  |     color: var(--light-title-text); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .dark-mode { | ||||||
|  |     background-color: var(--dark-background); | ||||||
|  |     color: var(--dark-title-text); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /* Header */ | ||||||
|  | .header { | ||||||
|  |     background-color: var(--light-header-bg, var(--light-accent)); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .dark-mode .header { | ||||||
|  |     background-color: var(--dark-header-bg, var(--dark-accent)); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /* Icon Button */ | ||||||
|  | .icon-button { | ||||||
|  |     background-color: var(--light-icon-bg); | ||||||
|  |     border: 1px solid var(--light-border, var(--light-accent)); | ||||||
|  |     color: var(--light-icons); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .icon-button:hover { | ||||||
|  |     background-color: var(--light-icon-hover-bg); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .dark-mode .icon-button { | ||||||
|  |     background-color: var(--dark-icon-bg); | ||||||
|  |     border: 1px solid var(--dark-border, var(--dark-accent)); | ||||||
|  |     color: var(--dark-icons); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .dark-mode .icon-button:hover { | ||||||
|  |     background-color: var(--dark-icon-hover-bg); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /* Group Title */ | ||||||
|  | .group h2 { | ||||||
|  |     color: var(--light-group-text, var(--light-accent)); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .dark-mode .group h2 { | ||||||
|  |     color: var(--dark-group-text, var(--dark-accent)); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /* Router */ | ||||||
|  | .router { | ||||||
|  |     background-color: var(--light-router-bg); | ||||||
|  |     color: var(--light-router-text); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .router:hover { | ||||||
|  |     box-shadow: 0 4px 8px var(--light-router-hover-shadow); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .router h2 { | ||||||
|  |     color: var(--light-router-title, var(--light-accent)); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .dark-mode .router h2 { | ||||||
|  |     color: var(--dark-router-title, var(--dark-accent)); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .dark-mode .router { | ||||||
|  |     background-color: var(--dark-router-bg); | ||||||
|  |     color: var(--dark-router-text); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .dark-mode .router:hover { | ||||||
|  |     box-shadow: 0 4px 8px var(--dark-router-hover-shadow); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /* Group Title Icon */ | ||||||
|  | .group-title .fa { | ||||||
|  |     color: var(--light-group-text, var(--light-accent)); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .dark-mode .group-title .fa { | ||||||
|  |     color: var(--dark-group-text, var(--dark-accent)); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /* Settings Menu Icon */ | ||||||
|  | .settings-menu .fa { | ||||||
|  |     color: var(--light-icons); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .dark-mode .settings-menu .fa { | ||||||
|  |     color: var(--dark-icons); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /* Router Icon */ | ||||||
|  | .router svg { | ||||||
|  |     font-size: 26px; | ||||||
|  |     color: var(--light-router-title, var(--light-accent)); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .dark-mode .router svg { | ||||||
|  |     color: var(--dark-router-title, var(--dark-accent)); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /* Footer */ | ||||||
|  | .footer { | ||||||
|  |     background-color: var(--light-footer-bg, var(--light-accent)); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .dark-mode .footer { | ||||||
|  |     background-color: var(--dark-footer-bg); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /* Search Bar */ | ||||||
|  | .search-bar { | ||||||
|  |     background-color: var(--light-search-bar-bg); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .dark-mode .search-bar { | ||||||
|  |     background-color: var(--dark-search-bar-bg); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .search-bar i { | ||||||
|  |     color: var(--light-search-bar-icon, var(--light-accent)); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .search-bar svg { | ||||||
|  |     color: var(--light-search-bar-icon, var(--light-accent)); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .dark-mode .search-bar svg { | ||||||
|  |     color: var(--dark-search-bar-icon, var(--dark-accent)); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .dark-mode .search-bar i { | ||||||
|  |     color: var(--dark-search-bar-icon, var(--dark-accent)); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .search-bar input { | ||||||
|  |     color: var(--light-search-bar-input, var(--light-accent)); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .dark-mode .search-bar input { | ||||||
|  |     color: var(--dark-search-bar-input, var(--dark-accent)); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /* Selected Router */ | ||||||
|  | .router.selected { | ||||||
|  |     border: 2px solid var(--light-selected-router-border, var(--light-accent)); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .dark-mode .router.selected { | ||||||
|  |     border: 2px solid var(--dark-selected-router-border, var(--dark-accent)); | ||||||
|  | } | ||||||
| @@ -7,31 +7,35 @@ | |||||||
|     <link rel="stylesheet" href="{{ url_for('static', filename='css/colors.css') }}"> |     <link rel="stylesheet" href="{{ url_for('static', filename='css/colors.css') }}"> | ||||||
|     <link rel="stylesheet" href="{{ url_for('static', filename='css/styles.css') }}"> |     <link rel="stylesheet" href="{{ url_for('static', filename='css/styles.css') }}"> | ||||||
|     <link rel="stylesheet" href="{{ url_for('static', filename='css/components.css') }}"> |     <link rel="stylesheet" href="{{ url_for('static', filename='css/components.css') }}"> | ||||||
|  |     <link rel="icon" href="{{ url_for('static', filename='tf.png')  }}" type="image/png">  | ||||||
|     <script src="{{ url_for('static', filename='js/fontawesome.js') }}" defer></script> |     <script src="{{ url_for('static', filename='js/fontawesome.js') }}" defer></script> | ||||||
|     <script src="{{ url_for('static', filename='js/scripts.js') }}" defer></script> |     <script src="{{ url_for('static', filename='js/scripts.js') }}" defer></script> | ||||||
| </head> | </head> | ||||||
| <body> | <body> | ||||||
|     <div class="header"> |     <div id="header" class="header"> | ||||||
|         <h1>{{ title }}</h1> |         <h1>{{ title }}</h1> | ||||||
|         <div class="settings"> |         <div class="settings"> | ||||||
|             <button class="icon-button" onclick="toggleSettings()"><i class="fas fa-cog"></i></button> |             <button class="icon-button" onclick="toggleSettings()"><i class="fas fa-cog"></i></button> | ||||||
|             <div id="settings-menu" class="settings-menu"> |             <div id="settings-menu" class="settings-menu"> | ||||||
|                 <button class="icon-button" onclick="toggleDarkMode()"><i id="theme-icon" class="fas fa-moon"></i></button> |                 <button class="icon-button" onclick="toggleDarkMode()"><i id="theme-icon" class="fas fa-moon"></i></button> | ||||||
|                 <button class="icon-button" onclick="refreshData()"><i class="fas fa-sync-alt"></i></button> |                 <button class="icon-button" onclick="refreshData()"><i class="fas fa-sync-alt"></i></button> | ||||||
|  |                 <button class="icon-button" onclick="toggleViewMode()"><i class="fas fa-list" id="view-mode-icon"></i></button> | ||||||
|             </div> |             </div> | ||||||
|         </div> |         </div> | ||||||
|     </div> |     </div> | ||||||
|     <div class="content"> |     <div class="content" id="content"> | ||||||
|         {% for group, data in groups.items() %} |         {% for group, data in groups.items() %} | ||||||
|             <div class="group {% if data.collapsed %}collapsed{% endif %}" id="{{ group }}" data-collapsed="{{ data.collapsed }}"> |             <div class="group {% if data.collapsed %}collapsed{% endif %}" id="{{ group }}" data-collapsed="{{ data.collapsed }}"> | ||||||
|                 <h2 class="group-title" onclick="toggleGroup('{{ group }}')"><i class="{{ data.icon }}"></i> {{ group }}</h2> |                 <h2 class="group-title" onclick="toggleGroup('{{ group }}')"><i class="{{ data.icon }}"></i> {{ group }}</h2> | ||||||
|                 <div class="router-container"> |                 <div class="router-container"> | ||||||
|                     {% for router in data.routers %} |                     {% for router in data.routers %} | ||||||
|                         <div class="router" onclick="window.open('http://{{ router['rule'].split('`')[1] }}', '_blank')"> |                         <div class="router" onclick="window.open('{{ router['protocol'] }}://{{ router['rule'].split('`')[1] }}', '{{ router['target'] }}')"> | ||||||
|                             <i class="{{ router['icon'] }}"></i> |                             <i class="{{ router['icon'] }}"></i> | ||||||
|                             <div> |                             <div> | ||||||
|                                 <h2>{{ router['display_name'] }}</h2> |                                 <h2>{{ router['display_name'] }}</h2> | ||||||
|  |                                 {% if router['description'] %} | ||||||
|                                     <p>{{ router['description'] }}</p> |                                     <p>{{ router['description'] }}</p> | ||||||
|  |                                 {% endif %} | ||||||
|                             </div> |                             </div> | ||||||
|                         </div> |                         </div> | ||||||
|                     {% endfor %} |                     {% endfor %} | ||||||
| @@ -39,6 +43,13 @@ | |||||||
|             </div> |             </div> | ||||||
|         {% endfor %} |         {% endfor %} | ||||||
|     </div> |     </div> | ||||||
|  |     <div class="footer"> | ||||||
|  |         <div class="search-bar"> | ||||||
|  |             <i class="fas fa-search" id="start-search" onclick="searchFocus()"></i> | ||||||
|  |             <input type="text" id="search-input" placeholder="Search..."> | ||||||
|  |             <i class="fas fa-times" id="clear-search" onclick="clearSearch()"></i> | ||||||
|  |         </div> | ||||||
|  |     </div> | ||||||
| </body> | </body> | ||||||
| </html> | </html> | ||||||
|  |  | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user