This commit is contained in:
2026-09-05 01:30:24 +02:00
parent 90e515af8a
commit a5c45d693a
16 changed files with 48 additions and 49 deletions
+4 -12
View File
@@ -1,14 +1,11 @@
/* =============================================
f0ckm.css - Unified Stylesheet
Merged: f0ck.css + w0bm.css + view styles
============================================= */
/* f0ckwork omega */ /* f0ckwork omega */
/* originally written by sirx for f0ck.me */ /* originally written by sirx for f0ck.me */
/* heavily modified for f0ckm by Kibi Kelburton */ /* heavily modified for f0ckm by Kibi Kelburton */
/* use whatever you like */ /* use whatever you like */
/* once upon a time this was a stiefelstrapse! but no more! */ /* once upon a time this was a stiefelstrapse! but no more! */
/* This used to be a soulful attempt but ended up in being artificially enhanced! */
:root { :root {
--white: #fff; --white: #fff;
--black: #000; --black: #000;
@@ -1087,9 +1084,6 @@ html[theme='light'] button,
html[theme='light'] .btn, html[theme='light'] .btn,
html[theme='paper'] button, html[theme='paper'] button,
html[theme='paper'] .btn { html[theme='paper'] .btn {
background: #ffffff;
color: #334155;
border: 1px solid #cbd5e1;
} }
html[theme='light'] button:hover, html[theme='light'] button:hover,
@@ -10834,8 +10828,6 @@ body.layout-legacy .comment-content img.emoji {
filter: brightness(1.2); filter: brightness(1.2);
} }
/* === SITE OVERRIDES (formerly w0bm.css) === */
.navbar-brand { .navbar-brand {
display: grid; display: grid;
grid-template-columns: 1fr auto; grid-template-columns: 1fr auto;
@@ -11009,9 +11001,9 @@ html[theme="f0ck95"] .embed-responsive.embed-responsive-16by9 {
background-color: transparent; background-color: transparent;
} }
.v0ck_player_controls { /* .v0ck_player_controls {
background: rgba(0, 0, 0, 0.10) !important; background: rgba(0, 0, 0, 0.10) !important;
} } */
html[theme="term"] .image-brand { html[theme="term"] .image-brand {
filter: hue-rotate(-50deg); filter: hue-rotate(-50deg);
+2 -2
View File
@@ -106,7 +106,7 @@
} }
.v0ck_player_button svg:hover { .v0ck_player_button svg:hover {
filter: drop-shadow(0 0 9px var(--accent)); filter: drop-shadow(0 0 1px var(--accent));
fill: #000; fill: #000;
stroke: var(--accent); stroke: var(--accent);
stroke-width: 30px; stroke-width: 30px;
@@ -132,7 +132,7 @@
padding: 0; padding: 0;
align-items: center; align-items: center;
z-index: 2; z-index: 2;
background: linear-gradient(0deg, rgba(0, 0, 0, 0.8) 20%, rgba(0, 0, 0, 0) 100%); background: linear-gradient(0deg, rgba(0, 0, 0, 0.8) 1%, rgba(0, 0, 0, 0) 100%);
transition: opacity .3s, transform .3s; transition: opacity .3s, transform .3s;
flex-wrap: wrap; flex-wrap: wrap;
transform: translateY(100%) translateY(-3px); transform: translateY(100%) translateY(-3px);
+2 -1
View File
@@ -745,7 +745,8 @@ class CommentSystem {
// Render skeleton (Result: Layout visible immediately) // Render skeleton (Result: Layout visible immediately)
// Skip when preserveScroll=true (tab re-focus refresh): the user already sees comments, // Skip when preserveScroll=true (tab re-focus refresh): the user already sees comments,
// so wiping the DOM causes the browser to lose the #c anchor element and auto-scroll to top. // so wiping the DOM causes the browser to lose the #c anchor element and auto-scroll to top.
if (!scrollToId && !preserveScroll) { // Only render skeleton for logged-in users to avoid flashing "Login to comment" before comments/permissions load.
if (!scrollToId && !preserveScroll && this.user) {
this.render([], this.user, initialIsSubscribed); this.render([], this.user, initialIsSubscribed);
this.restoreState(state); this.restoreState(state);
} }
+22 -11
View File
@@ -9168,7 +9168,7 @@ class NotificationSystem {
// Restore mention placeholders // Restore mention placeholders
content = content.replace(/\x02MNT(\d+)\x03/g, (_, i) => mentionStore[+i]); content = content.replace(/\x02MNT(\d+)\x03/g, (_, i) => mentionStore[+i]);
} catch (e) { } catch (e) {
console.error('Marked error in w0bm.js', e); console.error('Marked error:', e);
// Fallback // Fallback
content = content.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;"); content = content.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
content = content.replace(/^&gt;(.*)/gm, '<span class="greentext">&gt;$1</span>'); content = content.replace(/^&gt;(.*)/gm, '<span class="greentext">&gt;$1</span>');
@@ -10575,7 +10575,8 @@ document.addEventListener('DOMContentLoaded', () => {
const myHallsError = document.getElementById('my-halls-modal-error'); const myHallsError = document.getElementById('my-halls-modal-error');
const isLoggedIn = window.f0ckSession && window.f0ckSession.logged_in; const isLoggedIn = window.f0ckSession && window.f0ckSession.logged_in;
const csrf = window.f0ckSession?.csrf_token || ''; const getCsrf = () => (window.f0ckSession && window.f0ckSession.csrf_token) || '';
const csrf = getCsrf();
if (myHallsSection) { if (myHallsSection) {
myHallsSection.style.display = isLoggedIn ? '' : 'none'; myHallsSection.style.display = isLoggedIn ? '' : 'none';
@@ -10696,11 +10697,15 @@ document.addEventListener('DOMContentLoaded', () => {
confirmBtn.disabled = true; confirmBtn.disabled = true;
confirmBtn.textContent = window.f0ckI18n?.hall_adding || 'Adding...'; confirmBtn.textContent = window.f0ckI18n?.hall_adding || 'Adding...';
const token = getCsrf();
try { try {
const resp = await fetch('/mod/halls/add', { const resp = await fetch('/mod/halls/add', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, headers: {
body: `id=${itemid}&hall=${encodeURIComponent(hallSlug)}` 'Content-Type': 'application/x-www-form-urlencoded',
'X-CSRF-Token': token
},
body: `id=${itemid}&hall=${encodeURIComponent(hallSlug)}&csrf_token=${encodeURIComponent(token)}`
}); });
const data = await resp.json(); const data = await resp.json();
if (data.success) { if (data.success) {
@@ -10722,6 +10727,7 @@ document.addEventListener('DOMContentLoaded', () => {
const onMyHallConfirm = async () => { const onMyHallConfirm = async () => {
if (!myHallsSelect || !myHallsConfirmBtn || !myHallsError) return; if (!myHallsSelect || !myHallsConfirmBtn || !myHallsError) return;
let hallSlug = myHallsSelect.value; let hallSlug = myHallsSelect.value;
const token = getCsrf();
// Create new hall inline if requested // Create new hall inline if requested
if (!hallSlug && myHallsNewName && myHallsNewName.value.trim()) { if (!hallSlug && myHallsNewName && myHallsNewName.value.trim()) {
@@ -10732,8 +10738,8 @@ document.addEventListener('DOMContentLoaded', () => {
myHallsConfirmBtn.textContent = window.f0ckI18n?.hall_creating || 'Creating…'; myHallsConfirmBtn.textContent = window.f0ckI18n?.hall_creating || 'Creating…';
const r = await fetch('/api/v2/me/halls', { const r = await fetch('/api/v2/me/halls', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json', 'x-csrf-token': csrf }, headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': token },
body: JSON.stringify({ name: newName, slug: newSlug }) body: JSON.stringify({ name: newName, slug: newSlug, csrf_token: token })
}); });
const d = await r.json(); const d = await r.json();
if (!d.success) throw new Error(d.message || d.msg || 'Failed to create'); if (!d.success) throw new Error(d.message || d.msg || 'Failed to create');
@@ -10758,8 +10764,8 @@ document.addEventListener('DOMContentLoaded', () => {
try { try {
const resp = await fetch(`/api/v2/me/halls/${encodeURIComponent(hallSlug)}/items`, { const resp = await fetch(`/api/v2/me/halls/${encodeURIComponent(hallSlug)}/items`, {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json', 'x-csrf-token': csrf }, headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': token },
body: JSON.stringify({ item_id: itemid }) body: JSON.stringify({ item_id: itemid, csrf_token: token })
}); });
const data = await resp.json(); const data = await resp.json();
if (data.success) { if (data.success) {
@@ -10796,11 +10802,15 @@ document.addEventListener('DOMContentLoaded', () => {
if (!confirm(`Remove from hall "${hallSlug}"?`)) return; if (!confirm(`Remove from hall "${hallSlug}"?`)) return;
siteHallRemoveBtn.disabled = true; siteHallRemoveBtn.disabled = true;
siteHallRemoveBtn.textContent = window.f0ckI18n?.hall_removing || 'Removing…'; siteHallRemoveBtn.textContent = window.f0ckI18n?.hall_removing || 'Removing…';
const token = getCsrf();
try { try {
const resp = await fetch('/mod/halls/remove', { const resp = await fetch('/mod/halls/remove', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, headers: {
body: `id=${itemid}&hall=${encodeURIComponent(hallSlug)}` 'Content-Type': 'application/x-www-form-urlencoded',
'X-CSRF-Token': token
},
body: `id=${itemid}&hall=${encodeURIComponent(hallSlug)}&csrf_token=${encodeURIComponent(token)}`
}); });
const data = await resp.json(); const data = await resp.json();
if (data.success) { if (data.success) {
@@ -10825,10 +10835,11 @@ document.addEventListener('DOMContentLoaded', () => {
if (!slug) return; if (!slug) return;
myHallsRemoveBtn.disabled = true; myHallsRemoveBtn.disabled = true;
myHallsRemoveBtn.textContent = 'Removing…'; myHallsRemoveBtn.textContent = 'Removing…';
const token = getCsrf();
try { try {
const resp = await fetch(`/api/v2/me/halls/${encodeURIComponent(slug)}/items/${itemid}`, { const resp = await fetch(`/api/v2/me/halls/${encodeURIComponent(slug)}/items/${itemid}`, {
method: 'DELETE', method: 'DELETE',
headers: { 'x-csrf-token': csrf } headers: { 'X-CSRF-Token': token }
}); });
const data = await resp.json(); const data = await resp.json();
if (data.success) { if (data.success) {
+1 -1
View File
@@ -1,4 +1,4 @@
const CACHE_NAME = 'w0bm-pwa-v9'; const CACHE_NAME = 'f0ckm-pwa';
const ASSETS_TO_CACHE = [ const ASSETS_TO_CACHE = [
'/', '/',
'/s/css/f0ckm.css', '/s/css/f0ckm.css',
+1 -1
View File
@@ -520,7 +520,7 @@
"limit_reached": "Upload limit reached (0/{limit})", "limit_reached": "Upload limit reached (0/{limit})",
"limit_remaining": "{remaining}/{limit} uploads remaining", "limit_remaining": "{remaining}/{limit} uploads remaining",
"auth_required_title": "Authentication Required", "auth_required_title": "Authentication Required",
"auth_required_text": "You must be logged in to upload content to w0bm.", "auth_required_text": "You must be logged in to upload content.",
"login_btn": "Login" "login_btn": "Login"
}, },
"messages": { "messages": {
+1 -1
View File
@@ -514,7 +514,7 @@
"limit_reached": "Uploadlimiet bereikt (0/{limit})", "limit_reached": "Uploadlimiet bereikt (0/{limit})",
"limit_remaining": "{remaining}/{limit} uploads over", "limit_remaining": "{remaining}/{limit} uploads over",
"auth_required_title": "Authenticatie Vereist", "auth_required_title": "Authenticatie Vereist",
"auth_required_text": "Je moet ingelogd zijn om inhoud naar w0bm te uploaden.", "auth_required_text": "Je moet ingelogd zijn om inhoud naar f0ckm te uploaden.",
"login_btn": "Inloggen" "login_btn": "Inloggen"
}, },
"messages": { "messages": {
+1 -1
View File
@@ -515,7 +515,7 @@
"limit_reached": "Aufladierungsgrenze erreicht (0/{limit})", "limit_reached": "Aufladierungsgrenze erreicht (0/{limit})",
"limit_remaining": "{remaining}/{limit} Aufladierungen verbleibend", "limit_remaining": "{remaining}/{limit} Aufladierungen verbleibend",
"auth_required_title": "Authentifizierung erforderlich", "auth_required_title": "Authentifizierung erforderlich",
"auth_required_text": "Sie müssen angemeldet sein, um Inhalte auf w0bm hochzuladen.", "auth_required_text": "Sie müssen angemeldet sein, um Inhalte hochzuladen.",
"login_btn": "Anmeldung" "login_btn": "Anmeldung"
}, },
"messages": { "messages": {
+1 -1
View File
@@ -431,7 +431,7 @@ export default (router, tpl) => {
session: (req.session && req.session.user) ? { ...req.session } : false, session: (req.session && req.session.user) ? { ...req.session } : false,
page_meta: { page_meta: {
title: 'about', title: 'about',
description: 'About w0bm.com', description: 'About',
url: `https://${cfg.main.url.domain}/about` url: `https://${cfg.main.url.domain}/about`
} }
}, req) }, req)
+1 -1
View File
@@ -42,7 +42,7 @@ export default (router, tpl) => {
web_url_upload: !!cfg.websrv.web_url_upload, web_url_upload: !!cfg.websrv.web_url_upload,
page_meta: { page_meta: {
title: 'upload', title: 'upload',
description: 'Upload content to w0bm', description: 'Upload content',
url: `https://${cfg.main.url.domain}/upload` url: `https://${cfg.main.url.domain}/upload`
} }
}, req) }, req)
+5 -5
View File
@@ -299,9 +299,9 @@ export default (router, tpl) => {
router.post(/^\/api\/v2\/me\/halls\/?$/, async (req, res) => { router.post(/^\/api\/v2\/me\/halls\/?$/, async (req, res) => {
if (cfg.websrv.userhalls_enabled === false) return res.writeHead(404, { 'Content-Type': 'application/json' }).end(JSON.stringify({ success: false })); if (cfg.websrv.userhalls_enabled === false) return res.writeHead(404, { 'Content-Type': 'application/json' }).end(JSON.stringify({ success: false }));
if (!requireLogin(req, res)) return; if (!requireLogin(req, res)) return;
const name = (req.post.name || '').trim(); const name = ((req.post?.name ?? req.body?.name) || '').trim();
const slug = slugify(req.post.slug || name); const slug = slugify((req.post?.slug ?? req.body?.slug) || name);
const description = (req.post.description || '').trim() || null; const description = ((req.post?.description ?? req.body?.description) || '').trim() || null;
if (!name || !slug) { if (!name || !slug) {
return res.writeHead(400, { 'Content-Type': 'application/json' }) return res.writeHead(400, { 'Content-Type': 'application/json' })
@@ -319,7 +319,7 @@ export default (router, tpl) => {
if (cfg.websrv.userhalls_enabled === false) return res.writeHead(404, { 'Content-Type': 'application/json' }).end(JSON.stringify({ success: false })); if (cfg.websrv.userhalls_enabled === false) return res.writeHead(404, { 'Content-Type': 'application/json' }).end(JSON.stringify({ success: false }));
if (!requireLogin(req, res)) return; if (!requireLogin(req, res)) return;
const slug = decodeURIComponent(req.params.slug); const slug = decodeURIComponent(req.params.slug);
const { name, slug: newSlugRaw, description, is_private } = req.post; const { name, slug: newSlugRaw, description, is_private } = (req.post || req.body || {});
const newSlug = newSlugRaw ? slugify(newSlugRaw) : undefined; const newSlug = newSlugRaw ? slugify(newSlugRaw) : undefined;
const result = await f0cklib.updateUserHall(req.session.id, slug, { const result = await f0cklib.updateUserHall(req.session.id, slug, {
@@ -361,7 +361,7 @@ export default (router, tpl) => {
router.post(/^\/api\/v2\/me\/halls\/(?<slug>[^/]+)\/items\/?$/, async (req, res) => { router.post(/^\/api\/v2\/me\/halls\/(?<slug>[^/]+)\/items\/?$/, async (req, res) => {
if (!requireLogin(req, res)) return; if (!requireLogin(req, res)) return;
const slug = decodeURIComponent(req.params.slug); const slug = decodeURIComponent(req.params.slug);
const itemId = +req.post.item_id; const itemId = +(req.post?.item_id ?? req.body?.item_id);
if (!itemId) { if (!itemId) {
return res.writeHead(400, { 'Content-Type': 'application/json' }) return res.writeHead(400, { 'Content-Type': 'application/json' })
+1 -1
View File
@@ -328,7 +328,7 @@ export default async bot => {
} }
if (e.type === 'tg' && // proto: tg if (e.type === 'tg' && // proto: tg
!isWCommand && // !w / !w0bm !isWCommand && // !w
!e.raw?.forward_date && // is forwarded? !e.raw?.forward_date && // is forwarded?
!mediagroupids.has(mediaGroupId) // prepared mediagroup? !mediagroupids.has(mediaGroupId) // prepared mediagroup?
) { ) {
+3 -1
View File
@@ -216,17 +216,19 @@
</div> </div>
</div> </div>
@if(session || !hide_comments_from_public)
<div id="comments-container" <div id="comments-container"
data-item-id="{{ item.id }}" data-item-id="{{ item.id }}"
@if(session) data-user="{{ session.user }}" @endif @if(session) data-user="{{ session.user }}" @endif
@if(session && (session.admin || session.is_moderator)) data-is-admin="true" @endif @if(session && (session.admin || session.is_moderator)) data-is-admin="true" @endif
@if(item.is_comments_locked) data-is-locked="true" @endif> @if(item.is_comments_locked) data-is-locked="true" @endif>
@if(!item.is_comments_locked) @if(session && !item.is_comments_locked)
<div class="comment-input main-input"> <div class="comment-input main-input">
<textarea disabled></textarea> <textarea disabled></textarea>
</div> </div>
@endif @endif
</div> </div>
@endif
<button class="mobile-scroll-to-top" title="Back to top" aria-label="Scroll to top"><i class="fa-solid fa-chevron-up"></i></button> <button class="mobile-scroll-to-top" title="Back to top" aria-label="Scroll to top"><i class="fa-solid fa-chevron-up"></i></button>
<script id="initial-subscription" type="application/json">{{ isSubscribed }}</script> <script id="initial-subscription" type="application/json">{{ isSubscribed }}</script>
-8
View File
@@ -1,8 +0,0 @@
@include(snippets/header)
<div id="main">
<div class="matrix-page">
<h1>Matrix</h1>
<p>#w0bm:w0bm.com</p>
</div>
</div>
@include(snippets/footer)
+2 -1
View File
@@ -127,7 +127,7 @@
@endif @endif
@if(private_society && !session) @if(private_society && !session)
<script> <script>
window.f0ckSession = { logged_in: false, enable_xd_score: @if(enable_xd_score) true @else false @endif, default_theme: "{{ default_theme }}", show_content_warning: @if(show_content_warning) true @else false @endif, use_new_layout: @if(default_layout === 'legacy')false @else true @endif, comment_display_mode: {{ comment_display_mode }}, comment_max_length: {{ comment_max_length !== null && comment_max_length !== undefined ? comment_max_length : 'null' }}, development: @if(development) true @else false @endif, allow_comment_deletion: @if(allow_comment_deletion) true @else false @endif, hide_sidebar_default: @if(hide_sidebar_default) true @else false @endif }; window.f0ckSession = { logged_in: false, hide_comments_from_public: @if(hide_comments_from_public) true @else false @endif, enable_xd_score: @if(enable_xd_score) true @else false @endif, default_theme: "{{ default_theme }}", show_content_warning: @if(show_content_warning) true @else false @endif, use_new_layout: @if(default_layout === 'legacy')false @else true @endif, comment_display_mode: {{ comment_display_mode }}, comment_max_length: {{ comment_max_length !== null && comment_max_length !== undefined ? comment_max_length : 'null' }}, development: @if(development) true @else false @endif, allow_comment_deletion: @if(allow_comment_deletion) true @else false @endif, hide_sidebar_default: @if(hide_sidebar_default) true @else false @endif };
window.f0ckDebug = window.f0ckSession.development ? console.log.bind(console) : () => {}; window.f0ckDebug = window.f0ckSession.development ? console.log.bind(console) : () => {};
(() => { (() => {
const loginModal = document.getElementById('login-modal'); const loginModal = document.getElementById('login-modal');
@@ -367,6 +367,7 @@
window.f0ckSession = { window.f0ckSession = {
strict_mode: @if(session && session.strict_mode) true @else false @endif, strict_mode: @if(session && session.strict_mode) true @else false @endif,
logged_in: @if(session) true @else false @endif, logged_in: @if(session) true @else false @endif,
hide_comments_from_public: @if(hide_comments_from_public) true @else false @endif,
use_new_layout: @if(session)@if(session.use_new_layout) true @else false @endif@else @if(default_layout === 'legacy')false @else true @endif@endif, use_new_layout: @if(session)@if(session.use_new_layout) true @else false @endif@else @if(default_layout === 'legacy')false @else true @endif@endif,
csrf_token: "{{ csrf_token }}", csrf_token: "{{ csrf_token }}",
default_theme: "{{ default_theme }}", default_theme: "{{ default_theme }}",
+1 -1
View File
@@ -7,7 +7,7 @@
<video id="my-video" class="embed-responsive-item" width="640" height="360" src="{{ item.dest }}" preload="auto" data-size="{{ item.size }}" loop playsinline></video> <video id="my-video" class="embed-responsive-item" width="640" height="360" src="{{ item.dest }}" preload="auto" data-size="{{ item.size }}" loop playsinline></video>
</div> </div>
@elseif(item.mime.startsWith("audio")) @elseif(item.mime.startsWith("audio"))
<div class="embed-responsive embed-responsive-16by9" style="background: url('@if(item.coverart)//w0bm.com{{ item.coverart }}@else/s/img/200.gif@endif') no-repeat center / contain black;"> <div class="embed-responsive embed-responsive-16by9" style="background: url('@if(item.coverart){{ item.coverart }}@else/s/img/200.gif@endif') no-repeat center / contain black;">
<audio id="my-video" class="embed-responsive-item" preload="auto" loop src="{{ item.dest }}" data-setup="{}" data-size="{{ item.size }}" poster="@if(item.coverart){{ item.coverart }}@else/s/img/200.gif@endif" type="{{ item.mime }}"></audio> <audio id="my-video" class="embed-responsive-item" preload="auto" loop src="{{ item.dest }}" data-setup="{}" data-size="{{ item.size }}" poster="@if(item.coverart){{ item.coverart }}@else/s/img/200.gif@endif" type="{{ item.mime }}"></audio>
<img id="f0ck-audio-cover" src="@if(item.coverart){{ item.coverart }}@else/s/img/200.gif@endif" style="display: none;"> <img id="f0ck-audio-cover" src="@if(item.coverart){{ item.coverart }}@else/s/img/200.gif@endif" style="display: none;">
</div> </div>