Compare commits

1 Commits

Author SHA1 Message Date
dbb8861aed adding different layouts for testing 2026-05-17 14:02:35 +02:00
18 changed files with 238 additions and 41 deletions

View File

@@ -1459,7 +1459,8 @@ CREATE TABLE public.user_options (
receive_user_notifications boolean DEFAULT true, receive_user_notifications boolean DEFAULT true,
do_not_disturb boolean DEFAULT false, do_not_disturb boolean DEFAULT false,
comment_display_mode integer DEFAULT 1, 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
); );

View File

@@ -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) { div.posts>a:not(.notif-item) {
display: inline-block; display: inline-block;
position: relative; position: relative;

View File

@@ -40,6 +40,24 @@ window.cancelAnimFrame = (function () {
if (ua.includes('Chrome')) htmlEl.classList.add('is-chrome'); if (ua.includes('Chrome')) htmlEl.classList.add('is-chrome');
if (ua.includes('Safari') && !ua.includes('Chrome')) htmlEl.classList.add('is-safari'); 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 = () => { window.updateVisitIndicators = () => {
try { try {
// View indicators and counters have been permanently removed as requested. // 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 // Ensure loading state is reset just in case
if (restoredPosts._infiniteState) restoredPosts._infiniteState.loading = false; 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 // Restore Scroll Position
@@ -1806,7 +1830,8 @@ window.cancelAnimFrame = (function () {
// Clear entire main content and create fresh grid structure (with sidebar for index) // Clear entire main content and create fresh grid structure (with sidebar for index)
const indexWrapper = document.createElement('div'); const indexWrapper = document.createElement('div');
indexWrapper.className = 'index-layout-wrapper'; indexWrapper.className = 'index-layout-wrapper';
indexWrapper.innerHTML = '<div class="index-container"><div class="posts grid-transition"></div></div>'; const layoutClass = 'layout-' + window.getEffectiveFeedLayout();
indexWrapper.innerHTML = `<div class="index-container"><div class="posts grid-transition ${layoutClass}"></div></div>`;
main.innerHTML = ''; main.innerHTML = '';
main.appendChild(indexWrapper); main.appendChild(indexWrapper);
@@ -2305,6 +2330,8 @@ window.cancelAnimFrame = (function () {
if (replace) { if (replace) {
// Atomic replacement to prevent "jumping" // Atomic replacement to prevent "jumping"
posts.innerHTML = data.html; 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.updateVisitIndicators();
window.initLazyLoading(); window.initLazyLoading();

View File

@@ -551,11 +551,13 @@
}); });
} }
// New Dual Column Layout Toggle // Feed Layout Select
const layoutToggle = document.getElementById('use_new_layout_toggle'); const feedLayoutSelect = document.getElementById('feed_layout_select');
if (layoutToggle) { if (feedLayoutSelect) {
layoutToggle.addEventListener('change', async () => { feedLayoutSelect.addEventListener('change', async () => {
const use_new_layout = layoutToggle.checked; const feed_layout = parseInt(feedLayoutSelect.value, 10);
const prev = feedLayoutSelect.dataset.prev ?? feedLayoutSelect.value;
feedLayoutSelect.dataset.prev = feedLayoutSelect.value;
try { try {
const res = await fetch('/api/v2/settings/layout', { const res = await fetch('/api/v2/settings/layout', {
method: 'PUT', method: 'PUT',
@@ -563,23 +565,24 @@
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'X-CSRF-Token': window.f0ckSession?.csrf_token 'X-CSRF-Token': window.f0ckSession?.csrf_token
}, },
body: JSON.stringify({ use_new_layout }) body: JSON.stringify({ feed_layout })
}); });
const data = await res.json(); const data = await res.json();
if (data.success) { if (data.success) {
window.location.reload(); window.location.reload();
} else { } else {
alert(data.msg || 'Error saving preference'); alert(data.msg || 'Error saving preference');
layoutToggle.checked = !use_new_layout; // Revert feedLayoutSelect.value = prev; // Revert
} }
} catch (err) { } catch (err) {
console.error(err); console.error(err);
alert('Failed to save Layout preference'); alert('Failed to save layout preference');
layoutToggle.checked = !use_new_layout; // Revert feedLayoutSelect.value = prev; // Revert
} }
}); });
} }
// Disable Autoplay Toggle // Disable Autoplay Toggle
const autoplayToggle = document.getElementById('disable_autoplay_toggle'); const autoplayToggle = document.getElementById('disable_autoplay_toggle');
if (autoplayToggle) { if (autoplayToggle) {

View File

@@ -136,8 +136,12 @@
"ui_section": "Benutzeroberfläche", "ui_section": "Benutzeroberfläche",
"appearance_section": "Erscheinungsbild", "appearance_section": "Erscheinungsbild",
"show_motd": "Nachricht des Tages (MOTD) anzeigen", "show_motd": "Nachricht des Tages (MOTD) anzeigen",
"modern_layout": "Modernes Layout", "feed_layout": "Feed-Layout",
"modern_layout_hint": "3-Spalten-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": "Alternativer Autor-Infoblock",
"alternative_infobox_hint": "Zeigt einen erweiterten Autor-Block mit Avatar und Bio auf Beitragsseiten", "alternative_infobox_hint": "Zeigt einen erweiterten Autor-Block mit Avatar und Bio auf Beitragsseiten",
"disable_autoplay": "Automatische Wiedergabe deaktivieren", "disable_autoplay": "Automatische Wiedergabe deaktivieren",

View File

@@ -136,8 +136,12 @@
"ui_section": "User Interface", "ui_section": "User Interface",
"appearance_section": "Appearance", "appearance_section": "Appearance",
"show_motd": "Show Message of the Day (MOTD)", "show_motd": "Show Message of the Day (MOTD)",
"modern_layout": "Modern layout", "feed_layout": "Feed Layout",
"modern_layout_hint": "3 Column 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": "Alternative Author Infobox",
"alternative_infobox_hint": "Show a rich author card with avatar and bio on item pages", "alternative_infobox_hint": "Show a rich author card with avatar and bio on item pages",
"disable_autoplay": "Disable Autoplay", "disable_autoplay": "Disable Autoplay",

View File

@@ -136,8 +136,12 @@
"ui_section": "Gebruikersinterface", "ui_section": "Gebruikersinterface",
"appearance_section": "Uiterlijk", "appearance_section": "Uiterlijk",
"show_motd": "Toon Bericht van de Dag (MOTD)", "show_motd": "Toon Bericht van de Dag (MOTD)",
"modern_layout": "Moderne layout", "feed_layout": "Feed-indeling",
"modern_layout_hint": "Indeling met 3 kolommen", "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": "Alternatief auteur-informatievak",
"alternative_infobox_hint": "Toont een uitgebreide auteurkaart met avatar en bio op itempagina's", "alternative_infobox_hint": "Toont een uitgebreide auteurkaart met avatar en bio op itempagina's",
"disable_autoplay": "Automatisch afspelen uitschakelen", "disable_autoplay": "Automatisch afspelen uitschakelen",

View File

@@ -136,8 +136,12 @@
"ui_section": "Benutzeroberfläche", "ui_section": "Benutzeroberfläche",
"appearance_section": "Erscheinungsbild", "appearance_section": "Erscheinungsbild",
"show_motd": "Nachricht des Tages (NdT) anzeigen", "show_motd": "Nachricht des Tages (NdT) anzeigen",
"modern_layout": "Modernes Layout", "feed_layout": "Feed-Layout",
"modern_layout_hint": "3-Spalten-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": "Alternativer Autor-Infoblock",
"alternative_infobox_hint": "Zeigt einen erweiterten Autor-Block mit Avatar und Bio auf Beitragsseiten", "alternative_infobox_hint": "Zeigt einen erweiterten Autor-Block mit Avatar und Bio auf Beitragsseiten",
"disable_autoplay": "Automatische Wiedergabe deaktivieren", "disable_autoplay": "Automatische Wiedergabe deaktivieren",

View File

@@ -12,7 +12,7 @@ import cfg from "../config.mjs";
import security from "../security.mjs"; import security from "../security.mjs";
import crypto from "crypto"; import crypto from "crypto";
import path from "path"; 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) => { export default (router, tpl) => {
router.get(/^\/login(\/)?$/, async (req, res) => { router.get(/^\/login(\/)?$/, async (req, res) => {
@@ -287,6 +287,7 @@ export default (router, tpl) => {
enable_cleanup: getEnableCleanup(), enable_cleanup: getEnableCleanup(),
shitpost_mode: getShitpostMode(), shitpost_mode: getShitpostMode(),
enable_cleanup_config: cfg.websrv.enable_cleanup !== false, enable_cleanup_config: cfg.websrv.enable_cleanup !== false,
default_feed_layout: getDefaultFeedLayout(),
tmp: null tmp: null
}, req) }, req)
}); });
@@ -618,6 +619,8 @@ export default (router, tpl) => {
const registration_open = req.post.registration_open === 'on' ? 'true' : 'false'; 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 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 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`; 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'); 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 ('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 ('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'); setManualApproval(manual_approval === 'true');
setMinTags(min_tags); setMinTags(min_tags);
setTrustedUploads(trusted_uploads); setTrustedUploads(trusted_uploads);
setDefaultFeedLayout(default_feed_layout);
if (req.headers['x-requested-with'] === 'XMLHttpRequest') { if (req.headers['x-requested-with'] === 'XMLHttpRequest') {
res.setHeader('Content-Type', 'application/json'); res.setHeader('Content-Type', 'application/json');
@@ -642,7 +646,8 @@ export default (router, tpl) => {
manual_approval: getManualApproval(), manual_approval: getManualApproval(),
registration_open: getRegistrationOpen(), registration_open: getRegistrationOpen(),
min_tags: getMinTags(), min_tags: getMinTags(),
trusted_uploads: getTrustedUploads() trusted_uploads: getTrustedUploads(),
default_feed_layout: getDefaultFeedLayout()
}) })
}); });
} }

View File

@@ -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) => { 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 03' }, 400);
}
try { try {
await db` await db`
update user_options 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} where user_id = ${+req.session.id}
`; `;
// Sync session immediately if (req.session) {
if (req.session) req.session.use_new_layout = use_new_layout; req.session.feed_layout = feed_layout;
return res.json({ success: true, use_new_layout }, 200); req.session.use_new_layout = feed_layout === 1;
}
return res.json({ success: true, feed_layout }, 200);
} catch (e) { } catch (e) {
console.error('Update Layout pref error:', e); console.error('Update Layout pref error:', e);
return res.json({ success: false, msg: 'Error updating preference' }, 500); return res.json({ success: false, msg: 'Error updating preference' }, 500);

View File

@@ -2,6 +2,7 @@ import cfg from "../config.mjs";
import db from "../sql.mjs"; import db from "../sql.mjs";
import lib from "../lib.mjs"; import lib from "../lib.mjs";
import f0cklib from "../routeinc/f0cklib.mjs"; import f0cklib from "../routeinc/f0cklib.mjs";
import { getDefaultFeedLayout } from "../settings.mjs";
const auth = async (req, res, next) => { const auth = async (req, res, next) => {
if (!req.session) if (!req.session)
@@ -320,6 +321,15 @@ export default (router, tpl) => {
// Only inject session for authenticated users to avoid showing member UI to guests // Only inject session for authenticated users to avoid showing member UI to guests
data.session = (req.session && req.session.user) ? { ...req.session } : false; 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 // 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. // 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() // This means any nested parens (e.g. indexOf('x'), .some(fn), (a || b)) inside @if()

View File

@@ -1,7 +1,7 @@
import db from "../sql.mjs"; import db from "../sql.mjs";
import lib from "../lib.mjs"; import lib from "../lib.mjs";
import security from "../security.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 { sendMail } from "../../lib/smtp.mjs";
import cfg from "../config.mjs"; import cfg from "../config.mjs";
import crypto from "crypto"; import crypto from "crypto";
@@ -145,8 +145,8 @@ export default (router, tpl) => {
const avatarFile = 'default.png'; const avatarFile = 'default.png';
await db` await db`
insert into user_options (user_id, mode, theme, fullscreen, avatar, avatar_file, use_new_layout, disable_autoplay, disable_swiping) 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}, ${getDefaultLayout() === 'modern'}, ${cfg.websrv.enable_autoplay === false}, ${cfg.websrv.enable_swiping === false}) values (${userId}, 3, 'amoled', 0, ${avatarId}, ${avatarFile}, ${getDefaultFeedLayout() === 1}, ${getDefaultFeedLayout()}, ${cfg.websrv.enable_autoplay === false}, ${cfg.websrv.enable_swiping === false})
`; `;
} catch (err) { } catch (err) {
console.error(`[REGISTER] DB Error during user creation:`, err); console.error(`[REGISTER] DB Error during user creation:`, err);

View File

@@ -8,6 +8,7 @@ let bypass_duplicate_check = false;
let protect_files = false; let protect_files = false;
let private_messages = true; let private_messages = true;
let default_layout = 'modern'; let default_layout = 'modern';
let default_feed_layout = 0;
let enable_pdf = false; let enable_pdf = false;
let enable_cleanup = false; let enable_cleanup = false;
let cleanup_start_date = ''; let cleanup_start_date = '';
@@ -62,6 +63,12 @@ export const setPrivateMessages = (val) => private_messages = !!val;
export const getDefaultLayout = () => default_layout; export const getDefaultLayout = () => default_layout;
export const setDefaultLayout = (val) => default_layout = (val === 'legacy' ? 'legacy' : 'modern'); 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 getLogUserIps = () => !!cfg.websrv.log_user_ips;
export const setLogUserIps = (val) => {}; // No-op, strictly config-based export const setLogUserIps = (val) => {}; // No-op, strictly config-based

View File

@@ -17,7 +17,7 @@ import { handleHallImageUpload, handleHallImageDelete, handleHallDelete, handleH
import { handleMetaExtract } from "./meta_extract_handler.mjs"; import { handleMetaExtract } from "./meta_extract_handler.mjs";
import { handleMetaStrip } from "./meta_strip_handler.mjs"; import { handleMetaStrip } from "./meta_strip_handler.mjs";
import { handleCommentUpload } from "./comment_upload_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 { updateHallsCache, getHalls } from "./inc/halls_cache.mjs";
import { createI18n } from "./inc/i18n.mjs"; import { createI18n } from "./inc/i18n.mjs";
import security from "./inc/security.mjs"; import security from "./inc/security.mjs";
@@ -504,7 +504,7 @@ process.on('uncaughtException', err => {
if (req.cookies.session) { if (req.cookies.session) {
const user = await db` 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" from "user_sessions"
left join "user" on "user".id = "user_sessions".user_id left join "user" on "user".id = "user_sessions".user_id
left join "user_options" on "user_options".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()}`); 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 // Fetch about_text from database
try { try {
const aboutSetting = await db`SELECT value FROM site_settings WHERE key = 'about_text' LIMIT 1`; 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, matrix_enabled: cfg.clients.find(c => c.type === 'matrix')?.enabled || false,
ts: Date.now(), ts: Date.now(),
get default_layout() { return getDefaultLayout(); }, get default_layout() { return getDefaultLayout(); },
get default_feed_layout() { return getDefaultFeedLayout(); },
show_koepfe: !!cfg.websrv.show_koepfe, show_koepfe: !!cfg.websrv.show_koepfe,
allow_language_change: cfg.websrv.allow_language_change !== false, allow_language_change: cfg.websrv.allow_language_change !== false,
enable_xd_score: !!cfg.websrv.enable_xd_score, enable_xd_score: !!cfg.websrv.enable_xd_score,

View File

@@ -86,6 +86,19 @@
</div> </div>
<div class="settings-item" style="background: rgba(0,0,0,0.2); padding: 15px; border-radius: 4px; display: flex; align-items: center; justify-content: space-between; margin-top: 10px;">
<div>
<label style="display: block; font-weight: bold; color: var(--accent);">Default Feed Layout</label>
<p style="margin: 2px 0 0 0; font-size: 0.8em; color: #aaa;">Default layout for new users and guests on the main page.</p>
</div>
<select id="default_feed_layout_select" onchange="saveAdminSettings()" style="background: #333; border: 1px solid #444; color: #fff; padding: 5px 8px; border-radius: 4px; font-size: 0.85em;">
<option value="0" {{ default_feed_layout === 0 ? 'selected' : '' }}>Grid (Compact)</option>
<option value="1" {{ default_feed_layout === 1 ? 'selected' : '' }}>Grid (3-column Modern)</option>
<option value="2" {{ default_feed_layout === 2 ? 'selected' : '' }}>Feed (X / Instagram)</option>
<option value="3" {{ default_feed_layout === 3 ? 'selected' : '' }}>YouTube Style</option>
</select>
</div>
<span id="settings-status" style="display: block; margin-top: 10px; font-size: 0.8em; font-weight: bold; text-align: right;"></span> <span id="settings-status" style="display: block; margin-top: 10px; font-size: 0.8em; font-weight: bold; text-align: right;"></span>
</div> </div>
</div> </div>
@@ -107,6 +120,7 @@
const registrationToggle = document.getElementById('registration_open_toggle'); const registrationToggle = document.getElementById('registration_open_toggle');
const minTagsInput = document.getElementById('min_tags_input'); const minTagsInput = document.getElementById('min_tags_input');
const trustedUploadsInput = document.getElementById('trusted_uploads_input'); const trustedUploadsInput = document.getElementById('trusted_uploads_input');
const feedLayoutSelect = document.getElementById('default_feed_layout_select');
status.textContent = 'Saving...'; status.textContent = 'Saving...';
status.style.color = 'var(--accent)'; status.style.color = 'var(--accent)';
@@ -123,6 +137,7 @@
...(registrationToggle ? { registration_open: registrationToggle.checked ? 'on' : 'off' } : {}), ...(registrationToggle ? { registration_open: registrationToggle.checked ? 'on' : 'off' } : {}),
min_tags: minTagsInput.value, min_tags: minTagsInput.value,
trusted_uploads: trustedUploadsInput.value, trusted_uploads: trustedUploadsInput.value,
default_feed_layout: feedLayoutSelect ? feedLayoutSelect.value : '0',
csrf_token: '{{ csrf_token }}' csrf_token: '{{ csrf_token }}'
}).toString() }).toString()
}); });

View File

@@ -1,7 +1,7 @@
<div class="index-layout-wrapper"> <div class="index-layout-wrapper">
<div class="index-container"> <div class="index-container">
@include(snippets/page-title) @include(snippets/page-title)
<div class="posts" data-current-page="{{ pagination.current }}" data-has-more="{{ pagination.next ? 'true' : 'false' }}"> <div class="posts {{ feed_layout_class }}" data-current-page="{{ pagination.current }}" data-has-more="{{ pagination.next ? 'true' : 'false' }}">
@each(items as item) @each(items as item)
<a href="{{ link.main }}{{ item.id }}" class="{{ item.is_pinned ? 'anim-boxshadow ' : '' }}thumb lazy-thumb {{ item.has_notification ? 'has-notif' : '' }} {{ item.is_pinned ? 'is-pinned' : '' }}" data-file="{{ item.dest }}" data-mime="{{ item.mime }}" data-user="{!! item.display_name || item.username !!}" data-ext="{{ item.mime.split('/')[1].replace('youtube', 'yt').replace('x-shockwave-flash', 'flash').replace('vnd.adobe.flash.movie', 'flash').toUpperCase() }}" data-mode="{{ item.tag_id == nsfl_tag_id ? 'nsfl' : (item.tag_id == 2 ? 'nsfw' : (item.tag_id == 1 ? 'sfw' : 'null')) }}" data-bg="/t/{{ item.id }}.webp" data-size="{{ enable_dynamic_thumbs ? (item.thumb_size || 1) : 1 }}"> <a href="{{ link.main }}{{ item.id }}" class="{{ item.is_pinned ? 'anim-boxshadow ' : '' }}thumb lazy-thumb {{ item.has_notification ? 'has-notif' : '' }} {{ item.is_pinned ? 'is-pinned' : '' }}" data-file="{{ item.dest }}" data-mime="{{ item.mime }}" data-user="{!! item.display_name || item.username !!}" data-ext="{{ item.mime.split('/')[1].replace('youtube', 'yt').replace('x-shockwave-flash', 'flash').replace('vnd.adobe.flash.movie', 'flash').toUpperCase() }}" data-mode="{{ item.tag_id == nsfl_tag_id ? 'nsfl' : (item.tag_id == 2 ? 'nsfw' : (item.tag_id == 1 ? 'sfw' : 'null')) }}" data-bg="/t/{{ item.id }}.webp" data-size="{{ enable_dynamic_thumbs ? (item.thumb_size || 1) : 1 }}">
<div class="thumb-indicators"> <div class="thumb-indicators">

View File

@@ -86,14 +86,19 @@
<span>{{ t('settings.show_motd') }}</span> <span>{{ t('settings.show_motd') }}</span>
</label> </label>
</div> </div>
<div class="setting-item"> <div class="setting-item" style="margin-bottom: 15px;">
<label for="use_new_layout_toggle" style="cursor: pointer; display: flex; align-items: center; gap: 10px;"> <label for="feed_layout_select" style="display: flex; flex-direction: column; gap: 6px;">
<input type="checkbox" id="use_new_layout_toggle" @if(session.use_new_layout===true) checked @endif> <span>{{ t('settings.feed_layout') }}</span>
<span>{{ t('settings.modern_layout') }}</span> <small class="text-muted">{{ t('settings.feed_layout_hint') }}</small>
</label> </label>
<small class="text-muted" style="margin-left: 25px;">{{ t('settings.modern_layout_hint') }}</small> <select id="feed_layout_select" style="margin-top: 6px; background: var(--bg-secondary, #1a1a1a); color: var(--text, #fff); border: 1px solid var(--nav-border-color, #333); border-radius: 4px; padding: 6px 10px; font-size: 0.9em; width: 100%; max-width: 340px;">
<option value="0" {{ (session.feed_layout === 0 || session.feed_layout === undefined || session.feed_layout === null) ? 'selected' : '' }}>{{ t('settings.feed_layout_grid') }}</option>
<option value="1" {{ session.feed_layout === 1 ? 'selected' : '' }}>{{ t('settings.feed_layout_modern') }}</option>
<option value="2" {{ session.feed_layout === 2 ? 'selected' : '' }}>{{ t('settings.feed_layout_feed') }}</option>
<option value="3" {{ session.feed_layout === 3 ? 'selected' : '' }}>{{ t('settings.feed_layout_youtube') }}</option>
</select>
</div> </div>
@if(!session.use_new_layout) @if(session.feed_layout !== 2 && session.feed_layout !== 3)
<div class="setting-item" style="margin-top: 15px;"> <div class="setting-item" style="margin-top: 15px;">
<label for="alternative_infobox_toggle" <label for="alternative_infobox_toggle"
style="cursor: pointer; display: flex; align-items: center; gap: 10px;"> style="cursor: pointer; display: flex; align-items: center; gap: 10px;">

View File

@@ -411,7 +411,9 @@
fileupload_comments_multifile: @if(fileupload_comments_multifile) true @else false @endif, fileupload_comments_multifile: @if(fileupload_comments_multifile) true @else false @endif,
fileupload_comments_size: {{ fileupload_comments_size }}, fileupload_comments_size: {{ fileupload_comments_size }},
fileupload_comments_max: {{ fileupload_comments_max }}, fileupload_comments_max: {{ fileupload_comments_max }},
fileupload_comments_mode: "{{ fileupload_comments_mode }}" fileupload_comments_mode: "{{ fileupload_comments_mode }}",
feed_layout: @if(session){{ session.feed_layout !== undefined ? session.feed_layout : 0 }}@else 0 @endif,
default_feed_layout: {{ default_feed_layout !== undefined ? default_feed_layout : 0 }}
}; };
window.f0ckDebug = window.f0ckSession.development ? console.log.bind(console) : () => {}; window.f0ckDebug = window.f0ckSession.development ? console.log.bind(console) : () => {};
window.f0ckI18n = { window.f0ckI18n = {