This commit is contained in:
2024-08-05 17:38:47 -03:00
parent 2353362c04
commit 62976c8e5c
12 changed files with 5 additions and 5 deletions

154
app/app.py Normal file
View File

@ -0,0 +1,154 @@
from flask import Flask, render_template
import re
import docker
import requests
import yaml
import os
from waitress import serve
app = Flask(__name__)
TRAEFIK_API_URL = os.getenv('TRAEFIK_API_URL')
# Check if TRAEFIK_API_URL is set
if not TRAEFIK_API_URL:
raise ValueError("TRAEFIK_API_URL environment variable is required and not set.")
USER_COLORS_FILE = os.getenv('USER_COLORS_FILE', False)
USER_CONFIG_FILE = os.getenv('USER_CONFIG_FILE', False)
router_configs, group_configs, global_configs = {}, {}, {}
client = docker.from_env()
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
if response.status_code == 200:
routers = response.json()
filtered_routers = filter_routers(routers, defaults)
return filtered_routers
return []
def filter_routers(routers, defaults):
filtered_routers = []
for router in routers:
for pattern in defaults["REGEX_PATTERNS"]:
if re.match(pattern, router['rule'].split('`')[1]):
if not is_router_hidden(router['name']):
router_name = router['name'].split('@')[0]
router['description'] = router_configs.get(router_name, {}).get('description', False)
router['display_name'] = router_configs.get(router_name, {}).get('router_name', router_name).upper()
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)
break
return filtered_routers
def is_router_hidden(router_name):
service_name = router_name.split('@')[0]
return router_configs.get(service_name, {}).get('hidden') == 'true'
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
@app.route('/static/css/colors.css')
def colors_css():
DEFAULT_COLORS_FILE = '/app/colors.yml'
default_colors = load_yaml(DEFAULT_COLORS_FILE)
if os.path.exists(USER_COLORS_FILE) and USER_COLORS_FILE:
user_colors = load_yaml(USER_COLORS_FILE)
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('/')
def index():
if os.path.exists(USER_CONFIG_FILE) and USER_CONFIG_FILE:
user_config = load_yaml(USER_CONFIG_FILE)
categorize_configs(user_config.get("config", {}))
fetch_containers()
defaults = get_defaults(global_configs)
title = defaults["DEFAULT_TITLE"]
routers = get_routers(defaults)
groups = get_groups(defaults)
for router in routers:
group_name = router['group']
if group_name not in groups:
group_name = DEFAULT_GROUP
groups[group_name]['routers'].append(router)
sorted_groups = {k: v for k, v in sorted(groups.items(), key=lambda item: item[1]['priority']) if v['routers']}
return render_template('index.html', title=title, groups=sorted_groups)
if __name__ == '__main__':
serve(app, host='0.0.0.0', port=5000)

26
app/colors.yml Normal file
View 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"

5
app/requirements.txt Normal file
View File

@ -0,0 +1,5 @@
docker==7.1.0
Flask==2.3.2
PyYAML==6.0.1
Requests==2.32.3
waitress==2.1.2

View File

@ -0,0 +1,229 @@
.header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px;
}
.header h1 {
margin: 0;
flex-grow: 1;
text-align: center;
font-size: 2em;
}
.settings {
position: relative;
}
.icon-button {
font-size: 24px;
cursor: pointer;
margin: 5px;
border-radius: 50%;
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
transition: background-color 0.3s, border-color 0.3s;
}
.settings-menu {
display: none;
position: absolute;
top: 50px;
right: 0;
flex-direction: column;
}
.settings-menu.visible {
display: flex;
}
.group {
margin-bottom: 15px; /* Reduced margin between groups */
}
.group-title {
display: inline-block;
cursor: pointer;
margin-top: 10px; /* Reduced margin above group title */
margin-left: 15px;
}
.group-icon {
margin-right: 8px;
}
.router-container {
display: flex;
flex-wrap: wrap;
justify-content: flex-start;
}
.router {
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
padding: 20px 15px; /* Added padding top and bottom */
margin: 15px; /* Added margin between boxes */
flex: 1 1 calc(33.333% - 40px); /* Adjusted for new margin */
max-width: calc(33.333% - 40px); /* Adjusted for new margin */
text-align: left;
cursor: pointer;
transition: box-shadow 0.3s ease;
box-sizing: border-box;
display: flex;
align-items: center;
}
.router svg {
font-size: 32px; /* Made the icon bigger */
margin-right: 15px; /* Added padding between icon and text */
}
.router:hover {
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
}
.router h2 {
font-size: 18px;
margin: 0;
}
.router div {
overflow: hidden;
}
.router p {
font-size: 14px;
margin: 5px 0 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.content {
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 */
@media (max-width: 1024px) {
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 */
max-width: calc(50% - 40px); /* Adjusted for new margin */
}
}
@media (max-width: 768px) {
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 */
max-width: calc(50% - 40px); /* Adjusted for new margin */
}
.header h1 {
font-size: 1.8em;
}
}
@media (max-width: 480px) {
body:not(.list-mode) .router {
flex: 1 1 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;
}
}

