Compare commits

..

7 Commits

Author SHA1 Message Date
ef6145dfb6 improve token styling 2026-01-18 19:45:37 +00:00
d4605b952e restructure file access 2026-01-18 19:00:53 +00:00
007ad4581b improve player look 2026-01-18 18:48:36 +00:00
8008d68c8b redesigne search buttons 2026-01-18 17:47:03 +00:00
09296f077f fixing style 2026-01-18 14:53:31 +00:00
a76c2ec4a0 use base.html in app.html 2026-01-18 13:31:30 +00:00
e2ce075c96 modern designe 2026-01-17 19:41:50 +00:00
14 changed files with 997 additions and 534 deletions

View File

@ -727,6 +727,8 @@ def file_access():
if 'timeframe' not in session: if 'timeframe' not in session:
session['timeframe'] = 'last24hours' session['timeframe'] = 'last24hours'
session['timeframe'] = request.args.get('timeframe', session['timeframe']) session['timeframe'] = request.args.get('timeframe', session['timeframe'])
if 'file_access_category' not in session:
session['file_access_category'] = None
now = datetime.now() now = datetime.now()
@ -775,25 +777,56 @@ def file_access():
'category': hf.extract_structure_from_string(rel_path)[0] 'category': hf.extract_structure_from_string(rel_path)[0]
}) })
# Get possible categories from the rows # Build a list of selectable categories for the dropdown
categories = sorted({r['category'] for r in rows if r['category'] is not None}) category_options = []
all_categories = [None] + categories has_uncategorized = any(r['category'] is None for r in rows)
top20 = [] if has_uncategorized:
for category in all_categories: category_options.append({
label = category if category is not None else 'Keine Kategorie gefunden !' 'value': 'uncategorized',
files = [r for r in rows if r['category'] == category][:20] 'label': 'Keine Kategorie gefunden !'
top20.append({
'category': label,
'files': files
}) })
unique_categories = sorted({r['category'] for r in rows if r['category'] is not None})
for category in unique_categories:
category_options.append({
'value': category,
'label': category
})
default_category_value = category_options[0]['value'] if category_options else None
requested_category = request.args.get('category')
stored_category = session.get('file_access_category')
selected_category = requested_category or stored_category or default_category_value
valid_values = {opt['value'] for opt in category_options}
if selected_category not in valid_values:
selected_category = default_category_value
session['file_access_category'] = selected_category
def matches_category(row):
if selected_category == 'uncategorized':
return row['category'] is None
return row['category'] == selected_category
filtered_rows = [r for r in rows if matches_category(r)] if selected_category is not None else []
top20_files = filtered_rows[:20]
selected_category_label = next(
(opt['label'] for opt in category_options if opt['value'] == selected_category),
'Keine Daten verfügbar'
)
title_short = app_config.get('TITLE_SHORT', 'Default Title') title_short = app_config.get('TITLE_SHORT', 'Default Title')
title_long = app_config.get('TITLE_LONG' , 'Default Title') title_long = app_config.get('TITLE_LONG' , 'Default Title')
return render_template( return render_template(
"file_access.html", "file_access.html",
timeframe=session['timeframe'], timeframe=session['timeframe'],
top20 = top20, categories=category_options,
selected_category=selected_category,
selected_category_label=selected_category_label,
top20_files=top20_files,
admin_enabled=auth.is_admin(), admin_enabled=auth.is_admin(),
title_short=title_short, title_short=title_short,
title_long=title_long title_long=title_long

4
app.py
View File

@ -1224,7 +1224,9 @@ def create_token(subpath):
token_qr_code=img_base64, token_qr_code=img_base64,
token_folder=token_item.get('folders'), token_folder=token_item.get('folders'),
token_url=url, token_url=url,
token_valid_to=token_item.get('validity', 'Unbekannt') token_valid_to=token_item.get('validity', 'Unbekannt'),
title_short=auth.return_app_config().get('TITLE_SHORT', ''),
title_long=auth.return_app_config().get('TITLE_LONG', '')
) )

View File

