add alternative controls

This commit is contained in:
2026-05-30 08:35:20 +02:00
parent 3ff61f4e36
commit c98e797d4f
13 changed files with 131 additions and 2 deletions

View File

@@ -111,6 +111,7 @@
"enable_swiping": true,
"enable_profile_description": true,
"user_alternative_infobox": false,
"user_alternative_steuerung": false,
"enable_swf": false,
"swf_thumb": "/s/img/swf.png",
"enable_item_title": true,

View File

@@ -1493,6 +1493,7 @@ CREATE TABLE public.user_options (
hide_koepfe boolean DEFAULT false NOT NULL,
language text,
use_alternative_infobox boolean DEFAULT false,
use_alternative_steuerung boolean DEFAULT false,
receive_system_notifications boolean DEFAULT true,
receive_user_notifications boolean DEFAULT true,
do_not_disturb boolean DEFAULT false,

View File

@@ -9023,6 +9023,28 @@ html[theme="f0ck95d"] .badge-dark {
color: white;
}
.steuerung.steuerung-icon {
font-size: x-large;
}
.steuerung.steuerung-icon a {
color: white;
display: inline-flex;
align-items: center;
justify-content: center;
width: 1.6em;
transition: color 0.15s ease, transform 0.15s ease;
}
.steuerung.steuerung-icon a:hover {
color: var(--accent);
}
.steuerung.steuerung-icon .fa-solid {
font-size: 0.85em;
}
.blahlol {
grid-column: 1 / 4;
width: 100%;

View File

@@ -9099,6 +9099,16 @@ if (navigator.vibrate) {
}, { passive: true });
}
// ── Steuerung icon style: #scrolltobottom smooth scroll ───────────────────────
// The alternative icon nav replaces the Zufall link with a down-chevron that
// scrolls the page to the bottom (comments / tag section).
document.addEventListener('click', (e) => {
const link = e.target.closest('a.steuerung-scroll-down');
if (!link) return;
e.preventDefault();
window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' });
});
// ── Spoiler Tags Event Delegation ─────────────────────────────────────────────
(function() {
const isHidden = (el) => el && el.classList && (el.classList.contains('spoiler') || el.classList.contains('blur-text'));

View File

@@ -670,6 +670,37 @@
});
}
// Alternative Steuerung Toggle (icon-only nav style)
const alternativeSteuerungToggle = document.getElementById('alternative_steuerung_toggle');
if (alternativeSteuerungToggle) {
alternativeSteuerungToggle.addEventListener('change', async () => {
const use_alternative_steuerung = alternativeSteuerungToggle.checked;
try {
const res = await fetch('/api/v2/settings/alternative_steuerung', {
method: 'PUT',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'X-CSRF-Token': window.f0ckSession?.csrf_token
},
body: new URLSearchParams({ use_alternative_steuerung })
});
const data = await res.json();
if (data.success) {
showStatus('Navigation style updated!', 'success');
if (window.f0ckSession) window.f0ckSession.use_alternative_steuerung = use_alternative_steuerung;
} else {
alert(data.msg || 'Error saving preference');
alternativeSteuerungToggle.checked = !use_alternative_steuerung;
}
} catch (err) {
console.error(err);
alert('Failed to save navigation style preference');
alternativeSteuerungToggle.checked = !use_alternative_steuerung;
}
});
}
// Notification Preferences Toggles
const setupPreferenceToggle = (id, sessionKey) => {
const el = document.getElementById(id);

View File

@@ -142,6 +142,8 @@
"modern_layout_hint": "3-Spalten-Layout",
"alternative_infobox": "Alternativer Autor-Infoblock",
"alternative_infobox_hint": "Zeigt einen erweiterten Autor-Block mit Avatar und Bio auf Beitragsseiten",
"alternative_steuerung": "Icon-Navigationsstil",
"alternative_steuerung_hint": "Ersetzt die Text-Navigation (← zurück | Zufall | weiter →) durch kompakte Chevron-Icons",
"disable_autoplay": "Automatische Wiedergabe deaktivieren",
"disable_autoplay_hint": "Verhindert die automatische Wiedergabe von Videos und Audio",
"disable_swiping": "Wischen deaktivieren",

View File

@@ -142,6 +142,8 @@
"modern_layout_hint": "3 Column Layout",
"alternative_infobox": "Alternative Author Infobox",
"alternative_infobox_hint": "Show a rich author card with avatar and bio on item pages",
"alternative_steuerung": "Icon nav style",
"alternative_steuerung_hint": "Replace text navigation (← prev | random | next →) with compact chevron icons",
"disable_autoplay": "Disable Autoplay",
"disable_autoplay_hint": "Prevent videos and audio from playing automatically",
"disable_swiping": "Disable Swiping",

View File

@@ -142,6 +142,8 @@
"modern_layout_hint": "Indeling met 3 kolommen",
"alternative_infobox": "Alternatief auteur-informatievak",
"alternative_infobox_hint": "Toont een uitgebreide auteurkaart met avatar en bio op itempagina's",
"alternative_steuerung": "Icoon-navigatiestijl",
"alternative_steuerung_hint": "Vervangt tekstnavigatie (← terug | willekeurig | verder →) door compacte chevron-iconen",
"disable_autoplay": "Automatisch afspelen uitschakelen",
"disable_autoplay_hint": "Voorkomen dat video's en audio automatisch worden afgespeeld",
"disable_swiping": "Swipen uitschakelen",

View File

@@ -142,6 +142,8 @@
"modern_layout_hint": "3-Spalten-Layout",
"alternative_infobox": "Alternativer Autor-Infoblock",
"alternative_infobox_hint": "Zeigt einen erweiterten Autor-Block mit Avatar und Bio auf Beitragsseiten",
"alternative_steuerung": "Icon-Navigationsstil",
"alternative_steuerung_hint": "Ersetzt die Text-Navigation (← zurück | Zufall | weiter →) durch kompakte Chevron-Icons",
"disable_autoplay": "Automatische Wiedergabe deaktivieren",
"disable_autoplay_hint": "Vermeiden Sie das automatische Abspielen von Videos und Tondateien",
"disable_swiping": "Wischen deaktivieren",

View File

@@ -645,6 +645,24 @@ export default router => {
}
});
// Update alternative steuerung preference (per-user toggle for icon-only nav)
group.put(/\/alternative_steuerung/, lib.loggedin, async (req, res) => {
const use_alternative_steuerung = req.post.use_alternative_steuerung === true || req.post.use_alternative_steuerung === 'true';
try {
await db`
update user_options
set use_alternative_steuerung = ${use_alternative_steuerung}
where user_id = ${+req.session.id}
`;
if (req.session) req.session.use_alternative_steuerung = use_alternative_steuerung;
return res.json({ success: true, use_alternative_steuerung }, 200);
} catch (e) {
console.error('Update alternative_steuerung error:', e);
return res.json({ success: false, msg: 'Error updating preference' }, 500);
}
});
// Update per-user language preference
group.put(/\/language/, lib.loggedin, async (req, res) => {
if (cfg.websrv.allow_language_change === false) {

View File

@@ -506,7 +506,7 @@ process.on('uncaughtException', err => {
if (req.cookies.session) {
const user = await db`
select "user".id, "user".login, "user".user, "user".admin, "user".is_moderator, "user".banned, "user".ban_reason, "user".ban_expires, "user".force_password_change, "user_sessions".id as sess_id, "user_sessions".csrf_token, "user_options".mode, "user_options".theme, "user_options".fullscreen, "user_options".excluded_tags, "user_options".avatar, "user_options".avatar_file, "user_options".show_motd, "user_options".strict_mode, "user_options".show_background, "user_options".use_new_layout, "user_options".username_color, "user_options".font, "user_options".disable_autoplay, "user_options".disable_swiping, "user_options".description, "user_options".display_name, COALESCE("user_options".min_xd_score, 0) as min_xd_score, "user_options".ruffle_volume, "user_options".ruffle_background, "user_options".quote_emojis, "user_options".embed_youtube_in_comments, "user_options".hide_koepfe, "user_options".language, "user_options".use_alternative_infobox, "user_options".receive_system_notifications, "user_options".receive_user_notifications, "user_options".do_not_disturb, "user_options".comment_display_mode, "user_options".force_comment_display_mode
select "user".id, "user".login, "user".user, "user".admin, "user".is_moderator, "user".banned, "user".ban_reason, "user".ban_expires, "user".force_password_change, "user_sessions".id as sess_id, "user_sessions".csrf_token, "user_options".mode, "user_options".theme, "user_options".fullscreen, "user_options".excluded_tags, "user_options".avatar, "user_options".avatar_file, "user_options".show_motd, "user_options".strict_mode, "user_options".show_background, "user_options".use_new_layout, "user_options".username_color, "user_options".font, "user_options".disable_autoplay, "user_options".disable_swiping, "user_options".description, "user_options".display_name, COALESCE("user_options".min_xd_score, 0) as min_xd_score, "user_options".ruffle_volume, "user_options".ruffle_background, "user_options".quote_emojis, "user_options".embed_youtube_in_comments, "user_options".hide_koepfe, "user_options".language, "user_options".use_alternative_infobox, "user_options".use_alternative_steuerung, "user_options".receive_system_notifications, "user_options".receive_user_notifications, "user_options".do_not_disturb, "user_options".comment_display_mode, "user_options".force_comment_display_mode
from "user_sessions"
left join "user" on "user".id = "user_sessions".user_id
left join "user_options" on "user_options".user_id = "user_sessions".user_id
@@ -633,9 +633,10 @@ process.on('uncaughtException', err => {
hide_koepfe: user[0].hide_koepfe ?? false,
language: (user[0].language && user[0].language.trim()) ? user[0].language.trim() : null,
use_alternative_infobox: user[0].use_alternative_infobox ?? (cfg.websrv.user_alternative_infobox !== false),
use_alternative_steuerung: user[0].use_alternative_steuerung ?? (cfg.websrv.user_alternative_steuerung !== false),
comment_display_mode: user[0].comment_display_mode ?? (cfg.websrv.default_comment_display_mode || 0),
force_comment_display_mode: user[0].force_comment_display_mode ?? 0
}, 'user_id', 'mode', 'theme', 'fullscreen', 'excluded_tags', 'font', 'disable_autoplay', 'disable_swiping', 'show_background', 'ruffle_volume', 'ruffle_background', 'quote_emojis', 'embed_youtube_in_comments', 'hide_koepfe', 'language', 'use_alternative_infobox', 'comment_display_mode', 'force_comment_display_mode')
}, 'user_id', 'mode', 'theme', 'fullscreen', 'excluded_tags', 'font', 'disable_autoplay', 'disable_swiping', 'show_background', 'ruffle_volume', 'ruffle_background', 'quote_emojis', 'embed_youtube_in_comments', 'hide_koepfe', 'language', 'use_alternative_infobox', 'use_alternative_steuerung', 'comment_display_mode', 'force_comment_display_mode')
}
on conflict ("user_id") do update set
theme = excluded.theme,
@@ -652,6 +653,7 @@ process.on('uncaughtException', err => {
hide_koepfe = excluded.hide_koepfe,
language = excluded.language,
use_alternative_infobox = excluded.use_alternative_infobox,
use_alternative_steuerung = excluded.use_alternative_steuerung,
comment_display_mode = excluded.comment_display_mode,
force_comment_display_mode = excluded.force_comment_display_mode,
user_id = excluded.user_id
@@ -1222,10 +1224,19 @@ process.on('uncaughtException', err => {
? data.user_alternative_infobox
: (cfg.websrv.user_alternative_infobox !== false)));
const useAltSteuerung = (req && req.session && typeof req.session.use_alternative_steuerung === 'boolean')
? req.session.use_alternative_steuerung
: (req && !req.session
? false
: (data && typeof data.user_alternative_steuerung === 'boolean'
? data.user_alternative_steuerung
: (cfg.websrv.user_alternative_steuerung !== false)));
data = Object.assign({}, globals, data || {}, {
t: perRequestT,
lang: perRequestLang,
user_alternative_infobox: useAltInfobox,
user_alternative_steuerung: useAltSteuerung,
comment_display_mode: (req && req.session && typeof req.session.comment_display_mode === 'number')
? req.session.comment_display_mode
: (data && typeof data.comment_display_mode === 'number'

View File

@@ -45,6 +45,7 @@
<div class="kontrollelement">
<div class="einheit">
@if(typeof pagination !== "undefined")
@if(!user_alternative_steuerung)
<nav class="steuerung">
@if(pagination.next)
<a class="nav-prev" href="{{ link.main }}{{ pagination.next }}{{ link.suffix }}">← {{ t('nav.prev') }}</a>
@@ -62,6 +63,21 @@
<a class="nav-next" href="#" style="visibility: hidden">{{ t('nav.next') }} →</a>
@endif
</nav>
@else
<nav class="steuerung steuerung-icon">
@if(pagination.next)
<a class="nav-prev" href="{{ link.main }}{{ pagination.next }}{{ link.suffix }}"><i class="fa-solid fa-chevron-left"></i></a>
@else
<a class="nav-prev" href="#" style="visibility: hidden"><i class="fa-solid fa-chevron-left"></i></a>
@endif
<a class="steuerung-scroll-down" href="#scrolltobottom"><i class="fa-solid fa-chevron-down"></i></a>
@if(pagination.prev)
<a class="nav-next" href="{{ link.main }}{{ pagination.prev }}{{ link.suffix }}"><i class="fa-solid fa-chevron-right"></i></a>
@else
<a class="nav-next" href="#" style="visibility: hidden"><i class="fa-solid fa-chevron-right"></i></a>
@endif
</nav>
@endif
@endif
</div>
</div>

View File

@@ -197,6 +197,17 @@
<small class="text-muted" style="margin-left: 25px;">{{ t('settings.alternative_infobox_hint') }}</small>
</div>
@endif
@if(!session.use_new_layout)
<div class="setting-item" style="margin-top: 15px;">
<label for="alternative_steuerung_toggle"
style="cursor: pointer; display: flex; align-items: center; gap: 10px;">
<input type="checkbox" id="alternative_steuerung_toggle" @if(session.use_alternative_steuerung===true) checked @endif>
<span>{{ t('settings.alternative_steuerung') }}</span>
</label>
<small class="text-muted" style="margin-left: 25px;">{{ t('settings.alternative_steuerung_hint') }}</small>
</div>
@endif
<div class="setting-item" style="margin-bottom: 15px;">
<label for="wheel_nav_toggle" style="cursor: pointer; display: flex; align-items: center; gap: 10px;">
<input type="checkbox" id="wheel_nav_toggle">