43
app/static/css/styles.css Normal file
View File

@ -0,0 +1,43 @@
body {
font-family: 'Helvetica', 'Arial', sans-serif;
margin: 0;
transition: background-color 0.3s, color 0.3s;
}
.content {
padding: 20px;
}
.group {
margin-bottom: 40px;
}
.group.collapsed .router-container {
display: none;
}
.router-container {
display: flex;
flex-wrap: wrap;
justify-content: flex-start;
}
.router {
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
padding: 20px;
margin: 10px;
flex: 1 1 calc(33.333% - 20px);
max-width: calc(33.333% - 20px);
text-align: center;
cursor: pointer;
transition: box-shadow 0.3s ease;
box-sizing: border-box;
}
/* Custom scrollbar for webkit browsers */
body::-webkit-scrollbar {
width: 8px;
height: 8px;
overflow: visible;
display: none;
}

6044
app/static/js/fontawesome.js Normal file

File diff suppressed because one or more lines are too long

261
app/static/js/scripts.js Normal file
View File

@ -0,0 +1,261 @@
let initialWindowHeight;
document.addEventListener("DOMContentLoaded", function() {
initialWindowHeight = window.innerHeight;
if (localStorage.getItem("dark-mode") === "true") {
document.body.classList.add("dark-mode");
document.getElementById('theme-icon').classList.remove('fa-moon');
document.getElementById('theme-icon').classList.add('fa-sun');
} else {
document.getElementById('theme-icon').classList.remove('fa-sun');
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 => {
const groupName = group.id;
const collapsed = group.dataset.collapsed === 'true';
if (collapsed) {
group.classList.add('collapsed');
} else if (localStorage.getItem(`collapsed-${groupName}`) === 'true') {
group.classList.add('collapsed');
}
});
document.addEventListener("click", function(event) {
const settingsMenu = document.getElementById('settings-menu');
if (!settingsMenu.contains(event.target) && !event.target.closest('.icon-button')) {
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() {
document.body.classList.toggle("dark-mode");
const themeIcon = document.getElementById('theme-icon');
if (document.body.classList.contains("dark-mode")) {
localStorage.setItem("dark-mode", "true");
themeIcon.classList.remove('fa-moon');
themeIcon.classList.add('fa-sun');
} else {
localStorage.setItem("dark-mode", "false");
themeIcon.classList.remove('fa-sun');
themeIcon.classList.add('fa-moon');
}
}
function toggleGroup(group) {
const groupElement = document.getElementById(group);
groupElement.classList.toggle("collapsed");
const isCollapsed = groupElement.classList.contains("collapsed");
localStorage.setItem(`collapsed-${group}`, isCollapsed);
}
function toggleSettings() {
const settingsMenu = document.getElementById('settings-menu');
settingsMenu.classList.toggle('visible');
}
function refreshData() {
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
app/static/tf.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 394 KiB

163
app/templates/colors.css Normal file
View 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));
}

55
app/templates/index.html Normal file
View File

@ -0,0 +1,55 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ title }}</title>
<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/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/scripts.js') }}" defer></script>
</head>
<body>
<div id="header" class="header">
<h1>{{ title }}</h1>
<div class="settings">
<button class="icon-button" onclick="toggleSettings()"><i class="fas fa-cog"></i></button>
<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="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 class="content" id="content">
{% for group, data in groups.items() %}
<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>
<div class="router-container">
{% for router in data.routers %}
<div class="router" onclick="window.open('{{ router['protocol'] }}://{{ router['rule'].split('`')[1] }}', '{{ router['target'] }}')">
<i class="{{ router['icon'] }}"></i>
<div>
<h2>{{ router['display_name'] }}</h2>
{% if router['description'] %}
<p>{{ router['description'] }}</p>
{% endif %}
</div>
</div>
{% endfor %}
</div>
</div>
{% endfor %}
</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>
</html>