Compare commits
2 Commits
973cd977ba
...
2353362c04
Author | SHA1 | Date | |
---|---|---|---|
2353362c04 | |||
ab6f27524d |
215
app.py
215
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):
|
|
||||||
service_name = router_name.split('@')[0]
|
|
||||||
for container in containers:
|
|
||||||
labels = container.attrs.get('Config', {}).get('Labels', {})
|
|
||||||
description_label = f'traefik-frontend.http.routers.{service_name}.description'
|
|
||||||
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': [],
|
|
||||||
'icon': DEFAULT_GROUP_ICON
|
|
||||||
}
|
|
||||||
|
|
||||||
if priority_label in labels:
|
|
||||||
groups[group_name]['priority'] = int(labels[priority_label])
|
|
||||||
if collapsed_label in labels:
|
|
||||||
groups[group_name]['collapsed'] = labels[collapsed_label].lower() == 'true'
|
|
||||||
if icon_label in labels:
|
|
||||||
groups[group_name]['icon'] = labels[icon_label]
|
|
||||||
|
|
||||||
|
def get_groups(defaults):
|
||||||
|
groups = {
|
||||||
|
defaults['DEFAULT_GROUP']: {
|
||||||
|
'priority': int(defaults['DEFAULT_GROUP_PRIORITY']),
|
||||||
|
'collapsed': defaults['DEFAULT_GROUP_COLLAPSED'].lower() == 'true' if isinstance(defaults['DEFAULT_GROUP_COLLAPSED'], str) else defaults['DEFAULT_GROUP_COLLAPSED'],
|
||||||
|
'routers': [],
|
||||||
|
'icon': defaults['DEFAULT_GROUP_ICON']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for group_name, config in group_configs.items():
|
||||||
|
groups[group_name] = {
|
||||||
|
'priority': int(config.get('priority', defaults['DEFAULT_GROUP_PRIORITY'])),
|
||||||
|
'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"]),
|
||||||
|
'routers': [],
|
||||||
|
'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>
|
||||||
<p>{{ router['description'] }}</p>
|
{% if router['description'] %}
|
||||||
|
<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>
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user