diff --git a/migrations/f0ckm_schema.sql b/migrations/f0ckm_schema.sql index e0c76f1..60aa577 100644 --- a/migrations/f0ckm_schema.sql +++ b/migrations/f0ckm_schema.sql @@ -1459,7 +1459,8 @@ CREATE TABLE public.user_options ( receive_user_notifications boolean DEFAULT true, do_not_disturb boolean DEFAULT false, comment_display_mode integer DEFAULT 1, - force_comment_display_mode integer DEFAULT 0 + force_comment_display_mode integer DEFAULT 0, + feed_layout smallint DEFAULT 0 NOT NULL ); diff --git a/public/s/css/f0ckm.css b/public/s/css/f0ckm.css index 03c2d15..dfb40d5 100644 --- a/public/s/css/f0ckm.css +++ b/public/s/css/f0ckm.css @@ -3628,6 +3628,83 @@ div.posts { } } +/* ── Feed Layout Variants ── */ + +/* Layout 0: Grid (compact) — default, no overrides needed */ +div.posts.layout-0 {} + +/* Layout 1: Modern (3-column wider grid) */ +div.posts.layout-1 { + grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); +} +@media (max-width: 600px) { + div.posts.layout-1 { + grid-template-columns: repeat(2, 1fr); + } +} + +/* Layout 2: Feed (X / Instagram style — single-column cards) */ +div.posts.layout-2 { + display: flex; + flex-direction: column; + gap: 18px; + max-width: 680px; + margin-left: auto; + margin-right: auto; + grid-template-columns: unset; +} +div.posts.layout-2 > a:not(.notif-item) { + width: 100%; + aspect-ratio: unset; + border-radius: 8px; + overflow: hidden; +} +div.posts.layout-2 > a:not(.notif-item)::before { + margin-top: 75%; /* ~4:3 portrait card */ +} +@media (max-width: 600px) { + div.posts.layout-2 { + max-width: 100%; + gap: 12px; + } +} + +/* Layout 3: YouTube style — 16:9 cards, 4-col desktop */ +div.posts.layout-3 { + grid-template-columns: repeat(4, 1fr); + gap: 12px; + padding: 0 4px; +} +div.posts.layout-3 > a:not(.notif-item) { + border-radius: 6px; + overflow: hidden; + box-shadow: 0 2px 6px rgba(0,0,0,0.4); + transition: transform 0.15s ease, box-shadow 0.15s ease; +} +div.posts.layout-3 > a:not(.notif-item):hover, +div.posts.layout-3 > a:not(.notif-item).touch-active { + transform: translateY(-2px); + box-shadow: 0 5px 16px rgba(0,0,0,0.6); + outline: none; +} +/* No ::before override — preserves dynamic thumb aspect ratios (data-size spanning) */ +/* Tablet: 3 columns */ +@media (max-width: 960px) and (min-width: 601px) { + div.posts.layout-3 { + grid-template-columns: repeat(3, 1fr); + gap: 10px; + } +} +/* Mobile: 2 columns */ +@media (max-width: 600px) { + div.posts.layout-3 { + grid-template-columns: repeat(2, 1fr); + gap: 8px; + padding: 0; + } +} + + div.posts>a:not(.notif-item) { display: inline-block; position: relative; diff --git a/public/s/js/f0ckm.js b/public/s/js/f0ckm.js index c3e26f6..8de3ef3 100644 --- a/public/s/js/f0ckm.js +++ b/public/s/js/f0ckm.js @@ -40,6 +40,24 @@ window.cancelAnimFrame = (function () { if (ua.includes('Chrome')) htmlEl.classList.add('is-chrome'); if (ua.includes('Safari') && !ua.includes('Chrome')) htmlEl.classList.add('is-safari'); + // Reload on back-forward cache restore so layout classes are always fresh from the server + window.addEventListener('pageshow', function(event) { + if (event.persisted) { + window.location.reload(); + } + }); + + // Mirrors the server-side fallback logic in index.mjs: + // use the user's own feed_layout if they explicitly chose one (> 0), + // otherwise fall back to the site-wide default set via the admin dashboard. + window.getEffectiveFeedLayout = () => { + const s = window.f0ckSession; + if (!s) return 0; + const userLayout = (s.feed_layout !== undefined && s.feed_layout !== null) ? parseInt(s.feed_layout, 10) : 0; + const siteDefault = (s.default_feed_layout !== undefined && s.default_feed_layout !== null) ? parseInt(s.default_feed_layout, 10) : 0; + return (userLayout > 0) ? userLayout : siteDefault; + }; + window.updateVisitIndicators = () => { try { // View indicators and counters have been permanently removed as requested. @@ -1739,6 +1757,12 @@ window.cancelAnimFrame = (function () { // Ensure loading state is reset just in case if (restoredPosts._infiniteState) restoredPosts._infiniteState.loading = false; } + + // Re-apply layout class on the restored node — the cached node may have + // an outdated class if the user changed layout since it was cached. + if (restoredPosts) { + restoredPosts.className = restoredPosts.className.replace(/\blayout-\d\b/g, '').trim() + ' layout-' + window.getEffectiveFeedLayout(); + } } // Restore Scroll Position @@ -1806,7 +1830,8 @@ window.cancelAnimFrame = (function () { // Clear entire main content and create fresh grid structure (with sidebar for index) const indexWrapper = document.createElement('div'); indexWrapper.className = 'index-layout-wrapper'; - indexWrapper.innerHTML = '
'; + const layoutClass = 'layout-' + window.getEffectiveFeedLayout(); + indexWrapper.innerHTML = `
`; main.innerHTML = ''; main.appendChild(indexWrapper); @@ -2305,6 +2330,8 @@ window.cancelAnimFrame = (function () { if (replace) { // Atomic replacement to prevent "jumping" posts.innerHTML = data.html; + // Re-apply layout class (innerHTML wipe discards it; use effective layout = user pref or site default) + posts.className = posts.className.replace(/\blayout-\d\b/g, '').trim() + ' layout-' + window.getEffectiveFeedLayout(); window.updateVisitIndicators(); window.initLazyLoading(); diff --git a/public/s/js/settings.js b/public/s/js/settings.js index 507e327..f821b23 100644 --- a/public/s/js/settings.js +++ b/public/s/js/settings.js @@ -551,11 +551,13 @@ }); } - // New Dual Column Layout Toggle - const layoutToggle = document.getElementById('use_new_layout_toggle'); - if (layoutToggle) { - layoutToggle.addEventListener('change', async () => { - const use_new_layout = layoutToggle.checked; + // Feed Layout Select + const feedLayoutSelect = document.getElementById('feed_layout_select'); + if (feedLayoutSelect) { + feedLayoutSelect.addEventListener('change', async () => { + const feed_layout = parseInt(feedLayoutSelect.value, 10); + const prev = feedLayoutSelect.dataset.prev ?? feedLayoutSelect.value; + feedLayoutSelect.dataset.prev = feedLayoutSelect.value; try { const res = await fetch('/api/v2/settings/layout', { method: 'PUT', @@ -563,23 +565,24 @@ 'Content-Type': 'application/json', 'X-CSRF-Token': window.f0ckSession?.csrf_token }, - body: JSON.stringify({ use_new_layout }) + body: JSON.stringify({ feed_layout }) }); const data = await res.json(); if (data.success) { window.location.reload(); } else { alert(data.msg || 'Error saving preference'); - layoutToggle.checked = !use_new_layout; // Revert + feedLayoutSelect.value = prev; // Revert } } catch (err) { console.error(err); - alert('Failed to save Layout preference'); - layoutToggle.checked = !use_new_layout; // Revert + alert('Failed to save layout preference'); + feedLayoutSelect.value = prev; // Revert } }); } + // Disable Autoplay Toggle const autoplayToggle = document.getElementById('disable_autoplay_toggle'); if (autoplayToggle) { diff --git a/src/inc/locales/de.json b/src/inc/locales/de.json index 43ebda6..7558fb5 100644 --- a/src/inc/locales/de.json +++ b/src/inc/locales/de.json @@ -136,8 +136,12 @@ "ui_section": "Benutzeroberfläche", "appearance_section": "Erscheinungsbild", "show_motd": "Nachricht des Tages (MOTD) anzeigen", - "modern_layout": "Modernes Layout", - "modern_layout_hint": "3-Spalten-Layout", + "feed_layout": "Feed-Layout", + "feed_layout_hint": "Wähle, wie die Hauptseite Beiträge anzeigt", + "feed_layout_grid": "Raster (Kompakt)", + "feed_layout_modern": "Raster (3-spaltig Modern)", + "feed_layout_feed": "Feed (X / Instagram-Stil)", + "feed_layout_youtube": "YouTube-Stil", "alternative_infobox": "Alternativer Autor-Infoblock", "alternative_infobox_hint": "Zeigt einen erweiterten Autor-Block mit Avatar und Bio auf Beitragsseiten", "disable_autoplay": "Automatische Wiedergabe deaktivieren", diff --git a/src/inc/locales/en.json b/src/inc/locales/en.json index e0109d1..a9e471a 100644 --- a/src/inc/locales/en.json +++ b/src/inc/locales/en.json @@ -136,8 +136,12 @@ "ui_section": "User Interface", "appearance_section": "Appearance", "show_motd": "Show Message of the Day (MOTD)", - "modern_layout": "Modern layout", - "modern_layout_hint": "3 Column Layout", + "feed_layout": "Feed Layout", + "feed_layout_hint": "Choose how the main page displays posts", + "feed_layout_grid": "Grid (Compact)", + "feed_layout_modern": "Grid (3-column Modern)", + "feed_layout_feed": "Feed (X / Instagram style)", + "feed_layout_youtube": "YouTube Style", "alternative_infobox": "Alternative Author Infobox", "alternative_infobox_hint": "Show a rich author card with avatar and bio on item pages", "disable_autoplay": "Disable Autoplay", diff --git a/src/inc/locales/nl.json b/src/inc/locales/nl.json index c378c2c..b7d4ad2 100644 --- a/src/inc/locales/nl.json +++ b/src/inc/locales/nl.json @@ -136,8 +136,12 @@ "ui_section": "Gebruikersinterface", "appearance_section": "Uiterlijk", "show_motd": "Toon Bericht van de Dag (MOTD)", - "modern_layout": "Moderne layout", - "modern_layout_hint": "Indeling met 3 kolommen", + "feed_layout": "Feed-indeling", + "feed_layout_hint": "Kies hoe de hoofdpagina berichten weergeeft", + "feed_layout_grid": "Raster (Compact)", + "feed_layout_modern": "Raster (3-koloms Modern)", + "feed_layout_feed": "Feed (X / Instagram-stijl)", + "feed_layout_youtube": "YouTube-stijl", "alternative_infobox": "Alternatief auteur-informatievak", "alternative_infobox_hint": "Toont een uitgebreide auteurkaart met avatar en bio op itempagina's", "disable_autoplay": "Automatisch afspelen uitschakelen", diff --git a/src/inc/locales/zange.json b/src/inc/locales/zange.json index 0e2cb93..7253fab 100644 --- a/src/inc/locales/zange.json +++ b/src/inc/locales/zange.json @@ -136,8 +136,12 @@ "ui_section": "Benutzeroberfläche", "appearance_section": "Erscheinungsbild", "show_motd": "Nachricht des Tages (NdT) anzeigen", - "modern_layout": "Modernes Layout", - "modern_layout_hint": "3-Spalten-Layout", + "feed_layout": "Feed-Layout", + "feed_layout_hint": "Wählze, wie die Hauptzeite Beiträge anzeigt", + "feed_layout_grid": "Raster (Kompakt)", + "feed_layout_modern": "Raster (3-spaltig Modern)", + "feed_layout_feed": "Feed (X / Instagram-Stil)", + "feed_layout_youtube": "YouTube-Stil", "alternative_infobox": "Alternativer Autor-Infoblock", "alternative_infobox_hint": "Zeigt einen erweiterten Autor-Block mit Avatar und Bio auf Beitragsseiten", "disable_autoplay": "Automatische Wiedergabe deaktivieren", diff --git a/src/inc/routes/admin.mjs b/src/inc/routes/admin.mjs index e99df57..0d2d44d 100644 --- a/src/inc/routes/admin.mjs +++ b/src/inc/routes/admin.mjs @@ -12,7 +12,7 @@ import cfg from "../config.mjs"; import security from "../security.mjs"; import crypto from "crypto"; import path from "path"; -import { getManualApproval, setManualApproval, getMinTags, setMinTags, getRegistrationOpen, setRegistrationOpen, getTrustedUploads, setTrustedUploads, getEnablePdf, setEnablePdf, getLogUserIps, setLogUserIps, getHashUserIps, setHashUserIps, getEnableCleanup, setEnableCleanup, getCleanupStartDate, setCleanupStartDate, getCleanupEndDate, setCleanupEndDate, getShitpostMode } from "../settings.mjs"; +import { getManualApproval, setManualApproval, getMinTags, setMinTags, getRegistrationOpen, setRegistrationOpen, getTrustedUploads, setTrustedUploads, getEnablePdf, setEnablePdf, getLogUserIps, setLogUserIps, getHashUserIps, setHashUserIps, getEnableCleanup, setEnableCleanup, getCleanupStartDate, setCleanupStartDate, getCleanupEndDate, setCleanupEndDate, getShitpostMode, getDefaultFeedLayout, setDefaultFeedLayout } from "../settings.mjs"; export default (router, tpl) => { router.get(/^\/login(\/)?$/, async (req, res) => { @@ -287,6 +287,7 @@ export default (router, tpl) => { enable_cleanup: getEnableCleanup(), shitpost_mode: getShitpostMode(), enable_cleanup_config: cfg.websrv.enable_cleanup !== false, + default_feed_layout: getDefaultFeedLayout(), tmp: null }, req) }); @@ -618,6 +619,8 @@ export default (router, tpl) => { const registration_open = req.post.registration_open === 'on' ? 'true' : 'false'; const min_tags = isNaN(parseInt(req.post.min_tags)) ? 3 : Math.max(0, parseInt(req.post.min_tags)); const trusted_uploads = Math.max(0, parseInt(req.post.trusted_uploads) ?? 3); + const raw_feed_layout = parseInt(req.post.default_feed_layout, 10); + const default_feed_layout = (!isNaN(raw_feed_layout) && raw_feed_layout >= 0 && raw_feed_layout <= 3) ? raw_feed_layout : getDefaultFeedLayout(); await db`INSERT INTO site_settings (key, value) VALUES ('manual_approval', ${manual_approval}) ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value`; @@ -626,13 +629,14 @@ export default (router, tpl) => { setRegistrationOpen(registration_open === 'true'); } - await db`INSERT INTO site_settings (key, value) VALUES ('min_tags', ${min_tags.toString()}) ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value`; await db`INSERT INTO site_settings (key, value) VALUES ('trusted_uploads', ${trusted_uploads.toString()}) ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value`; + await db`INSERT INTO site_settings (key, value) VALUES ('default_feed_layout', ${default_feed_layout.toString()}) ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value`; setManualApproval(manual_approval === 'true'); setMinTags(min_tags); setTrustedUploads(trusted_uploads); + setDefaultFeedLayout(default_feed_layout); if (req.headers['x-requested-with'] === 'XMLHttpRequest') { res.setHeader('Content-Type', 'application/json'); @@ -642,7 +646,8 @@ export default (router, tpl) => { manual_approval: getManualApproval(), registration_open: getRegistrationOpen(), min_tags: getMinTags(), - trusted_uploads: getTrustedUploads() + trusted_uploads: getTrustedUploads(), + default_feed_layout: getDefaultFeedLayout() }) }); } diff --git a/src/inc/routes/apiv2/settings.mjs b/src/inc/routes/apiv2/settings.mjs index 947814a..9251cd9 100644 --- a/src/inc/routes/apiv2/settings.mjs +++ b/src/inc/routes/apiv2/settings.mjs @@ -295,19 +295,34 @@ export default router => { } }); - // Update New Layout visibility preference + // Update feed layout preference (0=grid, 1=modern, 2=feed/instagram, 3=youtube) group.put(/\/layout/, lib.loggedin, async (req, res) => { - const use_new_layout = req.post.use_new_layout === true || req.post.use_new_layout === 'true'; + const raw = req.post.feed_layout !== undefined ? req.post.feed_layout : req.post.use_new_layout; + let feed_layout; + + // Backward compat: if old boolean use_new_layout was sent, map to int + if (req.post.feed_layout === undefined && req.post.use_new_layout !== undefined) { + feed_layout = (req.post.use_new_layout === true || req.post.use_new_layout === 'true') ? 1 : 0; + } else { + feed_layout = parseInt(raw, 10); + } + + if (isNaN(feed_layout) || feed_layout < 0 || feed_layout > 3) { + return res.json({ success: false, msg: 'Invalid layout value: must be 0–3' }, 400); + } try { await db` update user_options - set use_new_layout = ${use_new_layout} + set feed_layout = ${feed_layout}, + use_new_layout = ${feed_layout === 1} where user_id = ${+req.session.id} `; - // Sync session immediately - if (req.session) req.session.use_new_layout = use_new_layout; - return res.json({ success: true, use_new_layout }, 200); + if (req.session) { + req.session.feed_layout = feed_layout; + req.session.use_new_layout = feed_layout === 1; + } + return res.json({ success: true, feed_layout }, 200); } catch (e) { console.error('Update Layout pref error:', e); return res.json({ success: false, msg: 'Error updating preference' }, 500); diff --git a/src/inc/routes/index.mjs b/src/inc/routes/index.mjs index 2c513d0..fd3d5a7 100644 --- a/src/inc/routes/index.mjs +++ b/src/inc/routes/index.mjs @@ -2,6 +2,7 @@ import cfg from "../config.mjs"; import db from "../sql.mjs"; import lib from "../lib.mjs"; import f0cklib from "../routeinc/f0cklib.mjs"; +import { getDefaultFeedLayout } from "../settings.mjs"; const auth = async (req, res, next) => { if (!req.session) @@ -320,6 +321,15 @@ export default (router, tpl) => { // Only inject session for authenticated users to avoid showing member UI to guests data.session = (req.session && req.session.user) ? { ...req.session } : false; + // Pre-compute feed layout class (avoids template engine issues with complex ternaries) + // Logic: use user's own feed_layout if they explicitly set one (> 0), + // otherwise fall back to the site-wide default set in the admin dashboard. + const userFeedLayout = data.session ? parseInt(data.session.feed_layout, 10) : 0; + const siteFeedLayout = getDefaultFeedLayout(); + const rawFeedLayout = (userFeedLayout > 0) ? userFeedLayout : siteFeedLayout; + const feedLayoutNum = (!isNaN(rawFeedLayout) && rawFeedLayout >= 0 && rawFeedLayout <= 3) ? rawFeedLayout : 0; + data.feed_layout_class = 'layout-' + feedLayoutNum; + // Precompute boolean helpers for template @if() — the flummpress template engine uses a // non-greedy regex to parse @if(condition) and stops at the FIRST ')' it encounters. // This means any nested parens (e.g. indexOf('x'), .some(fn), (a || b)) inside @if() diff --git a/src/inc/routes/register.mjs b/src/inc/routes/register.mjs index ba72240..a2a603e 100644 --- a/src/inc/routes/register.mjs +++ b/src/inc/routes/register.mjs @@ -1,7 +1,7 @@ import db from "../sql.mjs"; import lib from "../lib.mjs"; import security from "../security.mjs"; -import { getRegistrationOpen, getDefaultLayout } from "../settings.mjs"; +import { getRegistrationOpen, getDefaultLayout, getDefaultFeedLayout } from "../settings.mjs"; import { sendMail } from "../../lib/smtp.mjs"; import cfg from "../config.mjs"; import crypto from "crypto"; @@ -145,8 +145,8 @@ export default (router, tpl) => { const avatarFile = 'default.png'; await db` - insert into user_options (user_id, mode, theme, fullscreen, avatar, avatar_file, use_new_layout, disable_autoplay, disable_swiping) - values (${userId}, 3, 'amoled', 0, ${avatarId}, ${avatarFile}, ${getDefaultLayout() === 'modern'}, ${cfg.websrv.enable_autoplay === false}, ${cfg.websrv.enable_swiping === false}) + insert into user_options (user_id, mode, theme, fullscreen, avatar, avatar_file, use_new_layout, feed_layout, disable_autoplay, disable_swiping) + values (${userId}, 3, 'amoled', 0, ${avatarId}, ${avatarFile}, ${getDefaultFeedLayout() === 1}, ${getDefaultFeedLayout()}, ${cfg.websrv.enable_autoplay === false}, ${cfg.websrv.enable_swiping === false}) `; } catch (err) { console.error(`[REGISTER] DB Error during user creation:`, err); diff --git a/src/inc/settings.mjs b/src/inc/settings.mjs index b1b8c74..3bb8a03 100644 --- a/src/inc/settings.mjs +++ b/src/inc/settings.mjs @@ -8,6 +8,7 @@ let bypass_duplicate_check = false; let protect_files = false; let private_messages = true; let default_layout = 'modern'; +let default_feed_layout = 0; let enable_pdf = false; let enable_cleanup = false; let cleanup_start_date = ''; @@ -62,6 +63,12 @@ export const setPrivateMessages = (val) => private_messages = !!val; export const getDefaultLayout = () => default_layout; export const setDefaultLayout = (val) => default_layout = (val === 'legacy' ? 'legacy' : 'modern'); +export const getDefaultFeedLayout = () => default_feed_layout; +export const setDefaultFeedLayout = (val) => { + const parsed = parseInt(val, 10); + default_feed_layout = (!isNaN(parsed) && parsed >= 0 && parsed <= 3) ? parsed : 0; +}; + export const getLogUserIps = () => !!cfg.websrv.log_user_ips; export const setLogUserIps = (val) => {}; // No-op, strictly config-based diff --git a/src/index.mjs b/src/index.mjs index 5f09216..3051a04 100644 --- a/src/index.mjs +++ b/src/index.mjs @@ -17,7 +17,7 @@ import { handleHallImageUpload, handleHallImageDelete, handleHallDelete, handleH import { handleMetaExtract } from "./meta_extract_handler.mjs"; import { handleMetaStrip } from "./meta_strip_handler.mjs"; import { handleCommentUpload } from "./comment_upload_handler.mjs"; -import { getManualApproval, setManualApproval, getMinTags, setMinTags, getRegistrationOpen, setRegistrationOpen, getTrustedUploads, setTrustedUploads, getBypassDuplicateCheck, setBypassDuplicateCheck, getProtectFiles, setProtectFiles, getPrivateMessages, setPrivateMessages, getDefaultLayout, setDefaultLayout, getEnablePdf, setEnablePdf, getEnableCleanup, setEnableCleanup, getCleanupStartDate, setCleanupStartDate, getCleanupEndDate, setCleanupEndDate, getLogUserIps, setLogUserIps, getHashUserIps, setHashUserIps, getShitpostMode, setShitpostMode } from "./inc/settings.mjs"; +import { getManualApproval, setManualApproval, getMinTags, setMinTags, getRegistrationOpen, setRegistrationOpen, getTrustedUploads, setTrustedUploads, getBypassDuplicateCheck, setBypassDuplicateCheck, getProtectFiles, setProtectFiles, getPrivateMessages, setPrivateMessages, getDefaultLayout, setDefaultLayout, getDefaultFeedLayout, setDefaultFeedLayout, getEnablePdf, setEnablePdf, getEnableCleanup, setEnableCleanup, getCleanupStartDate, setCleanupStartDate, getCleanupEndDate, setCleanupEndDate, getLogUserIps, setLogUserIps, getHashUserIps, setHashUserIps, getShitpostMode, setShitpostMode } from "./inc/settings.mjs"; import { updateHallsCache, getHalls } from "./inc/halls_cache.mjs"; import { createI18n } from "./inc/i18n.mjs"; import security from "./inc/security.mjs"; @@ -504,7 +504,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".feed_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 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 @@ -989,6 +989,19 @@ process.on('uncaughtException', err => { console.log(`[BOOT] Default layout set to: ${getDefaultLayout()}`); } + // Fetch default_feed_layout from DB site_settings + try { + const dflSetting = await db`SELECT value FROM site_settings WHERE key = 'default_feed_layout' LIMIT 1`; + if (dflSetting.length > 0) { + setDefaultFeedLayout(parseInt(dflSetting[0].value, 10)); + console.log(`[BOOT] Default feed layout loaded: ${getDefaultFeedLayout()}`); + } else { + console.log(`[BOOT] No default_feed_layout setting found, defaulting to 0 (Grid)`); + } + } catch (e) { + console.warn(`[BOOT] default_feed_layout fetch failed:`, e.message); + } + // Fetch about_text from database try { const aboutSetting = await db`SELECT value FROM site_settings WHERE key = 'about_text' LIMIT 1`; @@ -1076,6 +1089,7 @@ process.on('uncaughtException', err => { matrix_enabled: cfg.clients.find(c => c.type === 'matrix')?.enabled || false, ts: Date.now(), get default_layout() { return getDefaultLayout(); }, + get default_feed_layout() { return getDefaultFeedLayout(); }, show_koepfe: !!cfg.websrv.show_koepfe, allow_language_change: cfg.websrv.allow_language_change !== false, enable_xd_score: !!cfg.websrv.enable_xd_score, diff --git a/views/admin.html b/views/admin.html index c727f98..b8f0917 100644 --- a/views/admin.html +++ b/views/admin.html @@ -86,6 +86,19 @@ +
+
+ +

Default layout for new users and guests on the main page.

+
+ +
+ @@ -107,6 +120,7 @@ const registrationToggle = document.getElementById('registration_open_toggle'); const minTagsInput = document.getElementById('min_tags_input'); const trustedUploadsInput = document.getElementById('trusted_uploads_input'); + const feedLayoutSelect = document.getElementById('default_feed_layout_select'); status.textContent = 'Saving...'; status.style.color = 'var(--accent)'; @@ -123,6 +137,7 @@ ...(registrationToggle ? { registration_open: registrationToggle.checked ? 'on' : 'off' } : {}), min_tags: minTagsInput.value, trusted_uploads: trustedUploadsInput.value, + default_feed_layout: feedLayoutSelect ? feedLayoutSelect.value : '0', csrf_token: '{{ csrf_token }}' }).toString() }); diff --git a/views/index-partial.html b/views/index-partial.html index f398bea..dc8749f 100644 --- a/views/index-partial.html +++ b/views/index-partial.html @@ -1,7 +1,7 @@
@include(snippets/page-title) -