@ -1,44 +1,57 @@
/* Ensure html and body take full height */ /* Ensure html and body take full height */
html, body { html, body {
background-color: var(--light-background); background: var(--light-background);
color: var(--main-text-color); color: var(--main-text-color);
height: 100%; height: 100%;
margin: 0; margin: 0;
/* padding: 0; */
} }
/* Global Styles */ /* Global Styles */
body { body {
font-family: 'Helvetica Neue', Arial, sans-serif; font-family: 'Manrope', 'Segoe UI', sans-serif;
padding-top: 70px; /* Adjust to your header height */ padding-top: 0;
overflow-x: hidden; /* Prevent horizontal scroll */ overflow-x: hidden;
letter-spacing: 0.01em;
line-height: 1.6;
background:
radial-gradient(1200px 900px at -10% 10%, rgba(34, 211, 238, 0.12), transparent 40%),
radial-gradient(1100px 800px at 110% -20%, rgba(59, 130, 246, 0.14), transparent 45%),
var(--light-background);
} }
/* Header styles */ /* Header styles */
.site-header { .site-header {
background-color: var(--dark-background); background: linear-gradient(120deg, var(--dark-background), #13253f);
color: var(--header-text-color); color: var(--header-text-color);
position: fixed; position: sticky;
top: 0; top: 0;
width: 100%; width: 100%;
z-index: 1000; z-index: 2000;
display: flex; display: flex;
align-items: center; align-items: center;
gap: 10px;
padding: 10px 20px; padding: 10px 20px;
} }
.site-header img.logo { .site-header img.logo {
height: 50px; height: 54px;
margin-right: 15px; margin-right: 6px;
filter: drop-shadow(0 8px 10px rgba(0, 0, 0, 0.25));
} }
.site-header h1 { .site-header h1 {
font-size: 1.5em; font-size: 1.3rem;
margin: 0; margin: 0;
letter-spacing: 0.08em;
text-transform: uppercase;
font-weight: 800;
} }
.wrapper { .wrapper {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
min-height: 100%; min-height: 100%;
padding-bottom: 200px; padding-bottom: 220px;
} }
.wrapper > .admin-nav, .wrapper > .admin-nav,
@ -49,79 +62,179 @@ body {
.wrapper > main, .wrapper > main,
.wrapper > section { .wrapper > section {
flex: 1 1 auto; flex: 1 1 auto;
padding-top: 6px;
} }
.wrapper > footer { .wrapper > footer {
flex: 0 0 auto; flex: 0 0 auto;
} }
.container { .container {
max-width: 1200px; max-width: 1200px;
width: 90%; width: 95%;
margin: 0 auto;
padding: 20px 0;
} }
.container a { .container a {
font-size: 18px; font-size: 17px;
} }
.container-fluid { /* Carded content shell */
main.tab-content,
section.tab-content {
background: rgba(255, 255, 255, 0.92);
border: 1px solid var(--border-color);
box-shadow: none;
padding: 20px; padding: 20px;
margin-top: -1px;
}
/* Search view container (custom element) */
search {
display: block;
background: rgba(255, 255, 255, 0.92);
border: 1px solid var(--border-color);
border-radius: 0 0 16px 16px;
box-shadow: none;
margin-top: -1px;
padding: 16px 12px 10px;
position: relative;
}
/* Search view controls */
.search-close-btn {
position: absolute;
top: 12px;
right: 14px;
opacity: 0.85;
}
.input-clear-btn {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
opacity: 0.85;
}
.input-clear-btn:hover {
opacity: 1;
}
.search-close-btn:hover {
opacity: 1;
} }
/* Breadcrumb Styles */ /* Breadcrumb Styles */
.breadcrumb a { .breadcrumb {
font-size: 19px; display: flex;
text-decoration: none; align-items: center;
color: var(--main-text-color); gap: 8px;
flex-wrap: wrap;
padding: 10px 16px;
margin: 0 0 14px;
} }
.breadcrumb a {
font-size: 17px;
text-decoration: none;
color: var(--brand-navy);
font-weight: 700;
}
.breadcrumb span { .breadcrumb span {
font-size: 19px; font-size: 17px;
color: #ccc; color: var(--muted-text);
margin-left: 5px; margin: 0;
margin-right: 5px;
margin-bottom: 10px;
} }
/* Admin Nav Styles */ /* Admin Nav Styles */
.admin-nav { .admin-nav {
color: #ccc; color: var(--brand-ink);
border: 1px solid #ccc; border: 1px solid var(--border-color);
padding: 10px; background: rgba(255, 255, 255, 0.9);
background-color: #979797; margin-bottom: 12px;
width: 100vw;
margin-left: calc(50% - 50vw);
margin-right: calc(50% - 50vw);
box-sizing: border-box;
position: relative;
z-index: 1000;
overflow: visible;
}
.admin-nav-rail {
position: relative;
overflow: visible;
}
.admin-nav-track {
display: flex;
align-items: center;
flex-wrap: nowrap;
justify-content: flex-start;
padding: 0 10px; /* keep first/last items off the screen edge */
overflow-x: auto;
overflow-y: visible;
-webkit-overflow-scrolling: touch;
white-space: nowrap;
} }
.admin-nav a { .admin-nav a {
color: #ccc; color: var(--brand-ink);
text-decoration: none; text-decoration: none;
padding: 5px 0; padding: 8px 12px;
font-weight: 600;
font-size: 13px;
border: 1px solid transparent;
background: linear-gradient(135deg, #fff, #f4f7fb);
transition: all 0.2s ease;
box-shadow: 0 6px 16px rgba(15, 23, 42, 0.05);
flex-shrink: 0;
}
.admin-nav a:hover {
color: var(--brand-sky);
border-color: var(--border-color);
transform: translateY(-1px);
}
.admin-nav span {
color: var(--border-color);
display: none;
} }
.admin-nav .dropdown-toggle { .admin-nav .dropdown-toggle {
color: #ccc; color: var(--brand-ink);
text-decoration: none; padding: 8px 12px;
padding: 5px 0;
} }
.admin-nav .dropdown-toggle::after { .admin-nav .dropdown-toggle::after {
border-top-color: #ccc; display: none; /* hide default Bootstrap caret */
} }
.admin-nav .dropdown-menu { .admin-nav .dropdown-menu {
background-color: #f2f2f2; background-color: #fff;
border-color: #ccc; border-color: var(--border-color);
border-radius: 10px;
box-shadow: var(--card-shadow);
z-index: 4000;
position: absolute;
top: 0;
left: 100%;
margin-top: 0;
margin-left: 8px;
transform: none;
} }
.admin-nav .dropdown-item { .admin-nav .dropdown-item {
color: #333; color: var(--brand-ink);
padding-left: 14px; padding-left: 14px;
padding-right: 14px; padding-right: 14px;
} }
.admin-nav .dropdown-item:hover, .admin-nav .dropdown-item:hover,
.admin-nav .dropdown-item:focus { .admin-nav .dropdown-item:focus {
background-color: #e6e6e6; background-color: #eef4ff;
color: #000; color: var(--brand-navy);
} }
/* List Styles */ /* List Styles */
@ -138,18 +251,29 @@ div.directory-item a, li.directory-item a, li.file-item a, li.link-item a {
/* Directory Items (in a list) */ /* Directory Items (in a list) */
.directory-item, .link-item, .directory-item, .file-item { .directory-item, .link-item, .directory-item, .file-item {
margin-bottom: 10px; margin-bottom: 12px;
background-color: #fff; background-color: #fff;
border-radius: 5px; border-radius: 12px;
box-shadow: 0 1px 3px rgba(0,0,0,0.1); box-shadow: var(--card-shadow);
padding: 10px 15px; padding: 12px 18px;
border: 1px solid var(--border-color);
transition: all 0.2s ease;
}
.directory-item:hover,
.link-item:hover,
.file-item:hover {
border-color: var(--brand-sky);
box-shadow: 0 14px 26px rgba(59, 130, 246, 0.14);
transform: translateY(-1px);
} }
/* Grid Layout for Directories */ /* Grid Layout for Directories */
.directories-grid { .directories-grid {
display: grid; display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 0 10px; column-gap: 14px;
row-gap: 0;
text-align: center; text-align: center;
} }
@ -159,49 +283,58 @@ div.directory-item a, li.directory-item a, li.file-item a, li.link-item a {
grid-template-columns: 1fr auto; grid-template-columns: 1fr auto;
align-items: center; align-items: center;
background-color: #fff; background-color: #fff;
border-radius: 5px; border-radius: 12px;
box-shadow: 0 1px 3px rgba(0,0,0,0.1); box-shadow: var(--card-shadow);
} }
/* Link Styles */ /* Link Styles */
.directory-link, .directory-link,
.link-link, .link-link,
a.play-file { a.play-file {
color: var(--player-text-color); color: var(--brand-ink);
text-decoration: none; text-decoration: none;
word-break: break-all; word-break: break-all;
font-weight: 600;
} }
a.show-transcript { a.show-transcript {
text-decoration: none; text-decoration: none;
color: var(--main-text-color); color: var(--brand-navy);
font-size: 20px; font-size: 18px;
margin-left: 10px; margin-left: 10px;
font-weight: 600;
} }
a.show-transcript:hover { a.show-transcript:hover {
color: rgb(113, 146, 167); color: var(--brand-sky);
} }
.highlight { .highlight {
background-color: yellow; background-color: #fffbcc;
padding: 2px 4px;
border-radius: 4px;
} }
#transcriptContent code.timecode-link { #transcriptContent code.timecode-link {
cursor: pointer; cursor: pointer;
font-size: small; font-size: small;
} }
a.create-share { a.create-share {
text-decoration: none; text-decoration: none;
color: var(--main-text-color); color: var(--brand-navy);
font-size: 20px; font-size: 18px;
margin-left: 10px; margin-left: 10px;
font-weight: 600;
} }
a.create-share:hover { a.create-share:hover {
color: rgb(113, 146, 167); color: var(--brand-sky);
} }
.currently-playing { .currently-playing {
background-color: var(--selected-background); background-color: var(--selected-background);
border: 1px solid var(--brand-sky);
} }
/* Footer Player Styles */ /* Footer Player Styles */
@ -210,14 +343,16 @@ footer {
bottom: 0; bottom: 0;
left: 0; left: 0;
right: 0; right: 0;
background-color: var(--dark-background);
color: #fff;
padding: 7px;
text-align: center; text-align: center;
z-index: 1000; /* Make sure it sits on top */ z-index: 1000; /* Make sure it sits on top */
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
pointer-events: none; /* avoid blocking underlying content */
}
footer .audio-player-container {
pointer-events: auto; /* keep controls clickable */
} }
/* Modal Styles */ /* Modal Styles */
@ -235,21 +370,25 @@ footer {
#transcriptModal .modal-content { #transcriptModal .modal-content {
background-color: #fff; background-color: #fff;
margin: 5% auto; margin: 5% auto;
padding: 20px; padding: 15px;
border-radius: 5px; border-radius: 5px;
width: 90%; width: 90%;
max-width: 800px; max-width: 800px;
position: relative; position: relative;
max-height: 80vh; max-height: 80vh;
overflow-y: auto; overflow-y: auto;
display: flex;
flex-direction: column;
} }
#transcriptModal .close { #transcriptModal .close {
position: absolute; position: sticky;
right: 15px; top: 0;
top: 10px; right: 0;
font-size: 28px; font-size: 28px;
font-weight: bold; font-weight: bold;
cursor: pointer; cursor: pointer;
margin-left: auto;
z-index: 5;
} }
/* Basic Markdown Styles */ /* Basic Markdown Styles */
@ -313,9 +452,10 @@ footer {
.image-item { .image-item {
position: relative; position: relative;
overflow: hidden; overflow: hidden;
border: 2px solid #ccc; border: 1px solid var(--border-color);
border-radius: 8px; border-radius: 12px;
background: #f9f9f9; background: linear-gradient(145deg, #ffffff, #f6f8fc);
box-shadow: var(--card-shadow);
} }
/* the filename overlay, centered at bottom */ /* the filename overlay, centered at bottom */
@ -422,41 +562,60 @@ footer {
/* Tab Navigation Styles */ /* Tab Navigation Styles */
.main-tabs { .main-tabs {
background-color: var(--dark-background);
display: flex; display: flex;
justify-content: center; justify-content: flex-start;
padding: 0; padding: 6px 18px 0;
margin: 0; gap: 0;
gap: 2px; border-bottom: 1px solid var(--border-color);
border-bottom: 2px solid #444; flex-wrap: nowrap;
border-radius: 0;
width: 100vw;
margin-left: calc(50% - 50vw);
margin-right: calc(50% - 50vw);
box-sizing: border-box;
overflow-x: auto;
overflow-y: hidden;
-webkit-overflow-scrolling: touch;
scrollbar-width: thin;
} }
.tab-button { .tab-button {
background: #333; background: rgba(217, 217, 217, 0.5);
border: 1px solid #444; border: 1px solid var(--border-color);
border-bottom: none; border-bottom: none;
color: #999; color: var(--muted-text);
padding: 6px 20px; padding: 10px 16px;
font-size: 14px; font-size: 13px;
cursor: pointer; cursor: pointer;
transition: all 0.2s ease; transition: all 0.2s ease;
border-radius: 5px 5px 0 0; border-radius: 10px 10px 0 0;
margin: 0; margin: 0 6px 0 0;
position: relative; position: relative;
top: 2px; font-weight: 700;
letter-spacing: 0.04em;
text-transform: none;
box-shadow:
0 -10px 18px -12px rgba(15, 23, 42, 0.18),
10px 0 18px -12px rgba(15, 23, 42, 0.12),
-10px 0 18px -12px rgba(15, 23, 42, 0.12);
flex: 0 0 auto;
} }
.tab-button:hover { .tab-button:hover {
color: #fff; color: var(--brand-ink);
background: #3a3a3a; background: rgba(255, 255, 255, 0.92);
} }
.tab-button.active { .tab-button.active {
color: var(--main-text-color, #000); color: var(--brand-ink);
background: var(--light-background); background: rgba(255, 255, 255, 0.92);
border-color: #444; border-color: var(--border-color);
border-bottom-color: var(--light-background); z-index: 2;
z-index: 1; box-shadow:
0 -12px 22px -12px rgba(15, 23, 42, 0.22),
12px 0 20px -14px rgba(15, 23, 42, 0.14),
-12px 0 20px -14px rgba(15, 23, 42, 0.14);
margin-bottom: -1px;
} }
.tab-content { .tab-content {
@ -471,27 +630,28 @@ footer {
.messages-section-header { .messages-section-header {
max-width: 900px; max-width: 900px;
margin: 0 auto; margin: 0 auto;
padding: 0 0 8px; padding: 0 0 10px;
} }
.messages-container { .messages-container {
max-width: 900px; max-width: 900px;
margin: 0 auto; margin: 0 auto;
padding: 20px 0; padding: 10px 0 24px;
} }
.message-card { .message-card {
background: var(--card-background, #fff); background: linear-gradient(145deg, #ffffff, #f7f9ff);
border: 1px solid #ddd; border: 1px solid var(--border-color);
border-radius: 8px; border-radius: 14px;
padding: 20px; padding: 20px 22px;
margin-bottom: 20px; margin-bottom: 20px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1); box-shadow: var(--card-shadow);
transition: box-shadow 0.3s ease; transition: box-shadow 0.3s ease, transform 0.2s ease;
} }
.message-card:hover { .message-card:hover {
box-shadow: 0 4px 12px rgba(0,0,0,0.15); box-shadow: 0 20px 38px rgba(15, 23, 42, 0.16);
transform: translateY(-2px);
} }
.message-header { .message-header {
@ -500,18 +660,18 @@ footer {
align-items: center; align-items: center;
margin-bottom: 15px; margin-bottom: 15px;
padding-bottom: 10px; padding-bottom: 10px;
border-bottom: 2px solid #e0e0e0; border-bottom: 1px dashed var(--border-color);
} }
.message-title { .message-title {
font-size: 1.5em; font-size: 1.5em;
font-weight: bold; font-weight: bold;
color: var(--main-text-color); color: var(--brand-ink);
margin: 0; margin: 0;
} }
.message-datetime { .message-datetime {
color: #666; color: var(--muted-text);
font-size: 0.9em; font-size: 0.9em;
font-style: italic; font-style: italic;
} }
@ -527,9 +687,9 @@ footer {
gap: 6px; gap: 6px;
padding: 8px 16px; padding: 8px 16px;
margin: 4px 0; margin: 4px 0;
background: #f0f4f8; background: #ecf6ff;
color: #495057; color: #0f172a;
border: 1px solid #d1dce5; border: 1px solid #bed8ff;
border-radius: 6px; border-radius: 6px;
font-size: 0.95em; font-size: 0.95em;
font-weight: 500; font-weight: 500;
@ -539,9 +699,9 @@ footer {
} }
.message-content .folder-link-btn:hover { .message-content .folder-link-btn:hover {
background: #e3eaf0; background: #dff0ff;
border-color: #b8c5d0; border-color: #93c5fd;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.12); box-shadow: 0 6px 18px rgba(14, 165, 233, 0.24);
} }
.message-content .folder-link-btn:active { .message-content .folder-link-btn:active {
@ -554,13 +714,13 @@ footer {
} }
.message-content a { .message-content a {
color: #007bff; color: var(--brand-sky);
text-decoration: underline; text-decoration: underline;
cursor: pointer; cursor: pointer;
} }
.message-content a:hover { .message-content a:hover {
color: #0056b3; color: #0ea5e9;
text-decoration: underline; text-decoration: underline;
} }
@ -620,10 +780,12 @@ footer {
} }
.message-content blockquote { .message-content blockquote {
border-left: 4px solid #ccc; border-left: 4px solid var(--border-color);
padding-left: 15px; padding-left: 15px;
color: #666; color: var(--muted-text);
font-style: italic; font-style: italic;
background: #f8fbff;
border-radius: 6px;
} }
.message-image { .message-image {
@ -638,7 +800,7 @@ footer {
.message-actions { .message-actions {
margin-top: 15px; margin-top: 15px;
padding-top: 10px; padding-top: 10px;
border-top: 1px solid #e0e0e0; border-top: 1px solid var(--border-color);
display: flex; display: flex;
gap: 10px; gap: 10px;
} }
@ -654,12 +816,12 @@ footer {
} }
.btn-edit { .btn-edit {
background-color: #007bff; background: linear-gradient(135deg, #2563eb, #22d3ee);
color: white; color: white;
} }
.btn-edit:hover { .btn-edit:hover {
background-color: #0056b3; background: linear-gradient(135deg, #1d4ed8, #0ea5e9);
} }
.btn-delete { .btn-delete {
@ -674,7 +836,7 @@ footer {
.no-messages { .no-messages {
text-align: center; text-align: center;
padding: 40px; padding: 40px;
color: #999; color: var(--muted-text);
font-size: 1.2em; font-size: 1.2em;
} }
@ -690,9 +852,9 @@ footer {
display: grid; display: grid;
grid-template-columns: repeat(1, minmax(0, 1fr)); grid-template-columns: repeat(1, minmax(0, 1fr));
gap: 0; gap: 0;
border: 1px solid #ddd; border: 1px solid var(--border-color);
border-top: 0; border-top: 0;
border-radius: 0 0 6px 6px; border-radius: 0 0 10px 10px;
overflow: visible; overflow: visible;
} }
@ -701,9 +863,9 @@ footer {
} }
.calendar-day { .calendar-day {
background: var(--card-background, #fff); background: #ffffff;
border-bottom: 1px solid #ddd; border-bottom: 1px solid var(--border-color);
padding: 8px 10px 4px; padding: 12px 12px 6px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
min-width: 0; min-width: 0;
@ -713,19 +875,19 @@ footer {
} }
.calendar-day-today { .calendar-day-today {
background: #f7fbff; background: linear-gradient(135deg, #f6fbff, #ecf4ff);
box-shadow: inset 0 0 0 2px #4da3ff; box-shadow: inset 0 0 0 2px #3b82f6;
border-color: #4da3ff; border-color: #3b82f6;
} }
.calendar-day-sunday, .calendar-day-sunday,
.calendar-day-holiday { .calendar-day-holiday {
background: #fff7f7; background: #fff8f5;
} }
.calendar-day-today.calendar-day-sunday, .calendar-day-today.calendar-day-sunday,
.calendar-day-today.calendar-day-holiday { .calendar-day-today.calendar-day-holiday {
background: linear-gradient(135deg, #f7fbff 0%, #f7fbff 60%, #fff2f2 100%); background: linear-gradient(135deg, #f6fbff 0%, #f6fbff 60%, #fff1ed 100%);
} }
.calendar-day-sunday .calendar-day-header, .calendar-day-sunday .calendar-day-header,
@ -757,7 +919,7 @@ footer {
@media (min-width: 992px) { @media (min-width: 992px) {
.calendar-grid { .calendar-grid {
grid-template-columns: repeat(7, minmax(0, 1fr)); grid-template-columns: repeat(7, minmax(0, 1fr));
border-radius: 0 0 8px 8px; border-radius: 0 0 12px 12px;
} }
.calendar-weekday-header { .calendar-weekday-header {
@ -765,21 +927,21 @@ footer {
grid-template-columns: repeat(7, minmax(0, 1fr)); grid-template-columns: repeat(7, minmax(0, 1fr));
text-align: center; text-align: center;
font-weight: 600; font-weight: 600;
color: #999; color: var(--muted-text);
border: 1px solid #ddd; border: 1px solid var(--border-color);
border-bottom: 0; border-bottom: 0;
border-radius: 8px 8px 0 0; border-radius: 12px 12px 0 0;
overflow: visible; overflow: visible;
position: sticky; position: sticky;
top: 70px; top: 70px;
z-index: 900; z-index: 900;
background: var(--card-background, #fff); background: var(--card-background, #fff);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.04); box-shadow: 0 10px 28px rgba(0, 0, 0, 0.05);
} }
.calendar-day { .calendar-day {
border-bottom: 1px solid #ddd; border-bottom: 1px solid var(--border-color);
border-right: 1px solid #ddd; border-right: 1px solid var(--border-color);
} }
.calendar-day:nth-child(7n) { .calendar-day:nth-child(7n) {
@ -795,17 +957,17 @@ footer {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
margin-bottom: 6px; margin-bottom: 8px;
gap: 8px; gap: 8px;
} }
.calendar-day-name { .calendar-day-name {
font-weight: 600; font-weight: 700;
text-transform: capitalize; text-transform: capitalize;
} }
.calendar-day-date { .calendar-day-date {
color: #666; color: var(--muted-text);
font-size: 0.95rem; font-size: 0.95rem;
white-space: nowrap; white-space: nowrap;
} }
@ -817,9 +979,9 @@ footer {
} }
.calendar-day thead th { .calendar-day thead th {
background: #f1f1f1; background: #f6f8fb;
color: #666; color: var(--muted-text);
border-color: #ddd; border-color: var(--border-color);
} }
.calendar-day.empty thead { .calendar-day.empty thead {
@ -851,7 +1013,7 @@ footer {
.details-indicator { .details-indicator {
font-weight: 700; font-weight: 700;
color: #666; color: var(--brand-sky);
margin-left: 6px; margin-left: 6px;
cursor: pointer; cursor: pointer;
position: relative; position: relative;
@ -869,7 +1031,7 @@ footer {
} }
.calendar-details-row td { .calendar-details-row td {
background: #f8f9fa; background: #f8fbff;
} }
.calendar-inline-actions { .calendar-inline-actions {
@ -882,8 +1044,10 @@ footer {
.file-browser-content { .file-browser-content {
max-height: 400px; max-height: 400px;
overflow-y: auto; overflow-y: auto;
border: 1px solid #ddd; border: 1px solid var(--border-color);
border-radius: 5px; border-radius: 10px;
background: #fff;
box-shadow: var(--card-shadow);
} }
.file-browser-content .list-group-item { .file-browser-content .list-group-item {
@ -906,15 +1070,98 @@ footer {
} }
.file-browser-panel { .file-browser-panel {
background-color: #f5f5f5; background-color: #f8fbff;
border: 1px solid #ddd; border: 1px solid var(--border-color);
border-radius: 5px; border-radius: 12px;
padding: 15px; padding: 16px;
margin-bottom: 10px; margin-bottom: 10px;
} }
.file-browser-panel h6 { .file-browser-panel h6 {
margin-top: 0; margin-top: 0;
margin-bottom: 10px; margin-bottom: 10px;
color: #333; color: var(--brand-ink);
}
/* Buttons - harmonize with dark blue palette */
.btn {
border-radius: 10px;
font-weight: 700;
letter-spacing: 0.01em;
}
.btn-primary {
background: linear-gradient(135deg, var(--brand-navy), var(--dark-background));
border: 1px solid var(--brand-navy);
color: #e5e7eb;
box-shadow: 0 6px 14px rgba(15, 23, 42, 0.18);
}
.btn-primary:hover {
background: linear-gradient(135deg, #1b2a44, #0d1524);
border-color: #1b2a44;
box-shadow: 0 8px 18px rgba(15, 23, 42, 0.2);
}
.btn-primary:active,
.btn-primary:focus {
background: linear-gradient(135deg, #0d1524, #0a1020);
border-color: #0d1524;
box-shadow: 0 4px 10px rgba(15, 23, 42, 0.22);
color: #e5e7eb;
}
.btn-secondary {
background: linear-gradient(135deg, #1f2e4a, #0f172a);
border: 1px solid #1f2e4a;
color: #e5e7eb;
box-shadow: 0 6px 14px rgba(15, 23, 42, 0.18);
}
.btn-secondary:hover,
.btn-secondary:focus {
background: linear-gradient(135deg, #223456, #111a2f);
border-color: #223456;
box-shadow: 0 8px 16px rgba(15, 23, 42, 0.2);
}
.btn-secondary:active {
background: linear-gradient(135deg, #0c1220, #0a0f1a);
border-color: #0c1220;
box-shadow: 0 4px 10px rgba(15, 23, 42, 0.22);
}
.btn-outline-secondary {
border: 1px solid var(--brand-navy);
color: var(--brand-navy);
background: #fff;
box-shadow: 0 2px 8px rgba(15, 23, 42, 0.08);
}
.btn-outline-secondary:hover,
.btn-outline-secondary:focus {
background: linear-gradient(135deg, #f7f9fd, #eef2f9);
color: var(--brand-navy);
border-color: var(--brand-navy);
box-shadow: 0 4px 12px rgba(15, 23, 42, 0.12);
}
.btn-outline-secondary:active {
background: #e5eaf5;
color: var(--brand-navy);
border-color: var(--brand-navy);
box-shadow: 0 2px 8px rgba(15, 23, 42, 0.12);
}
.btn-light {
background: linear-gradient(135deg, #e8ecf4, #dfe6f3);
border: 1px solid var(--border-color);
color: var(--brand-ink);
box-shadow: 0 4px 10px rgba(15, 23, 42, 0.08);
}
.btn-light:hover,
.btn-light:focus {
background: linear-gradient(135deg, #dfe5f1, #d7dfef);
color: var(--brand-ink);
border-color: var(--brand-navy);
} }

View File

@ -252,9 +252,9 @@ function renderContent(data) {
contentHTML += '<div class="directories-grid">'; contentHTML += '<div class="directories-grid">';
data.directories.forEach(dir => { data.directories.forEach(dir => {
if (admin_enabled && data.breadcrumbs.length != 1 && dir.share) { if (admin_enabled && data.breadcrumbs.length != 1 && dir.share) {
share_link = `<a href="#" class="create-share" data-url="${dir.path}">⚙️</a>`; share_link = `<a href="#" class="create-share" data-url="${dir.path}"><i class="bi bi-gear"></i></a>`;
} }
contentHTML += `<div class="directory-item"><a href="#" class="directory-link" data-path="${dir.path}">📁 ${dir.name}</a> contentHTML += `<div class="directory-item"><a href="#" class="directory-link" data-path="${dir.path}"><i class="bi bi-folder"></i> ${dir.name}</a>
${share_link} ${share_link}
</div>`; </div>`;
}); });
@ -262,22 +262,22 @@ function renderContent(data) {
} else { } else {
contentHTML += '<ul>'; contentHTML += '<ul>';
if (data.breadcrumbs.length === 1 && Array.isArray(data.folder_today) && data.folder_today.length > 0) { if (data.breadcrumbs.length === 1 && Array.isArray(data.folder_today) && data.folder_today.length > 0) {
contentHTML += `<li class="directory-item"><a href="#" class="directory-link" data-path="heute">📅 Heute</a></li>`; contentHTML += `<li class="directory-item"><a href="#" class="directory-link" data-path="heute"><i class="bi bi-calendar2-day"></i> Heute</a></li>`;
} else if (data.breadcrumbs.length === 1 && Array.isArray(data.folder_yesterday) && data.folder_yesterday.length > 0) { } else if (data.breadcrumbs.length === 1 && Array.isArray(data.folder_yesterday) && data.folder_yesterday.length > 0) {
contentHTML += `<li class="directory-item"><a href="#" class="directory-link" data-path="gestern">📅 Gestern</a></li>`; contentHTML += `<li class="directory-item"><a href="#" class="directory-link" data-path="gestern"><i class="bi bi-calendar2-day"></i> Gestern</a></li>`;
} }
if (data.breadcrumbs.length === 1 && data.toplist_enabled) { if (data.breadcrumbs.length === 1 && data.toplist_enabled) {
contentHTML += `<li class="directory-item"><a href="#" class="directory-link" data-path="toplist">🔥 oft angehört</a></li>`; contentHTML += `<li class="directory-item"><a href="#" class="directory-link" data-path="toplist"><i class="bi bi-graph-up"></i> oft angehört</a></li>`;
} }
data.directories.forEach(dir => { data.directories.forEach(dir => {
if (admin_enabled && data.breadcrumbs.length != 1 && dir.share) { if (admin_enabled && data.breadcrumbs.length != 1 && dir.share) {
share_link = `<a href="#" class="create-share" data-url="${dir.path}">⚙️</a>`; share_link = `<a href="#" class="create-share" data-url="${dir.path}"><i class="bi bi-gear"></i></a>`;
} }
if (dir.path.includes('toplist')) { if (dir.path.includes('toplist')) {
link_symbol = ''; link_symbol = '<i class="bi bi-star"></i>';
} else { } else {
link_symbol = '📁'; link_symbol = '<i class="bi bi-folder"></i>';
} }
contentHTML += `<li class="directory-item"><a href="#" class="directory-link" data-path="${dir.path}">${link_symbol} ${dir.name}</a>${share_link}</li>`; contentHTML += `<li class="directory-item"><a href="#" class="directory-link" data-path="${dir.path}">${link_symbol} ${dir.name}</a>${share_link}</li>`;
}); });
@ -286,7 +286,7 @@ function renderContent(data) {
} }
// Add search link at top level above directories/files // Add search link at top level above directories/files
if (data.breadcrumbs.length === 1) { if (data.breadcrumbs.length === 1) {
contentHTML = `<ul><li class="link-item" onclick="viewSearch()"><a onclick="viewSearch()" class="link-link">🔎 Suche</a></li></ul>` + contentHTML; contentHTML = `<ul><li class="link-item" onclick="viewSearch()"><a onclick="viewSearch()" class="link-link"><i class="bi bi-search"></i> Suche</a></li></ul>` + contentHTML;
} }
// Render files (including music and non-image files) // Render files (including music and non-image files)
@ -294,18 +294,18 @@ function renderContent(data) {
if (nonImageFiles.length > 0) { if (nonImageFiles.length > 0) {
contentHTML += '<ul>'; contentHTML += '<ul>';
nonImageFiles.forEach((file, idx) => { nonImageFiles.forEach((file, idx) => {
let symbol = '📄'; let symbol = '<i class="bi bi-file-earmark"></i>';
if (file.file_type === 'music') { if (file.file_type === 'music') {
symbol = '🔊'; symbol = '<i class="bi bi-volume-up"></i>';
currentMusicFiles.push({ path: file.path, index: idx, title: file.name.replace('.mp3', '') }); currentMusicFiles.push({ path: file.path, index: idx, title: file.name.replace('.mp3', '') });
} else if (file.file_type === 'image') { } else if (file.file_type === 'image') {
symbol = '🖼️'; symbol = '<i class="bi bi-image"></i>';
} }
const indexAttr = file.file_type === 'music' ? ` data-index="${currentMusicFiles.length - 1}"` : ''; const indexAttr = file.file_type === 'music' ? ` data-index="${currentMusicFiles.length - 1}"` : '';
contentHTML += `<li class="file-item"> contentHTML += `<li class="file-item">
<a href="#" class="play-file"${indexAttr} data-url="${file.path}" data-file-type="${file.file_type}">${symbol} ${file.name.replace('.mp3', '')}</a>`; <a href="#" class="play-file"${indexAttr} data-url="${file.path}" data-file-type="${file.file_type}">${symbol} ${file.name.replace('.mp3', '')}</a>`;
if (file.has_transcript) { if (file.has_transcript) {
contentHTML += `<a href="#" class="show-transcript" data-url="${file.transcript_url}" data-audio-url="${file.path}" title="Show Transcript">&#128196;</a>`; contentHTML += `<a href="#" class="show-transcript" data-url="${file.transcript_url}" data-audio-url="${file.path}" title="Show Transcript"><i class="bi bi-journal-text"></i></a>`;
} }
contentHTML += `</li>`; contentHTML += `</li>`;
}); });

View File

@ -1,23 +1,26 @@
.audio-player-container { .audio-player-container {
background-color: #fff; background: linear-gradient(135deg, #f9fbff, #eef4ff);
border-radius: 8px; border-radius: 14px;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1); box-shadow: 0 12px 26px rgba(15, 23, 42, 0.12);
padding: 7px; border: 8px solid #0b1220;
width: 100%; padding: 12px 12px;
width: min(600px, 100%);
display: none; display: none;
color: #1f2937;
margin-left: 8px;
margin-right: 8px;
} }
/* Now Playing Info */ /* Now Playing Info */
.now-playing-info { .now-playing-info {
margin-top: 5px; margin-top: 5px;
font-size: 16px; font-size: 15px;
color: var(--main-text-color); color: #1f2937;
text-align: center; text-align: center;
} }
.audio-player { .audio-player {
--player-button-width: 4em; --control-size: 3.4em;
--sound-button-width: 3em;
--space: 0.5em; --space: 0.5em;
} }
@ -26,6 +29,7 @@
align-items: center; align-items: center;
width: 100%; width: 100%;
gap: var(--space); /* ensures spacing between controls */ gap: var(--space); /* ensures spacing between controls */
color: #1f2937;
} }
/* Make the slider container fill the available space and stack its children vertically */ /* Make the slider container fill the available space and stack its children vertically */
@ -40,10 +44,10 @@
-webkit-appearance: none; -webkit-appearance: none;
width: 100%; width: 100%;
height: 0.5em; height: 0.5em;
background-color: #ccc; background-color: #e6edf7;
border-radius: 5px; border-radius: 5px;
background-size: 0% 100%; background-size: 0% 100%;
background-image: linear-gradient(var(--dark-background), var(--dark-background)); background-image: linear-gradient(90deg, #0b1220, #0f172a);
background-repeat: no-repeat; background-repeat: no-repeat;
appearance: none; appearance: none;
outline: none; outline: none;
@ -56,9 +60,9 @@
width: 1em; width: 1em;
height: 1em; height: 1em;
border-radius: 50%; border-radius: 50%;
background-color: var(--dark-background); background-color: #0f172a;
cursor: pointer; cursor: pointer;
border: none; border: 2px solid #0b1220;
outline: none; outline: none;
} }
@ -67,9 +71,9 @@
width: 1em; width: 1em;
height: 1em; height: 1em;
border-radius: 50%; border-radius: 50%;
background-color: var(--dark-background); background-color: #0f172a;
cursor: pointer; cursor: pointer;
border: none; border: 2px solid #0b1220;
outline: none; outline: none;
} }
@ -78,9 +82,9 @@
width: 1em; width: 1em;
height: 1em; height: 1em;
border-radius: 50%; border-radius: 50%;
background-color: var(--dark-background); background-color: #0f172a;
cursor: pointer; cursor: pointer;
border: none; border: 2px solid #0b1220;
outline: none; outline: none;
} }
@ -104,18 +108,55 @@
.player-button, .player-button,
.sound-button { .sound-button {
background-color: transparent; width: var(--control-size);
border: 0; height: var(--control-size);
background: transparent;
border: none;
border-radius: 50%;
box-shadow: none;
cursor: pointer; cursor: pointer;
display: inline-flex;
align-items: center;
justify-content: center;
transition: background-color 0.15s ease, transform 0.1s ease;
padding: 0; padding: 0;
line-height: 1;
overflow: visible;
} }
.player-button { .player-button:hover,
width: var(--player-button-width); .sound-button:hover {
height: var(--player-button-width); background: rgba(15, 23, 42, 0.08);
transform: translateY(-1px);
} }
.sound-button { .player-button:active,
width: var(--sound-button-width); .sound-button:active {
height: var(--sound-button-width); transform: translateY(0);
background: rgba(15, 23, 42, 0.12);
}
.player-button:focus-visible,
.sound-button:focus-visible {
outline: 2px solid rgba(20, 33, 61, 0.85);
outline-offset: 2px;
}
.player-button svg,
.sound-button svg {
width: var(--control-size);
height: var(--control-size);
display: block;
pointer-events: none;
}
.player-button svg path,
.sound-button svg path {
fill: var(--dark-background);
}
.timeline::-webkit-slider-thumb,
.timeline::-moz-range-thumb,
.timeline::-ms-thumb {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.18);
} }

View File

@ -26,3 +26,44 @@ function toClipboard(url) {
document.body.removeChild(textarea); document.body.removeChild(textarea);
} }
} }
function printToken() {
const printable = document.getElementById('tokenPrintable');
if (!printable) {
console.error('Token printable content not found.');
return;
}
const clone = printable.cloneNode(true);
clone.querySelectorAll('.token-action-buttons').forEach(el => el.remove());
const styles = Array.from(document.querySelectorAll('link[rel="stylesheet"], style'))
.map(node => node.outerHTML)
.join('\n');
const popup = window.open('', '_blank', 'width=900,height=1200');
if (!popup) {
alert('Bitte Popups erlauben, um das PDF zu speichern.');
return;
}
popup.document.write(`<!doctype html>
<html>
<head>
<title>Token PDF</title>
${styles}
<style>
body { padding: 24px; }
.token-action-buttons { display: none !important; }
.card { box-shadow: none !important; }
</style>
</head>
<body>${clone.outerHTML}</body>
</html>`);
popup.document.close();
popup.focus();
popup.onload = () => {
popup.print();
popup.close();
};
}

View File

@ -417,7 +417,7 @@ function setAutoPlay(active) {
const playButton = document.getElementById('gallery-play'); const playButton = document.getElementById('gallery-play');
isAutoPlay = active; isAutoPlay = active;
if (playButton) { if (playButton) {
playButton.textContent = active ? '⏸' : '▶'; playButton.textContent = active ? '||' : '>';
playButton.setAttribute('aria-pressed', active ? 'true' : 'false'); playButton.setAttribute('aria-pressed', active ? 'true' : 'false');
playButton.classList.toggle('playing', active); playButton.classList.toggle('playing', active);
} }

View File

@ -1,6 +1,66 @@
document.addEventListener('DOMContentLoaded', function() { function wireAdminDropdowns() {
// (No per-icon setup needed here—just ensure your CSS is in place.) const reposition = (toggle, menu) => {
const rect = toggle.getBoundingClientRect();
const gutter = 8;
const menuWidth = menu.offsetWidth || 220;
const menuHeight = menu.offsetHeight || 0;
// Prefer placing the menu to the right of the toggle; clamp within viewport.
let left = rect.right + gutter;
if (left + menuWidth > window.innerWidth - gutter) {
left = Math.max(gutter, window.innerWidth - menuWidth - gutter);
}
let top = Math.max(gutter, Math.min(rect.top, window.innerHeight - menuHeight - gutter));
menu.style.position = 'fixed';
menu.style.transform = 'none';
menu.style.top = `${top}px`;
menu.style.left = `${left}px`;
menu.style.right = 'auto';
menu.style.minWidth = `${Math.max(rect.width, 160)}px`;
menu.style.maxWidth = '260px';
menu.style.display = 'block';
menu.dataset.dropdownOpen = '1';
menu.classList.add('show');
};
const resetMenu = (menu) => {
menu.style.position = '';
menu.style.transform = '';
menu.style.top = '';
menu.style.left = '';
menu.style.right = '';
menu.style.minWidth = '';
menu.style.maxWidth = '';
menu.style.display = '';
menu.classList.remove('show');
delete menu.dataset.dropdownOpen;
};
document.querySelectorAll('.admin-nav .dropdown-toggle').forEach((toggle) => {
if (toggle.dataset.dropdownWired) return;
toggle.dataset.dropdownWired = '1';
const menu = toggle.nextElementSibling;
if (!menu) return;
toggle.addEventListener('shown.bs.dropdown', () => reposition(toggle, menu));
toggle.addEventListener('hide.bs.dropdown', () => resetMenu(menu));
window.addEventListener('scroll', () => {
if (menu.dataset.dropdownOpen) reposition(toggle, menu);
}, { passive: true });
window.addEventListener('resize', () => {
if (menu.dataset.dropdownOpen) reposition(toggle, menu);
}); });
});
}
if (document.readyState === 'complete' || document.readyState === 'interactive') {
wireAdminDropdowns();
} else {
document.addEventListener('DOMContentLoaded', wireAdminDropdowns);
}
// 1) Delegate clicks on any .toggle-icon, now or in the future: // 1) Delegate clicks on any .toggle-icon, now or in the future:
document.addEventListener('click', function(e) { document.addEventListener('click', function(e) {

View File

@ -14,11 +14,11 @@ document.addEventListener('DOMContentLoaded', function() {
card.className = 'card'; card.className = 'card';
card.innerHTML = ` card.innerHTML = `
<div class="card-body"> <div class="card-body">
<p><button class="btn btn-light" onclick="player.loadTrack('${file.relative_path}')" style="width:100%;">🔊 ${filenameWithoutExtension}</button></p> <p><button class="btn btn-light" onclick="player.loadTrack('${file.relative_path}')" style="width:100%;"><i class="bi bi-volume-up"></i> ${filenameWithoutExtension}</button></p>
<p><button onclick="window.open('/path/${file.relative_path}', '_self');" class="btn btn-light btn-sm" style="width:100%;">📁 ${parentFolder}</button></p> <p><button onclick="window.open('/path/${file.relative_path}', '_self');" class="btn btn-light btn-sm" style="width:100%;"><i class="bi bi-folder"></i> ${parentFolder}</button></p>
<p class="card-text">Anzahl Downloads: ${file.hitcount}</p> <p class="card-text">Anzahl Downloads: ${file.hitcount}</p>
${ file.performance_date !== undefined ? `<p class="card-text">Datum: ${file.performance_date}</p>` : ``} ${ file.performance_date !== undefined ? `<p class="card-text">Datum: ${file.performance_date}</p>` : ``}
${ file.transcript_hits !== undefined ? `<p class="card-text">Treffer im Transkript: ${file.transcript_hits} <a href="#" class="show-transcript" data-url="${transcriptURL}" data-audio-url="${file.relative_path}" highlight="${file.query}">&#128196;</a></p>` : ``} ${ file.transcript_hits !== undefined ? `<p class="card-text">Treffer im Transkript: ${file.transcript_hits} <a href="#" class="show-transcript" data-url="${transcriptURL}" data-audio-url="${file.relative_path}" highlight="${file.query}"><i class="bi bi-journal-text"></i></a></p>` : ``}
</div> </div>
`; `;
resultsDiv.appendChild(card); resultsDiv.appendChild(card);
@ -66,6 +66,9 @@ document.addEventListener('DOMContentLoaded', function() {
e.preventDefault(); e.preventDefault();
const query = document.getElementById('query').value.trim(); const query = document.getElementById('query').value.trim();
const includeTranscript = document.getElementById('includeTranscript').checked; const includeTranscript = document.getElementById('includeTranscript').checked;
const spinnerTimer = setTimeout(() => {
if (typeof showSpinner === 'function') showSpinner();
}, 200);
// Get the selected category radio button, if any // Get the selected category radio button, if any
const categoryRadio = document.querySelector('input[name="category"]:checked'); const categoryRadio = document.querySelector('input[name="category"]:checked');
@ -96,7 +99,12 @@ document.addEventListener('DOMContentLoaded', function() {
formData.append('dateto', document.getElementById('dateto').value); formData.append('dateto', document.getElementById('dateto').value);
formData.append('includeTranscript', includeTranscript); formData.append('includeTranscript', includeTranscript);
fetch('/searchcommand', { const settleSpinner = () => {
clearTimeout(spinnerTimer);
if (typeof hideSpinner === 'function') hideSpinner();
};
const fetchPromise = fetch('/searchcommand', {
method: 'POST', method: 'POST',
body: formData body: formData
}) })
@ -118,6 +126,12 @@ document.addEventListener('DOMContentLoaded', function() {
.catch(error => { .catch(error => {
console.error('Error:', error); console.error('Error:', error);
}); });
// Always clear/hide spinner once the request settles
if (typeof fetchPromise.finally === 'function') {
fetchPromise.finally(settleSpinner);
} else {
fetchPromise.then(settleSpinner, settleSpinner);
}
}); });
// Clear button event handler // Clear button event handler

80
static/token.css Normal file
View File

@ -0,0 +1,80 @@
.token-container {
height: 100%;
display: flex;
flex-direction: column;
}
#tokenPrintable .qr-code {
max-width: 420px;
width: 100%;
display: block;
margin: 0 auto;
}
#tokenPrintable .token-qr-wrap {
position: relative;
display: inline-block;
}
#tokenPrintable .token-pdf-logo {
max-width: 120px;
width: 28%;
min-width: 80px;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
margin: 0;
z-index: 2;
pointer-events: none;
filter: drop-shadow(0 0 8px rgba(0, 0, 0, 0.8)) drop-shadow(0 0 14px rgba(0, 0, 0, 0.6));
}
#tokenPrintable .token-folder-title {
text-align: center;
}
#tokenPrintable .token-page-title {
text-align: center;
margin-bottom: 3rem;
margin-top: 1rem;
}
#tokenPrintable .copy-btn {
display: inline-flex;
align-items: center;
padding: 0.5rem 1rem;
font-size: 1rem;
cursor: pointer;
border: 1px solid #007bff;
background-color: #007bff;
color: white;
border-radius: 0.25rem;
}
#tokenPrintable .copy-btn:active {
background-color: #0056b3;
border-color: #0056b3;
}
@media print {
.token-action-buttons {
display: none !important;
}
#tokenPrintable .qr-code {
max-width: 620px;
width: 90% !important;
}
#tokenPrintable .token-pdf-logo {
max-width: 140px;
width: 24%;
}
#tokenPrintable .token-qr-wrap {
position: relative;
display: inline-block;
}
}

View File

@ -1,77 +1,29 @@
<!DOCTYPE html> {% extends 'base.html' %}
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<title>{{ title_short }}</title> {% block title %}{{ title_short }}{% endblock %}
<meta property="og:title" content="{{ og_title }}" /> {% block head_extra %}
<meta property="og:description" content="{{ og_description }}" /> <meta property="og:title" content="{{ og_title }}">
<meta property="og:image" content="/icon/logo-192x192.png" /> <meta property="og:description" content="{{ og_description }}">
<meta property="og:type" content="website" /> <meta property="og:image" content="/icon/logo-192x192.png">
<meta property="og:type" content="website">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<meta name="description" content="... uns aber, die wir gerettet werden, ist es eine Gotteskraft."> <meta name="description" content="... uns aber, die wir gerettet werden, ist es eine Gotteskraft.">
<meta name="author" content="{{ title_short }}"> <meta name="author" content="{{ title_short }}">
<link rel="icon" href="/icon/logo-192x192.png" type="image/png" sizes="192x192"> <link rel="icon" href="/icon/logo-192x192.png" type="image/png" sizes="192x192">
<!-- Web App Manifest -->
<link rel="manifest" href="{{ url_for('static', filename='manifest.json') }}"> <link rel="manifest" href="{{ url_for('static', filename='manifest.json') }}">
<!-- Android Theme Color -->
<meta name="theme-color" content="#000">
<!-- Apple-specific tags -->
<link rel="touch-icon" href="/icon/logo-192x192.png"> <link rel="touch-icon" href="/icon/logo-192x192.png">
<meta name="mobile-web-app-capable" content="yes"> <meta name="mobile-web-app-capable" content="yes">
<meta name="mobile-web-app-status-bar-style" content="default"> <meta name="mobile-web-app-status-bar-style" content="default">
<meta name="mobile-web-app-title" content="Gottesdienste"> <meta name="mobile-web-app-title" content="Gottesdienste">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet" />
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.5/font/bootstrap-icons.css" rel="stylesheet">
<!-- Your CSS -->
<link rel="stylesheet" href="{{ url_for('static', filename='theme.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='app.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='gallery.css') }}"> <link rel="stylesheet" href="{{ url_for('static', filename='gallery.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='audioplayer.css') }}"> <link rel="stylesheet" href="{{ url_for('static', filename='audioplayer.css') }}">
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<script>const admin_enabled = {{ admin_enabled | tojson | safe }};</script> <script>const admin_enabled = {{ admin_enabled | tojson | safe }};</script>
</head> {% endblock %}
<body>
<script src="{{ url_for('static', filename='functions.js') }}"></script>
<header class="site-header">
<a href="/">
<img src="/custom_logo/logoW.png" alt="Logo" class="logo">
</a>
<h1>{{ title_long }}</h1>
</header>
<div class="wrapper">
{% if admin_enabled %}
<div class="admin-nav">
<a href="{{ url_for('mylinks') }}">Meine Links</a>
<span> | </span>
<a href="{{ url_for('folder_secret_config_editor') }}" id="edit-folder-config">Ordnerkonfiguration</a>
<span> | </span>
<div class="dropdown d-inline-block">
<a class="dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false">
Auswertungen
</a>
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="{{ url_for('dashboard') }}">Dashbord</a></li>
<li><a class="dropdown-item" href="{{ url_for('connections') }}">Verbindungen</a></li>
<li><a class="dropdown-item" href="{{ url_for('file_access') }}">Dateizugriffe</a></li>
<li><a class="dropdown-item" href="{{ url_for('songs_dashboard') }}">Wiederholungen</a></li>
<li><a class="dropdown-item" href="{{ url_for('search_db_analyzer') }}">Dateiindex Analyse</a></li>
</ul>
</div>
</div>
{% endif %}
{% if features %} {% block content %}
<!-- Tab Navigation --> <!-- Tab Navigation -->
{% if features %}
<nav class="main-tabs"> <nav class="main-tabs">
{% if "files" in features %}<button class="tab-button active" data-tab="browse">Audio/Photo</button>{% endif %} {% if "files" in features %}<button class="tab-button active" data-tab="browse">Audio/Photo</button>{% endif %}
{% if "messages" in features %}<button class="tab-button" data-tab="messages">Nachrichten</button>{% endif %} {% if "messages" in features %}<button class="tab-button" data-tab="messages">Nachrichten</button>{% endif %}
@ -81,7 +33,6 @@
<!-- Browse Section --> <!-- Browse Section -->
<main id="browse-section" class="tab-content active"> <main id="browse-section" class="tab-content active">
<div class="container">
<div id="breadcrumbs" class="breadcrumb"></div> <div id="breadcrumbs" class="breadcrumb"></div>
<div id="content"></div> <div id="content"></div>
<div id="directory-controls"> <div id="directory-controls">
@ -93,20 +44,24 @@
</svg> </svg>
</button> </button>
</div> </div>
</div>
</main> </main>
{% include 'messages_section.html' %} {% include 'messages_section.html' %}
{% include 'calendar_section.html' %} {% include 'calendar_section.html' %}
<search style="display: none;"> <search style="display: none;">
<button type="button" id="backBtn" class="btn-close search-close-btn" aria-label="beenden"></button>
<div class="container"> <div class="container">
<form id="searchForm" method="post" class="mb-4"> <form id="searchForm" method="post" class="mb-4">
<!-- Suchwörter --> <!-- Suchwörter -->
<div class="mb-3"> <div class="mb-3 search-query-wrap">
<label for="query" class="h5 form-label">Suchwörter:</label> <label for="query" class="h5 form-label">Suchwörter:</label>
<div class="input-group">
<input type="text" id="query" name="query" class="form-control" required> <input type="text" id="query" name="query" class="form-control" required>
<button type="button" id="clearBtn" class="btn btn-outline-secondary input-clear-btn" aria-label="zurücksetzen">
<span aria-hidden="true">&times;</span>
</button>
</div>
</div> </div>
<!-- Toggle für Suchoptionen --> <!-- Toggle für Suchoptionen -->
@ -125,7 +80,6 @@
<!-- Suchoptionen einklappbar --> <!-- Suchoptionen einklappbar -->
<div id="searchOptions" class="collapse border rounded p-3 mb-3"> <div id="searchOptions" class="collapse border rounded p-3 mb-3">
<!-- Kategorie --> <!-- Kategorie -->
<div class="mb-3"> <div class="mb-3">
<label class="form-label">Kategorie:</label> <label class="form-label">Kategorie:</label>
@ -191,17 +145,12 @@
<input type="date" id="dateto" name="dateto" class="form-control"> <input type="date" id="dateto" name="dateto" class="form-control">
</div> </div>
</div> </div>
</div> </div>
<!-- Ende Suchoptionen -->
<!-- Buttons --> <!-- Buttons -->
<div class="mb-3"> <div class="mb-3">
<button type="submit" class="btn btn-primary">Suchen</button> <button type="submit" class="btn btn-primary">Suchen</button>
<button type="button" id="clearBtn" class="btn btn-secondary ms-2">zurücksetzen</button>
<button type="button" id="backBtn" class="btn btn-secondary ms-2">beenden</button>
</div> </div>
</form> </form>
<!-- AJAX-loaded results --> <!-- AJAX-loaded results -->
@ -209,8 +158,6 @@
</div> </div>
</search> </search>
<!-- Global Audio Player in Footer --> <!-- Global Audio Player in Footer -->
<footer> <footer>
<div class="audio-player-container" id="audioPlayerContainer"> <div class="audio-player-container" id="audioPlayerContainer">
@ -220,17 +167,14 @@
</audio> </audio>
<div class="controls"> <div class="controls">
<button class="player-button icon-color"> <button class="player-button icon-color">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zM7 8a1 1 0 012 0v4a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v4a1 1 0 102 0V8a1 1 0 00-1-1z" clip-rule="evenodd" />
</svg>
</button> </button>
<div class="slider"> <div class="slider">
<input type="range" class="timeline" max="100" value="0" step="0.1"> <input type="range" class="timeline" max="100" value="0" step="0.1">
<div id="timeInfo" class="now-playing-info"></div> <div id="timeInfo" class="now-playing-info"></div>
</div> </div>
<button class="sound-button icon-color" onclick="player.fileDownload()"> <button class="sound-button icon-color" onclick="player.fileDownload()">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 122.88 120.89" width="35" height="35"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path fill-rule="evenodd" d="M84.58,47a7.71,7.71,0,1,1,10.8,11L66.09,86.88a7.72,7.72,0,0,1-10.82,0L26.4,58.37a7.71,7.71,0,1,1,10.81-11L53.1,63.12l.16-55.47a7.72,7.72,0,0,1,15.43.13l-.15,55L84.58,47ZM0,113.48.1,83.3a7.72,7.72,0,1,1,15.43.14l-.07,22q46,.09,91.91,0l.07-22.12a7.72,7.72,0,1,1,15.44.14l-.1,30h-.09a7.71,7.71,0,0,1-7.64,7.36q-53.73.1-107.38,0A7.7,7.7,0,0,1,0,113.48Z"/> <path d="M12 2.25A9.75 9.75 0 1 1 2.25 12 9.75 9.75 0 0 1 12 2.25Zm0 5a.75.75 0 0 0-.75.75v4.19l-1.72-1.72a.75.75 0 1 0-1.06 1.06l3.03 3.03a.75.75 0 0 0 1.06 0l3.03-3.03a.75.75 0 1 0-1.06-1.06L12.75 12.2V8a.75.75 0 0 0-.75-.75ZM7.5 15.25a.75.75 0 0 0 0 1.5h9a.75.75 0 0 0 0-1.5Z" />
</svg> </svg>
</button> </button>
</div> </div>
@ -240,7 +184,6 @@
</div> </div>
</div> </div>
</footer> </footer>
</div>
<!-- Transcript Modal --> <!-- Transcript Modal -->
<div id="transcriptModal"> <div id="transcriptModal">
@ -254,10 +197,10 @@
<div id="gallery-modal" style="display: none;"> <div id="gallery-modal" style="display: none;">
<button id="gallery-close">x</button> <button id="gallery-close">x</button>
<button id="gallery-info" aria-expanded="false" aria-controls="gallery-exif">i</button> <button id="gallery-info" aria-expanded="false" aria-controls="gallery-exif">i</button>
<img id="gallery-modal-content" src="" /> <img id="gallery-modal-content" src="">
<button class="gallery-nav gallery-prev"></button> <button class="gallery-nav gallery-prev"></button>
<button class="gallery-nav gallery-next"></button> <button class="gallery-nav gallery-next"></button>
<button id="gallery-play" aria-pressed="false"></button> <button id="gallery-play" aria-pressed="false" aria-label="Start slideshow">&gt;</button>
<div id="gallery-exif" aria-live="polite"></div> <div id="gallery-exif" aria-live="polite"></div>
<div id="gallery-filename" aria-live="polite"></div> <div id="gallery-filename" aria-live="polite"></div>
</div> </div>
@ -269,10 +212,9 @@
<div id="fullscreen-loader" style="display: none;"> <div id="fullscreen-loader" style="display: none;">
<div class="loader"></div> <div class="loader"></div>
</div> </div>
{% endblock %}
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script> {% block scripts %}
<!-- Load main app JS first, then gallery JS -->
<script src="{{ url_for('static', filename='audioplayer.js') }}"></script> <script src="{{ url_for('static', filename='audioplayer.js') }}"></script>
<script src="{{ url_for('static', filename='app.js') }}"></script> <script src="{{ url_for('static', filename='app.js') }}"></script>
<script src="{{ url_for('static', filename='gallery.js') }}"></script> <script src="{{ url_for('static', filename='gallery.js') }}"></script>
@ -281,9 +223,8 @@
<script> <script>
if ('serviceWorker' in navigator) { if ('serviceWorker' in navigator) {
window.addEventListener('load', () => { window.addEventListener('load', () => {
navigator.serviceWorker.register('{{ url_for("static", filename="sw.js") }}') navigator.serviceWorker.register('{{ url_for("static", filename="sw.js") }}');
}); });
} }
</script> </script>
</body> {% endblock %}
</html>

View File

@ -10,6 +10,10 @@
<!-- Android Theme Color --> <!-- Android Theme Color -->
<meta name="theme-color" content="#000"> <meta name="theme-color" content="#000">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Manrope:wght@400;500;600;700;800&display=swap" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet" /> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet" />
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.5/font/bootstrap-icons.css" rel="stylesheet"> <link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.5/font/bootstrap-icons.css" rel="stylesheet">
<link rel="stylesheet" href="{{ url_for('static', filename='theme.css') }}"> <link rel="stylesheet" href="{{ url_for('static', filename='theme.css') }}">
@ -29,14 +33,16 @@
{% if admin_enabled %} {% if admin_enabled %}
<!-- Navigation Bar --> <!-- Navigation Bar -->
<div class="admin-nav"> <div class="admin-nav">
<div class="admin-nav-rail">
<div class="admin-nav-track">
<a href="{{ url_for('index') }}">App</a> <a href="{{ url_for('index') }}">App</a>
<span> | </span> <span> | </span>
<a href="{{ url_for('mylinks') }}">Meine Links</a> <a href="{{ url_for('mylinks') }}">Meine Links</a>
<span> | </span> <span> | </span>
<a href="{{ url_for('folder_secret_config_editor') }}" id="edit-folder-config">Ordnerkonfiguration</a> <a href="{{ url_for('folder_secret_config_editor') }}" id="edit-folder-config">Ordnerkonfiguration</a>
<span> | </span> <span> | </span>
<div class="dropdown d-inline-block"> <div class="dropdown dropend d-inline-block">
<a class="dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false"> <a class="dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown" data-bs-display="static" aria-expanded="false">
Auswertungen Auswertungen
</a> </a>
<ul class="dropdown-menu"> <ul class="dropdown-menu">
@ -48,13 +54,15 @@
</ul> </ul>
</div> </div>
</div> </div>
</div>
</div>
{% endif %} {% endif %}
{% block content %}{% endblock %} {% block content %}{% endblock %}
</div> </div>
{% block scripts %}{% endblock %} {% block scripts %}{% endblock %}
<script src="{{ url_for('static', filename='general.js') }}"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script src="{{ url_for('static', filename='general.js') }}"></script>
</body> </body>
</html> </html>

View File

@ -34,47 +34,63 @@
<ul class="dropdown-menu" aria-labelledby="timeframeDropdown"> <ul class="dropdown-menu" aria-labelledby="timeframeDropdown">
<li> <li>
<a class="dropdown-item {% if session['timeframe'] == 'last24hours' %}active{% endif %}" <a class="dropdown-item {% if session['timeframe'] == 'last24hours' %}active{% endif %}"
href="{{ url_for('file_access', timeframe='last24hours') }}"> href="{{ url_for('file_access', timeframe='last24hours', category=selected_category) }}">
Last 24 Hours Last 24 Hours
</a> </a>
</li> </li>
<li> <li>
<a class="dropdown-item {% if session['timeframe'] == '7days' %}active{% endif %}" <a class="dropdown-item {% if session['timeframe'] == '7days' %}active{% endif %}"
href="{{ url_for('file_access', timeframe='7days') }}"> href="{{ url_for('file_access', timeframe='7days', category=selected_category) }}">
Last 7 Days Last 7 Days
</a> </a>
</li> </li>
<li> <li>
<a class="dropdown-item {% if session['timeframe'] == '14days' %}active{% endif %}" <a class="dropdown-item {% if session['timeframe'] == '14days' %}active{% endif %}"
href="{{ url_for('file_access', timeframe='14days') }}"> href="{{ url_for('file_access', timeframe='14days', category=selected_category) }}">
Last 14 Days Last 14 Days
</a> </a>
</li> </li>
<li> <li>
<a class="dropdown-item {% if session['timeframe'] == '30days' %}active{% endif %}" <a class="dropdown-item {% if session['timeframe'] == '30days' %}active{% endif %}"
href="{{ url_for('file_access', timeframe='30days') }}"> href="{{ url_for('file_access', timeframe='30days', category=selected_category) }}">
Last 30 Days Last 30 Days
</a> </a>
</li> </li>
<li> <li>
<a class="dropdown-item {% if session['timeframe'] == '365days' %}active{% endif %}" <a class="dropdown-item {% if session['timeframe'] == '365days' %}active{% endif %}"
href="{{ url_for('file_access', timeframe='365days') }}"> href="{{ url_for('file_access', timeframe='365days', category=selected_category) }}">
Last 365 Days Last 365 Days
</a> </a>
</li> </li>
</ul> </ul>
</div> </div>
<!-- Category Dropdown -->
{% if categories %}
<div class="dropdown">
<button class="btn btn-secondary dropdown-toggle"
type="button" id="categoryDropdown" data-bs-toggle="dropdown" aria-expanded="false">
{{ selected_category_label }}
</button>
<ul class="dropdown-menu" aria-labelledby="categoryDropdown">
{% for option in categories %}
<li>
<a class="dropdown-item {% if selected_category == option.value %}active{% endif %}"
href="{{ url_for('file_access', timeframe=session['timeframe'], category=option.value) }}">
{{ option.label }}
</a>
</li>
{% endfor %}
</ul>
</div>
{% endif %}
</div> </div>
<!-- Detailed Table of Top File Accesses --> <!-- Detailed Table of Top File Accesses -->
{% for top20_item in top20 %}
<div class="card mb-4"> <div class="card mb-4">
<div class="card-header d-flex justify-content-between align-items-center"> <div class="card-header">{{ selected_category_label }}</div>
<span>{{ top20_item['category'] }}</span> <div class="card-body">
<span class="toggle-icon" aria-label="collapse" role="button" tabindex="0">+</span> {% if top20_files %}
</div>
<div class="card-body collapsable">
<div class="table-responsive"> <div class="table-responsive">
<table class="table table-striped"> <table class="table table-striped">
<thead> <thead>
@ -84,23 +100,20 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for row in top20_item['files'] %} {% for row in top20_files %}
<tr> <tr>
<td>{{ row.access_count }}</td> <td>{{ row.access_count }}</td>
<td>{{ row.rel_path }}</td> <td>{{ row.rel_path }}</td>
</tr> </tr>
{% else %}
<tr>
<td colspan="2">No data available for the selected timeframe.</td>
</tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
</div> </div>
{% else %}
<p class="mb-0">No data available for the selected timeframe and category.</p>
{% endif %}
</div> </div>
</div> </div>
{% endfor %}
</div> </div>
{% endblock %} {% endblock %}

View File

@ -1,51 +1,34 @@
<style> <link rel="stylesheet" href="{{ url_for('static', filename='token.css') }}">
.container-fluid {
height: 100%;
display: flex;
flex-direction: column;
}
.qr-code {
max-width: 300px;
display: block;
margin: auto;
}
.copy-btn {
margin-top: 1rem;
display: inline-flex;
align-items: center;
padding: 0.5rem 1rem;
font-size: 1rem;
cursor: pointer;
border: 1px solid #007bff;
background-color: #007bff;
color: white;
border-radius: 0.25rem;
}
.copy-btn:active {
background-color: #0056b3;
border-color: #0056b3;
}
</style>
<div class="container-fluid"> <div class="container-fluid token-container">
<div class="card h-100 shadow-sm"> {% set folder_names = token_folder | map(attribute='foldername') | list %}
<div class="card h-100 shadow-sm text-center" id="tokenPrintable">
<h2 class="token-page-title">{{ title_long }}</h2>
{% if folder_names %}
<h3 class="token-folder-title">{{ folder_names | join(', ') }}</h3>
{% endif %}
<div class="token-qr-wrap mt-3">
<img src="/custom_logo/logoW.png" class="token-pdf-logo" alt="Logo">
<img src="data:image/png;base64,{{ token_qr_code }}" class="card-img-top qr-code p-3" alt="QR Code for token"> <img src="data:image/png;base64,{{ token_qr_code }}" class="card-img-top qr-code p-3" alt="QR Code for token">
</div>
<div class="card-body"> <div class="card-body">
<div class="card-text mt-2"> <div class="card-text mt-2">
<h2 class="card-title">Token-Link:</h2> <div class="token-action-buttons d-flex flex-wrap gap-2 justify-content-center text-center">
<a href="{{ token_url }}" class="copy-btn" style="text-decoration: none;" id="tokenLink">Link öffnen</a> <a href="{{ token_url }}" class="copy-btn" style="text-decoration: none;" id="tokenLink">Link öffnen</a>
<button class="copy-btn" onclick="toClipboard('{{ token_url }}')">Link kopieren</button> <button class="copy-btn" onclick="toClipboard('{{ token_url }}')">Link kopieren</button>
<button class="copy-btn" id="printTokenButton" type="button" onclick="printToken()">Drucken</button>
</div>
</div> </div>
<div class="card-text mt-2"> <div class="card-text mt-2">
<h2 class="mt-3">Gültig bis:</h2> <h2 class="mt-3">Gültig bis:</h2>
{{ token_valid_to }} {{ token_valid_to }}
</div> </div>
<div class="card-text mt-2"> <!-- <div class="card-text mt-2">
<h2 class="mt-3">Ordner:</h2> <h2 class="mt-3">Ordner:</h2>
{% for folder in token_folder %} {% for folder in token_folder %}
{{ folder.foldername }}<br> {{ folder.foldername }}<br>
{% endfor %} {% endfor %}
</div> </div> -->
</div> </div>
</div> </div>
</div> </div>