Compare commits
60 Commits
f0ckv2
...
chat-overh
| Author | SHA1 | Date | |
|---|---|---|---|
| f803ec27a5 | |||
| e61e24dfe8 | |||
| 44af6ae061 | |||
| 0739d72334 | |||
| 848055446a | |||
| 88e6b6efb9 | |||
| eea70ed62e | |||
| b6c906636c | |||
| 55b5a0ba9f | |||
| 5d60d80fc9 | |||
| 4dfb80b149 | |||
| b319f21cd4 | |||
| 28ed8d985c | |||
| 0f5e795793 | |||
| 78d14a1b56 | |||
| 3f149531da | |||
| 7a4432277d | |||
| 84eee7e13d | |||
| 2206b3eb86 | |||
| 5529724982 | |||
| 9c6070afff | |||
| 5bfa336773 | |||
| 517444f9ce | |||
| edb92ffa2a | |||
| 3344064030 | |||
| e998ba8fc7 | |||
| 6b087efab3 | |||
| 6c9f019646 | |||
| d35437373f | |||
| 6efb67f2f5 | |||
| fecb0f0b0c | |||
| d89c610dcb | |||
| 93c042251a | |||
| 990de4b710 | |||
| 2f9ad4d349 | |||
| 701bee7a9a | |||
| 4b7fba11ad | |||
| 6f8eb6f83f | |||
| d78b31633a | |||
| 294cf2455a | |||
| 663ba13d18 | |||
| 84c3f1ccda | |||
| 4dc7f10ae2 | |||
| 3ff1edf8bf | |||
| 9ad6eaefec | |||
| d4eb0e52d8 | |||
| ce54ed460d | |||
| cddc763c44 | |||
| 61036558a0 | |||
| 3354baf09f | |||
| c3c485af79 | |||
| 8f0e0d9b3a | |||
| fefd3b6ca6 | |||
| 5818a0ff39 | |||
| 036a9d372d | |||
| bef96160db | |||
| a99c17c16b | |||
| 469f699dd7 | |||
| d3f8c87317 | |||
| 568555eab8 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -18,3 +18,4 @@ public/tag_cache
|
|||||||
config.json
|
config.json
|
||||||
bundle.css
|
bundle.css
|
||||||
public/s/fonts/impact.woff
|
public/s/fonts/impact.woff
|
||||||
|
f0ck-svelte/*
|
||||||
@@ -53,11 +53,13 @@
|
|||||||
"amoled"
|
"amoled"
|
||||||
],
|
],
|
||||||
"theme": "amoled",
|
"theme": "amoled",
|
||||||
|
"default_font": "",
|
||||||
"default_layout": "legacy",
|
"default_layout": "legacy",
|
||||||
"custom_favicon": "/s/img/favicon.gif",
|
"custom_favicon": "/s/img/favicon.gif",
|
||||||
"custom_brand_image": [],
|
"custom_brand_image": [],
|
||||||
"custom_navbar_brand_text": "",
|
"custom_navbar_brand_text": "",
|
||||||
"show_koepfe": false,
|
"show_koepfe": false,
|
||||||
|
"hide_sidebar_default": true,
|
||||||
"koepfe": [],
|
"koepfe": [],
|
||||||
"enable_global_chat": true,
|
"enable_global_chat": true,
|
||||||
"enable_danmaku": true,
|
"enable_danmaku": true,
|
||||||
|
|||||||
@@ -99,6 +99,8 @@ services:
|
|||||||
container_name: f0ckm-nginx
|
container_name: f0ckm-nginx
|
||||||
profiles:
|
profiles:
|
||||||
- f0ckm-nginx
|
- f0ckm-nginx
|
||||||
|
environment:
|
||||||
|
- ENABLE_IPV6=true
|
||||||
ports:
|
ports:
|
||||||
- "80:80"
|
- "80:80"
|
||||||
- "443:443"
|
- "443:443"
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -13,8 +13,9 @@
|
|||||||
/* Meme Creator Styles */
|
/* Meme Creator Styles */
|
||||||
|
|
||||||
.meme-creator-container {
|
.meme-creator-container {
|
||||||
|
box-sizing: border-box !important;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
max-width: 1200px;
|
max-width: 1100px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
position: relative;
|
position: relative;
|
||||||
@@ -111,6 +112,7 @@
|
|||||||
|
|
||||||
.canvas-wrapper {
|
.canvas-wrapper {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
max-width: 650px;
|
||||||
min-width: 0; /* Allow shrinking */
|
min-width: 0; /* Allow shrinking */
|
||||||
background: #000;
|
background: #000;
|
||||||
border: 2px solid var(--nav-bg, #2b2b2b);
|
border: 2px solid var(--nav-bg, #2b2b2b);
|
||||||
@@ -238,22 +240,49 @@ canvas#memeCanvas {
|
|||||||
color: var(--white, #fff);
|
color: var(--white, #fff);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Mobile Stacking - Simple 2-Row Layout */
|
/* Mobile Stacking - Clean Flexbox Column Layout */
|
||||||
@media (max-width: 950px) {
|
@media (max-width: 950px) {
|
||||||
.meme-editor-layout {
|
.meme-editor-layout {
|
||||||
display: grid !important;
|
display: flex !important;
|
||||||
grid-template-columns: 1fr !important;
|
flex-direction: column !important;
|
||||||
grid-template-rows: 0.6fr auto !important;
|
align-items: center !important;
|
||||||
gap: 20px;
|
gap: 20px;
|
||||||
}
|
}
|
||||||
.meme-controls {
|
.meme-controls {
|
||||||
|
box-sizing: border-box !important;
|
||||||
width: 100% !important;
|
width: 100% !important;
|
||||||
grid-row: 2;
|
max-width: 500px !important;
|
||||||
overflow: visible !important;
|
overflow: visible !important;
|
||||||
}
|
}
|
||||||
.canvas-wrapper {
|
.canvas-wrapper {
|
||||||
|
box-sizing: border-box !important;
|
||||||
width: 100% !important;
|
width: 100% !important;
|
||||||
grid-row: 1;
|
max-width: 650px !important;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
overflow: visible !important;
|
||||||
|
height: auto !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Stack early when sidebar is open (sidebar is 300px, so 950 + 300 = 1250px)
|
||||||
|
This prevents the canvas from shrinking too much when the sidebar takes space. */
|
||||||
|
@media (max-width: 1250px) {
|
||||||
|
body:not(.sidebar-right-hidden) .meme-editor-layout {
|
||||||
|
display: flex !important;
|
||||||
|
flex-direction: column !important;
|
||||||
|
align-items: center !important;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
body:not(.sidebar-right-hidden) .meme-controls {
|
||||||
|
box-sizing: border-box !important;
|
||||||
|
width: 100% !important;
|
||||||
|
max-width: 500px !important;
|
||||||
|
overflow: visible !important;
|
||||||
|
}
|
||||||
|
body:not(.sidebar-right-hidden) .canvas-wrapper {
|
||||||
|
box-sizing: border-box !important;
|
||||||
|
width: 100% !important;
|
||||||
|
max-width: 650px !important;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
overflow: visible !important;
|
overflow: visible !important;
|
||||||
height: auto !important;
|
height: auto !important;
|
||||||
@@ -294,20 +323,19 @@ canvas#memeCanvas {
|
|||||||
|
|
||||||
/* Sidebar space reservation for meme pages */
|
/* Sidebar space reservation for meme pages */
|
||||||
.meme-layout-wrapper {
|
.meme-layout-wrapper {
|
||||||
|
box-sizing: border-box !important;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
transition: padding-right 0.3s ease-in-out;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 1200px) {
|
@media (min-width: 1000px) {
|
||||||
/* Reserve space for the fixed sidebar so content doesn't flow behind it */
|
/* Reserve space for the fixed sidebar so content doesn't flow behind it */
|
||||||
.meme-layout-wrapper {
|
body:not(.sidebar-right-hidden) .meme-layout-wrapper {
|
||||||
padding-right: 300px;
|
padding-right: 300px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Collapse reserved space when sidebar is hidden */
|
|
||||||
body.sidebar-right-hidden .meme-layout-wrapper {
|
|
||||||
padding-right: 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Collapse reserved space when sidebar is hidden */
|
||||||
|
body.sidebar-right-hidden .meme-layout-wrapper {
|
||||||
|
padding-right: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -155,7 +155,13 @@
|
|||||||
postid,
|
postid,
|
||||||
existingTags: tags,
|
existingTags: tags,
|
||||||
anchorEl: anchor,
|
anchorEl: anchor,
|
||||||
onSubmit: async (tag) => post("/api/v2/tags/" + postid, { tagname: tag }),
|
onSubmit: async (tag) => {
|
||||||
|
const res = await post("/api/v2/tags/" + postid, { tagname: tag });
|
||||||
|
if (res.success && window.invalidateItemCache) {
|
||||||
|
window.invalidateItemCache(postid);
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
},
|
||||||
renderTags
|
renderTags
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ class CommentSystem {
|
|||||||
document.body.classList.remove('sidebar-left-hidden');
|
document.body.classList.remove('sidebar-left-hidden');
|
||||||
const layout = this.container.closest('.item-layout-container');
|
const layout = this.container.closest('.item-layout-container');
|
||||||
if (layout) layout.classList.remove('sidebar-hidden');
|
if (layout) layout.classList.remove('sidebar-hidden');
|
||||||
} else if (isHidden) {
|
} else if (isHidden && (window.innerWidth >= 1000 || !document.body.classList.contains('layout-modern'))) {
|
||||||
this.container.classList.add('faded-out');
|
this.container.classList.add('faded-out');
|
||||||
this.container.style.display = 'none';
|
this.container.style.display = 'none';
|
||||||
document.body.classList.add('sidebar-left-hidden');
|
document.body.classList.add('sidebar-left-hidden');
|
||||||
@@ -126,14 +126,43 @@ class CommentSystem {
|
|||||||
const container = document.getElementById('comments-container');
|
const container = document.getElementById('comments-container');
|
||||||
if (!container) return;
|
if (!container) return;
|
||||||
|
|
||||||
const rect = container.getBoundingClientRect();
|
const currentlyAtBottom = btn.classList.contains('is-at-bottom');
|
||||||
// Detect if we are at the bottom of the comments section
|
let isAtBottom = currentlyAtBottom;
|
||||||
const isAtBottom = rect.bottom < window.innerHeight + 100;
|
|
||||||
|
|
||||||
if (isAtBottom) {
|
// Base the bottom state primarily on whether the input box / end-of-thread marker is visible
|
||||||
|
const bottomElement = container.querySelector('.main-input') || container.querySelector('.lock-notice') || container.querySelector('.login-placeholder');
|
||||||
|
|
||||||
|
if (bottomElement) {
|
||||||
|
const bottomRect = bottomElement.getBoundingClientRect();
|
||||||
|
if (currentlyAtBottom) {
|
||||||
|
// We are pointing UP. Switch to DOWN if we scroll significantly UP away from bottomElement
|
||||||
|
if (bottomRect.top > window.innerHeight + 250) {
|
||||||
|
isAtBottom = false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// We are pointing DOWN. Switch to UP if bottomElement enters viewport or we scroll past it
|
||||||
|
if (bottomRect.top <= window.innerHeight + 50) {
|
||||||
|
isAtBottom = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Fallback to container rect if no bottom element
|
||||||
|
const rect = container.getBoundingClientRect();
|
||||||
|
if (currentlyAtBottom) {
|
||||||
|
if (rect.bottom > window.innerHeight + 250) {
|
||||||
|
isAtBottom = false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (rect.bottom <= window.innerHeight + 50) {
|
||||||
|
isAtBottom = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isAtBottom && !currentlyAtBottom) {
|
||||||
btn.classList.add('is-at-bottom');
|
btn.classList.add('is-at-bottom');
|
||||||
btn.setAttribute('title', 'Scroll to top');
|
btn.setAttribute('title', 'Scroll to top');
|
||||||
} else {
|
} else if (!isAtBottom && currentlyAtBottom) {
|
||||||
btn.classList.remove('is-at-bottom');
|
btn.classList.remove('is-at-bottom');
|
||||||
btn.setAttribute('title', 'Scroll to bottom');
|
btn.setAttribute('title', 'Scroll to bottom');
|
||||||
}
|
}
|
||||||
@@ -567,7 +596,7 @@ class CommentSystem {
|
|||||||
if (cached) cached.content = fullContent;
|
if (cached) cached.content = fullContent;
|
||||||
}
|
}
|
||||||
|
|
||||||
contentEl.dataset.raw = this.escapeHtml(fullContent);
|
contentEl.dataset.raw = fullContent;
|
||||||
contentEl.innerHTML = this.renderCommentContent(fullContent, commentId);
|
contentEl.innerHTML = this.renderCommentContent(fullContent, commentId);
|
||||||
CommentSystem.autoplayConvertedGifs(contentEl);
|
CommentSystem.autoplayConvertedGifs(contentEl);
|
||||||
CommentSystem.playEmojiVideos(contentEl);
|
CommentSystem.playEmojiVideos(contentEl);
|
||||||
@@ -593,6 +622,7 @@ class CommentSystem {
|
|||||||
|
|
||||||
const contentEl = el.querySelector('.comment-content');
|
const contentEl = el.querySelector('.comment-content');
|
||||||
if (contentEl) {
|
if (contentEl) {
|
||||||
|
contentEl.dataset.raw = data.content;
|
||||||
contentEl.innerHTML = this.renderCommentContent(data.content, data.comment_id);
|
contentEl.innerHTML = this.renderCommentContent(data.content, data.comment_id);
|
||||||
CommentSystem.autoplayConvertedGifs(contentEl);
|
CommentSystem.autoplayConvertedGifs(contentEl);
|
||||||
CommentSystem.playEmojiVideos(contentEl);
|
CommentSystem.playEmojiVideos(contentEl);
|
||||||
@@ -828,26 +858,14 @@ class CommentSystem {
|
|||||||
* border-right) is fully restored after the effect completes.
|
* border-right) is fully restored after the effect completes.
|
||||||
*
|
*
|
||||||
* @param {Element} el - The .comment element to animate.
|
* @param {Element} el - The .comment element to animate.
|
||||||
* @param {boolean} entering - True to also add 'comment-entering' (own post);
|
* @param {boolean} entering - Unused (kept for compatibility).
|
||||||
* false for 'new-item-fade' only (live others).
|
|
||||||
*/
|
*/
|
||||||
static _animateNewComment(el, entering = false) {
|
static _animateNewComment(el, entering = false) {
|
||||||
if (!el) return;
|
if (!el) return;
|
||||||
|
el.classList.add('new-item-fade');
|
||||||
const classes = entering
|
setTimeout(() => {
|
||||||
? ['comment-entering', 'new-item-fade']
|
el.classList.remove('new-item-fade');
|
||||||
: ['new-item-fade'];
|
}, 2500);
|
||||||
|
|
||||||
// Reset in case the element already has a stale animation
|
|
||||||
classes.forEach(c => el.classList.remove(c));
|
|
||||||
void el.offsetWidth; // force reflow so re-adding the class restarts the animation
|
|
||||||
|
|
||||||
classes.forEach(c => el.classList.add(c));
|
|
||||||
|
|
||||||
const cleanup = () => {
|
|
||||||
classes.forEach(c => el.classList.remove(c));
|
|
||||||
};
|
|
||||||
el.addEventListener('animationend', cleanup, { once: true });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -863,7 +881,6 @@ class CommentSystem {
|
|||||||
if (el) {
|
if (el) {
|
||||||
// Remove highlight from any previously highlighted or newly-posted comment
|
// Remove highlight from any previously highlighted or newly-posted comment
|
||||||
document.querySelectorAll('.comment-highlighted').forEach(c => c.classList.remove('comment-highlighted'));
|
document.querySelectorAll('.comment-highlighted').forEach(c => c.classList.remove('comment-highlighted'));
|
||||||
document.querySelectorAll('.comment-entering').forEach(c => c.classList.remove('comment-entering'));
|
|
||||||
document.querySelectorAll('.new-item-fade').forEach(c => c.classList.remove('new-item-fade'));
|
document.querySelectorAll('.new-item-fade').forEach(c => c.classList.remove('new-item-fade'));
|
||||||
|
|
||||||
// Always smooth-scroll so there's no jarring jump
|
// Always smooth-scroll so there's no jarring jump
|
||||||
@@ -1009,7 +1026,7 @@ class CommentSystem {
|
|||||||
preview.dataset.id = targetId;
|
preview.dataset.id = targetId;
|
||||||
|
|
||||||
// Remove temporary animation/highlight classes
|
// Remove temporary animation/highlight classes
|
||||||
preview.classList.remove('new-item-fade', 'comment-highlighted', 'comment-entering');
|
preview.classList.remove('new-item-fade', 'comment-highlighted');
|
||||||
|
|
||||||
// Remove input forms or action buttons
|
// Remove input forms or action buttons
|
||||||
const actions = preview.querySelector('.comment-actions');
|
const actions = preview.querySelector('.comment-actions');
|
||||||
@@ -1144,7 +1161,12 @@ class CommentSystem {
|
|||||||
const contentEl = body.querySelector('.comment-content');
|
const contentEl = body.querySelector('.comment-content');
|
||||||
if (contentEl) {
|
if (contentEl) {
|
||||||
const LINE_MAX = 200;
|
const LINE_MAX = 200;
|
||||||
const rawText = (contentEl.dataset.raw || '').trim();
|
let rawText = (contentEl.dataset.raw || '').trim();
|
||||||
|
if (rawText.includes('>') || rawText.includes('<') || rawText.includes('&')) {
|
||||||
|
const txt = document.createElement('textarea');
|
||||||
|
txt.innerHTML = rawText;
|
||||||
|
rawText = txt.value;
|
||||||
|
}
|
||||||
// Preserve all lines but cap any single line exceeding LINE_MAX chars
|
// Preserve all lines but cap any single line exceeding LINE_MAX chars
|
||||||
const lines = rawText.split('\n').map(line =>
|
const lines = rawText.split('\n').map(line =>
|
||||||
line.length > LINE_MAX ? line.substring(0, LINE_MAX) + '\u2026' : line
|
line.length > LINE_MAX ? line.substring(0, LINE_MAX) + '\u2026' : line
|
||||||
@@ -1765,7 +1787,7 @@ class CommentSystem {
|
|||||||
const renderedContent = quoteEmojis
|
const renderedContent = quoteEmojis
|
||||||
? quoteContent.replace(/:([a-z0-9_]+):/g, (m, n) => this.renderEmoji(m, n))
|
? quoteContent.replace(/:([a-z0-9_]+):/g, (m, n) => this.renderEmoji(m, n))
|
||||||
: quoteContent;
|
: quoteContent;
|
||||||
return `<span class="greentext">>${renderedContent}</span>`;
|
return `<span class="greentext">>${renderedContent.replace(/>/g, '>')}</span>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Per-line limit to prevent marked.parse recursion on single giant lines
|
// 2. Per-line limit to prevent marked.parse recursion on single giant lines
|
||||||
@@ -2170,7 +2192,7 @@ class CommentSystem {
|
|||||||
if (!unsafe) return '';
|
if (!unsafe) return '';
|
||||||
const div = document.createElement('div');
|
const div = document.createElement('div');
|
||||||
div.textContent = unsafe;
|
div.textContent = unsafe;
|
||||||
return div.innerHTML;
|
return div.innerHTML.replace(/"/g, '"').replace(/'/g, ''');
|
||||||
}
|
}
|
||||||
|
|
||||||
renderCommentAttachments(files, content = '') {
|
renderCommentAttachments(files, content = '') {
|
||||||
@@ -3307,9 +3329,15 @@ class CommentSystem {
|
|||||||
params.append('has_poll', '1');
|
params.append('has_poll', '1');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const csrfToken = window.f0ckSession?.csrf_token || '';
|
||||||
|
if (csrfToken) params.append('csrf_token', csrfToken);
|
||||||
|
|
||||||
const res = await fetch('/api/comments', {
|
const res = await fetch('/api/comments', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
...(csrfToken ? { 'X-CSRF-Token': csrfToken } : {})
|
||||||
|
},
|
||||||
body: params
|
body: params
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -3779,6 +3807,14 @@ class CommentSystem {
|
|||||||
toggleComments() {
|
toggleComments() {
|
||||||
if (!this.container) return;
|
if (!this.container) return;
|
||||||
|
|
||||||
|
if (document.body.classList.contains('layout-modern')) {
|
||||||
|
if (window.innerWidth < 1000) return;
|
||||||
|
if (typeof window.toggleSidebarLeft === 'function') {
|
||||||
|
window.toggleSidebarLeft();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const layout = this.container.closest('.item-layout-container');
|
const layout = this.container.closest('.item-layout-container');
|
||||||
const sidebar = this.container.closest('.item-sidebar-left');
|
const sidebar = this.container.closest('.item-sidebar-left');
|
||||||
const siblings = sidebar ? [
|
const siblings = sidebar ? [
|
||||||
@@ -4423,20 +4459,71 @@ class CommentSystem {
|
|||||||
}, { passive: true });
|
}, { passive: true });
|
||||||
};
|
};
|
||||||
|
|
||||||
// Helper: reliable touch handling for tab buttons on mobile
|
// Helper: reliable touch handling for tab buttons on mobile (supports hold-to-action)
|
||||||
const addTabTouch = (btn, cb) => {
|
let lastHoldTime = 0;
|
||||||
|
const addTabTouch = (btn, cb, onHold) => {
|
||||||
let ts = null;
|
let ts = null;
|
||||||
|
let holdTimer = null;
|
||||||
|
let holdFired = false;
|
||||||
btn.addEventListener('touchstart', (e) => {
|
btn.addEventListener('touchstart', (e) => {
|
||||||
|
holdFired = false;
|
||||||
ts = { x: e.touches[0].clientX, y: e.touches[0].clientY };
|
ts = { x: e.touches[0].clientX, y: e.touches[0].clientY };
|
||||||
|
if (onHold) {
|
||||||
|
holdTimer = setTimeout(() => {
|
||||||
|
holdFired = true;
|
||||||
|
lastHoldTime = Date.now();
|
||||||
|
try { navigator.vibrate?.(40); } catch(err) {}
|
||||||
|
onHold();
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
}, { passive: true });
|
}, { passive: true });
|
||||||
|
|
||||||
|
btn.addEventListener('touchmove', (e) => {
|
||||||
|
if (!ts) return;
|
||||||
|
const dx = Math.abs(e.touches[0].clientX - ts.x);
|
||||||
|
const dy = Math.abs(e.touches[0].clientY - ts.y);
|
||||||
|
if (dx > 10 || dy > 10) {
|
||||||
|
clearTimeout(holdTimer);
|
||||||
|
ts = null;
|
||||||
|
}
|
||||||
|
}, { passive: true });
|
||||||
|
|
||||||
btn.addEventListener('touchend', (e) => {
|
btn.addEventListener('touchend', (e) => {
|
||||||
|
clearTimeout(holdTimer);
|
||||||
|
if (holdFired) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
holdFired = false;
|
||||||
|
ts = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!ts) return;
|
if (!ts) return;
|
||||||
const dx = Math.abs(e.changedTouches[0].clientX - ts.x);
|
const dx = Math.abs(e.changedTouches[0].clientX - ts.x);
|
||||||
const dy = Math.abs(e.changedTouches[0].clientY - ts.y);
|
const dy = Math.abs(e.changedTouches[0].clientY - ts.y);
|
||||||
ts = null;
|
ts = null;
|
||||||
if (dx < 10 && dy < 10) { e.preventDefault(); cb(); }
|
if (dx < 10 && dy < 10) { e.preventDefault(); cb(); }
|
||||||
});
|
});
|
||||||
|
|
||||||
|
btn.addEventListener('touchcancel', () => {
|
||||||
|
clearTimeout(holdTimer);
|
||||||
|
holdFired = false;
|
||||||
|
ts = null;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleFavDefault = (tabEl) => {
|
||||||
|
const KEY = 'f0ck_emoji_default_tab';
|
||||||
|
const isDefault = localStorage.getItem(KEY) === '__favs__';
|
||||||
|
if (isDefault) {
|
||||||
|
localStorage.removeItem(KEY);
|
||||||
|
tabEl.classList.remove('ep-tab-default');
|
||||||
|
window._showEmojiToast?.('Favorites unset as default tab');
|
||||||
|
} else {
|
||||||
|
localStorage.setItem(KEY, '__favs__');
|
||||||
|
tabEl.classList.add('ep-tab-default');
|
||||||
|
window._showEmojiToast?.('Favorites set as default tab');
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Build tab buttons — Favorites first
|
// Build tab buttons — Favorites first
|
||||||
@@ -4448,23 +4535,13 @@ class CommentSystem {
|
|||||||
favTab.innerHTML = '<i class="fa-solid fa-star" style="color:var(--emoji-fav-color,#f5c518);font-size:1.1em;"></i>';
|
favTab.innerHTML = '<i class="fa-solid fa-star" style="color:var(--emoji-fav-color,#f5c518);font-size:1.1em;"></i>';
|
||||||
favTab.addEventListener('mousedown', e => e.preventDefault());
|
favTab.addEventListener('mousedown', e => e.preventDefault());
|
||||||
favTab.addEventListener('click', () => showTab('__favs__'));
|
favTab.addEventListener('click', () => showTab('__favs__'));
|
||||||
addTabTouch(favTab, () => showTab('__favs__'));
|
addTabTouch(favTab, () => showTab('__favs__'), () => toggleFavDefault(favTab));
|
||||||
// Right-click on fav tab → set/unset as default
|
// Right-click on fav tab → set/unset as default
|
||||||
favTab.addEventListener('contextmenu', (e) => {
|
favTab.addEventListener('contextmenu', (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
if (navigator.maxTouchPoints > 0) return;
|
if (Date.now() - lastHoldTime < 1000) return;
|
||||||
const KEY = 'f0ck_emoji_default_tab';
|
toggleFavDefault(favTab);
|
||||||
const isDefault = localStorage.getItem(KEY) === '__favs__';
|
|
||||||
if (isDefault) {
|
|
||||||
localStorage.removeItem(KEY);
|
|
||||||
favTab.classList.remove('ep-tab-default');
|
|
||||||
window._showEmojiToast?.('Favorites unset as default tab');
|
|
||||||
} else {
|
|
||||||
localStorage.setItem(KEY, '__favs__');
|
|
||||||
favTab.classList.add('ep-tab-default');
|
|
||||||
window._showEmojiToast?.('Favorites set as default tab');
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
// Mark as default if already set
|
// Mark as default if already set
|
||||||
if (localStorage.getItem('f0ck_emoji_default_tab') === '__favs__') {
|
if (localStorage.getItem('f0ck_emoji_default_tab') === '__favs__') {
|
||||||
|
|||||||
@@ -46,11 +46,19 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Mark internal drags to prevent them from triggering the dropzone
|
||||||
|
window.addEventListener('dragstart', (e) => {
|
||||||
|
if (e.dataTransfer) {
|
||||||
|
e.dataTransfer.setData('application/x-f0ck-internal', 'true');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Global Drag Events
|
// Global Drag Events
|
||||||
window.addEventListener('dragenter', (e) => {
|
window.addEventListener('dragenter', (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (window.location.pathname === '/upload') return;
|
if (window.location.pathname === '/upload') return;
|
||||||
if (e.dataTransfer.types.includes('Files')) {
|
if (e.dataTransfer && e.dataTransfer.types.includes('application/x-f0ck-internal')) return;
|
||||||
|
if (e.dataTransfer && e.dataTransfer.types.includes('Files')) {
|
||||||
dragCounter++;
|
dragCounter++;
|
||||||
dropOverlay.classList.add('active');
|
dropOverlay.classList.add('active');
|
||||||
}
|
}
|
||||||
@@ -59,10 +67,17 @@
|
|||||||
window.addEventListener('dragover', (e) => {
|
window.addEventListener('dragover', (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (window.location.pathname === '/upload') return;
|
if (window.location.pathname === '/upload') return;
|
||||||
|
if (e.dataTransfer && e.dataTransfer.types.includes('application/x-f0ck-internal')) {
|
||||||
|
// Change dropEffect to none to indicate this dropzone doesn't accept internal elements
|
||||||
|
e.dataTransfer.dropEffect = 'none';
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
window.addEventListener('dragleave', (e) => {
|
window.addEventListener('dragleave', (e) => {
|
||||||
if (window.location.pathname === '/upload') return;
|
if (window.location.pathname === '/upload') return;
|
||||||
|
// dragleave doesn't always have reliable dataTransfer.types, but if it does, check it
|
||||||
|
if (e.dataTransfer && e.dataTransfer.types.includes('application/x-f0ck-internal')) return;
|
||||||
|
|
||||||
dragCounter--;
|
dragCounter--;
|
||||||
if (dragCounter <= 0) {
|
if (dragCounter <= 0) {
|
||||||
dragCounter = 0;
|
dragCounter = 0;
|
||||||
@@ -72,6 +87,8 @@
|
|||||||
|
|
||||||
window.addEventListener('drop', (e) => {
|
window.addEventListener('drop', (e) => {
|
||||||
if (window.location.pathname === '/upload') return;
|
if (window.location.pathname === '/upload') return;
|
||||||
|
if (e.dataTransfer && e.dataTransfer.types.includes('application/x-f0ck-internal')) return;
|
||||||
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
dragCounter = 0;
|
dragCounter = 0;
|
||||||
dropOverlay.classList.remove('active');
|
dropOverlay.classList.remove('active');
|
||||||
|
|||||||
@@ -2301,15 +2301,55 @@ window.cancelAnimFrame = (function () {
|
|||||||
let hiddenValue = localStorage.getItem('sidebarRightHidden');
|
let hiddenValue = localStorage.getItem('sidebarRightHidden');
|
||||||
let hidden;
|
let hidden;
|
||||||
if (hiddenValue === null) {
|
if (hiddenValue === null) {
|
||||||
hidden = false; // Default to visible on all devices
|
hidden = (window.f0ckSession && window.f0ckSession.hide_sidebar_default) ? true : false; // Default based on config
|
||||||
localStorage.setItem('sidebarRightHidden', hidden);
|
localStorage.setItem('sidebarRightHidden', hidden);
|
||||||
} else {
|
} else {
|
||||||
hidden = hiddenValue === 'true';
|
hidden = hiddenValue === 'true';
|
||||||
}
|
}
|
||||||
document.body.classList.toggle('sidebar-right-hidden', hidden);
|
document.body.classList.toggle('sidebar-right-hidden', hidden);
|
||||||
|
|
||||||
|
// Always force-hide on very small screens regardless of saved preference
|
||||||
|
if (window.innerWidth <= 599) {
|
||||||
|
document.body.classList.add('sidebar-right-hidden');
|
||||||
|
}
|
||||||
};
|
};
|
||||||
window.initSidebarRightToggle();
|
window.initSidebarRightToggle();
|
||||||
|
|
||||||
|
// Auto-hide sidebar on very small screens (≤599px), restore on wider
|
||||||
|
(() => {
|
||||||
|
const mq = window.matchMedia('(max-width: 599px)');
|
||||||
|
const applyBreakpoint = (e) => {
|
||||||
|
// Freeze transitions so the class change is instant (no sliding during resize)
|
||||||
|
const sidebar = document.querySelector('.global-sidebar-right, .item-sidebar-right, .index-sidebar-right');
|
||||||
|
const wrappers = document.querySelectorAll('.index-layout-wrapper, .item-layout-container, .pagination-container-fluid, .pagewrapper');
|
||||||
|
if (sidebar) sidebar.style.setProperty('transition', 'none', 'important');
|
||||||
|
wrappers.forEach(w => w.style.setProperty('transition', 'none', 'important'));
|
||||||
|
|
||||||
|
if (e.matches) {
|
||||||
|
// Entering narrow: force-hide without touching localStorage
|
||||||
|
document.body.classList.add('sidebar-right-hidden');
|
||||||
|
} else {
|
||||||
|
// Leaving narrow: restore from saved preference
|
||||||
|
const saved = localStorage.getItem('sidebarRightHidden');
|
||||||
|
const hidden = saved === 'true';
|
||||||
|
document.body.classList.toggle('sidebar-right-hidden', hidden);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Force reflow then restore transitions
|
||||||
|
if (sidebar) void sidebar.offsetWidth;
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
if (sidebar) sidebar.style.removeProperty('transition');
|
||||||
|
wrappers.forEach(w => {
|
||||||
|
w.style.removeProperty('transition');
|
||||||
|
w.style.removeProperty('padding-right');
|
||||||
|
w.style.removeProperty('right');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
mq.addEventListener('change', applyBreakpoint);
|
||||||
|
applyBreakpoint(mq); // run once on init
|
||||||
|
})();
|
||||||
|
|
||||||
const loadPageAjax = async (url, replace = true, options = {}) => {
|
const loadPageAjax = async (url, replace = true, options = {}) => {
|
||||||
if (isNavigating) return;
|
if (isNavigating) return;
|
||||||
|
|
||||||
@@ -2398,7 +2438,7 @@ window.cancelAnimFrame = (function () {
|
|||||||
const isAdmin = !!pathname.match(/^\/admin/);
|
const isAdmin = !!pathname.match(/^\/admin/);
|
||||||
const isMod = !!pathname.match(/^\/mod/);
|
const isMod = !!pathname.match(/^\/mod/);
|
||||||
const isSettings = pathname.match(/\/settings\/?(?:$|\?)/);
|
const isSettings = pathname.match(/\/settings\/?(?:$|\?)/);
|
||||||
const isStatic = pathname.match(/\/(about|rules|terms|upload|subscriptions|stats|docs|discord|ranking|meme|memes)($|\/|\?)/);
|
const isStatic = pathname.match(/\/(about|rules|terms|upload|subscriptions|stats|docs|discord|ranking|meme|memes|chat)($|\/|\?)/);
|
||||||
const isUpload = pathname.match(/\/upload\/?(?:$|\?)/);
|
const isUpload = pathname.match(/\/upload\/?(?:$|\?)/);
|
||||||
const parts = pathname.split('/').filter(Boolean);
|
const parts = pathname.split('/').filter(Boolean);
|
||||||
const isItem = !pathname.match(/\/p\//) && (
|
const isItem = !pathname.match(/\/p\//) && (
|
||||||
@@ -2406,8 +2446,9 @@ window.cancelAnimFrame = (function () {
|
|||||||
(parts.length >= 3 && (parts[0] === 'tag' || parts[0] === 'user' || parts[0] === 'h') && /^\d+$/.test(parts[parts.length - 1]))
|
(parts.length >= 3 && (parts[0] === 'tag' || parts[0] === 'user' || parts[0] === 'h') && /^\d+$/.test(parts[parts.length - 1]))
|
||||||
);
|
);
|
||||||
const isMessages = !!pathname.match(/^\/messages(\/|$)/);
|
const isMessages = !!pathname.match(/^\/messages(\/|$)/);
|
||||||
|
const isChat = !!pathname.match(/^\/chat(\/|$)/);
|
||||||
const isAbyss = !!pathname.match(/^\/abyss(\/|$|\?|#)/) || pathname === '/abyss';
|
const isAbyss = !!pathname.match(/^\/abyss(\/|$|\?|#)/) || pathname === '/abyss';
|
||||||
const isGrid = !isProfile && !isUserHall && !isUserHalls && !isHall && !isHalls && !isTags && !isComments && !isNotifs && !isItem && !isAdmin && !isMod && !isSettings && !isStatic && !isUpload && !isMessages && !isAbyss;
|
const isGrid = !isProfile && !isUserHall && !isUserHalls && !isHall && !isHalls && !isTags && !isComments && !isNotifs && !isItem && !isAdmin && !isMod && !isSettings && !isStatic && !isUpload && !isMessages && !isChat && !isAbyss;
|
||||||
|
|
||||||
if (isItem) {
|
if (isItem) {
|
||||||
isNavigating = false;
|
isNavigating = false;
|
||||||
@@ -2603,6 +2644,10 @@ window.cancelAnimFrame = (function () {
|
|||||||
if (isFav) ajaxUrl += `&fav=true`;
|
if (isFav) ajaxUrl += `&fav=true`;
|
||||||
if (mime) ajaxUrl += `&mime=${encodeURIComponent(mime)}`;
|
if (mime) ajaxUrl += `&mime=${encodeURIComponent(mime)}`;
|
||||||
|
|
||||||
|
// Preserve tagger filter from URL query string
|
||||||
|
const taggerMatch = urlObj.searchParams.get('tagger');
|
||||||
|
if (taggerMatch) ajaxUrl += `&tagger=${encodeURIComponent(taggerMatch)}`;
|
||||||
|
|
||||||
const isRandom = document.cookie.includes('random_mode=1') || url.includes('random=1') || window.location.search.includes('random=1');
|
const isRandom = document.cookie.includes('random_mode=1') || url.includes('random=1') || window.location.search.includes('random=1');
|
||||||
if (isRandom) ajaxUrl += `&random=1`;
|
if (isRandom) ajaxUrl += `&random=1`;
|
||||||
const isStrict = window.f0ckSession?.strict_mode || (localStorage.getItem('search_strict') === 'true');
|
const isStrict = window.f0ckSession?.strict_mode || (localStorage.getItem('search_strict') === 'true');
|
||||||
@@ -3144,6 +3189,9 @@ window.cancelAnimFrame = (function () {
|
|||||||
window.location.href = url; // Fallback
|
window.location.href = url; // Fallback
|
||||||
} finally {
|
} finally {
|
||||||
isNavigating = false;
|
isNavigating = false;
|
||||||
|
if (replace && document.activeElement && document.activeElement !== document.body) {
|
||||||
|
document.activeElement.blur();
|
||||||
|
}
|
||||||
if (navbar) navbar.classList.remove("pbwork");
|
if (navbar) navbar.classList.remove("pbwork");
|
||||||
if (window.updateStrictLinks && window.f0ckSession) window.updateStrictLinks(window.f0ckSession.strict_mode);
|
if (window.updateStrictLinks && window.f0ckSession) window.updateStrictLinks(window.f0ckSession.strict_mode);
|
||||||
}
|
}
|
||||||
@@ -3324,10 +3372,15 @@ window.cancelAnimFrame = (function () {
|
|||||||
const itemid = numericSegments.pop();
|
const itemid = numericSegments.pop();
|
||||||
|
|
||||||
// Extract context from Target URL first
|
// Extract context from Target URL first
|
||||||
let tag = null, user = null, isFavs = false, mime = null, hall = null, userHall = null, userHallOwner = null;
|
let tag = null, user = null, isFavs = false, mime = null, hall = null, userHall = null, userHallOwner = null, tagger = null;
|
||||||
const tagMatch = url.match(/\/tag\/([^/?]+)/);
|
const tagMatch = url.match(/\/tag\/([^/?]+)/);
|
||||||
if (tagMatch) tag = decodeURIComponent(tagMatch[1]);
|
if (tagMatch) tag = decodeURIComponent(tagMatch[1]);
|
||||||
|
|
||||||
|
// Tagger filter from query string
|
||||||
|
const taggerUrlObj = new URL(url, window.location.origin);
|
||||||
|
const taggerParam = taggerUrlObj.searchParams.get('tagger');
|
||||||
|
if (taggerParam) tagger = decodeURIComponent(taggerParam);
|
||||||
|
|
||||||
// User hall: /user/:owner/hall/:slug/:id
|
// User hall: /user/:owner/hall/:slug/:id
|
||||||
const userHallMatch = url.match(/\/user\/([^/]+)\/hall\/([^/]+)/);
|
const userHallMatch = url.match(/\/user\/([^/]+)\/hall\/([^/]+)/);
|
||||||
if (userHallMatch) {
|
if (userHallMatch) {
|
||||||
@@ -3365,6 +3418,10 @@ window.cancelAnimFrame = (function () {
|
|||||||
const wTagMatch = window.location.href.match(/\/tag\/([^/?]+)/);
|
const wTagMatch = window.location.href.match(/\/tag\/([^/?]+)/);
|
||||||
if (wTagMatch) tag = decodeURIComponent(wTagMatch[1]);
|
if (wTagMatch) tag = decodeURIComponent(wTagMatch[1]);
|
||||||
}
|
}
|
||||||
|
if (!tagger) {
|
||||||
|
const wTaggerParam = new URL(window.location.href).searchParams.get('tagger');
|
||||||
|
if (wTaggerParam) tagger = decodeURIComponent(wTaggerParam);
|
||||||
|
}
|
||||||
if (!user && !userHall) {
|
if (!user && !userHall) {
|
||||||
const wUserMatch = window.location.href.match(/\/user\/([^/?]+)/);
|
const wUserMatch = window.location.href.match(/\/user\/([^/?]+)/);
|
||||||
if (wUserMatch && !window.location.href.match(/\/user\/[^/]+\/hall\//) ) {
|
if (wUserMatch && !window.location.href.match(/\/user\/[^/]+\/hall\//) ) {
|
||||||
@@ -3400,6 +3457,7 @@ window.cancelAnimFrame = (function () {
|
|||||||
} else if (user) {
|
} else if (user) {
|
||||||
params.append('user', user);
|
params.append('user', user);
|
||||||
}
|
}
|
||||||
|
if (tagger) params.append('tagger', tagger);
|
||||||
if (isFavs) params.append('fav', 'true');
|
if (isFavs) params.append('fav', 'true');
|
||||||
|
|
||||||
const isStrict = window.f0ckSession?.strict_mode || (localStorage.getItem('search_strict') === 'true');
|
const isStrict = window.f0ckSession?.strict_mode || (localStorage.getItem('search_strict') === 'true');
|
||||||
@@ -3499,6 +3557,7 @@ window.cancelAnimFrame = (function () {
|
|||||||
else if (user) { _pushUrl = `/user/${encodeURIComponent(user)}/${itemid}`; if (isFavs) _pushUrl = `/user/${encodeURIComponent(user)}/favs/${itemid}`; }
|
else if (user) { _pushUrl = `/user/${encodeURIComponent(user)}/${itemid}`; if (isFavs) _pushUrl = `/user/${encodeURIComponent(user)}/favs/${itemid}`; }
|
||||||
else if (tag) _pushUrl = `/tag/${encodeURIComponent(tag).replace(/%2C/g,',').replace(/%20/g,' ')}/${itemid}`;
|
else if (tag) _pushUrl = `/tag/${encodeURIComponent(tag).replace(/%2C/g,',').replace(/%20/g,' ')}/${itemid}`;
|
||||||
else if (hall) _pushUrl = `/h/${encodeURIComponent(hall).replace(/%20/g,' ')}/${itemid}`;
|
else if (hall) _pushUrl = `/h/${encodeURIComponent(hall).replace(/%20/g,' ')}/${itemid}`;
|
||||||
|
if (tagger && tag) _pushUrl += `?tagger=${encodeURIComponent(tagger)}`;
|
||||||
if (mime) _pushUrl = _pushUrl.replace(new RegExp(`/${itemid}$`), `/${mime}/${itemid}`);
|
if (mime) _pushUrl = _pushUrl.replace(new RegExp(`/${itemid}$`), `/${mime}/${itemid}`);
|
||||||
if (_hash) _pushUrl += _hash;
|
if (_hash) _pushUrl += _hash;
|
||||||
|
|
||||||
@@ -3701,6 +3760,8 @@ window.cancelAnimFrame = (function () {
|
|||||||
}
|
}
|
||||||
else if (tag) pushUrl = `/tag/${encodeURIComponent(tag).replace(/%2C/g, ',').replace(/%20/g, ' ')}/${itemid}`;
|
else if (tag) pushUrl = `/tag/${encodeURIComponent(tag).replace(/%2C/g, ',').replace(/%20/g, ' ')}/${itemid}`;
|
||||||
else if (hall) pushUrl = `/h/${encodeURIComponent(hall).replace(/%20/g, ' ')}/${itemid}`;
|
else if (hall) pushUrl = `/h/${encodeURIComponent(hall).replace(/%20/g, ' ')}/${itemid}`;
|
||||||
|
// Append tagger filter so item nav stays in tagger context
|
||||||
|
if (tagger && tag) pushUrl += `?tagger=${encodeURIComponent(tagger)}`;
|
||||||
|
|
||||||
if (mime) {
|
if (mime) {
|
||||||
// If it already has itemid at the end, insert mime before it
|
// If it already has itemid at the end, insert mime before it
|
||||||
@@ -4019,7 +4080,7 @@ window.cancelAnimFrame = (function () {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isSpecialLink && (pathname === '/' || pathname.startsWith('/halls') || pathname.startsWith('/h/') || pathname.startsWith('/notifications') || pathname.startsWith('/tag') || pathname.startsWith('/user/') || pathname.match(/^\/(image|video|audio)/) || pathname.match(/\/p\/\d+/) || pathname.match(/^\/\d+/) || pathname.match(/^\/(about|rules|terms|upload|subscriptions|stats|docs|settings|admin|mod|ranking|messages|meme|memes)/) || pathname.startsWith('/abyss'))) {
|
if (!isSpecialLink && (pathname === '/' || pathname.startsWith('/halls') || pathname.startsWith('/h/') || pathname.startsWith('/notifications') || pathname.startsWith('/tag') || pathname.startsWith('/user/') || pathname.match(/^\/(image|video|audio)/) || pathname.match(/\/p\/\d+/) || pathname.match(/^\/\d+/) || pathname.match(/^\/(about|rules|terms|upload|subscriptions|stats|docs|settings|admin|mod|ranking|messages|meme|memes|chat)/) || pathname.startsWith('/abyss'))) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopImmediatePropagation();
|
e.stopImmediatePropagation();
|
||||||
|
|
||||||
@@ -4407,6 +4468,10 @@ window.cancelAnimFrame = (function () {
|
|||||||
if (window.renderTags) {
|
if (window.renderTags) {
|
||||||
window.renderTags(res.tags);
|
window.renderTags(res.tags);
|
||||||
}
|
}
|
||||||
|
// Evict the cached item HTML so navigating back fetches fresh content
|
||||||
|
if (window.invalidateItemCache) {
|
||||||
|
window.invalidateItemCache(postid);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
revert();
|
revert();
|
||||||
window.flashMessage('Error: ' + (res.msg || 'Failed to update rating'), 3000, 'error');
|
window.flashMessage('Error: ' + (res.msg || 'Failed to update rating'), 3000, 'error');
|
||||||
@@ -4469,7 +4534,7 @@ window.cancelAnimFrame = (function () {
|
|||||||
(parts.length >= 3 && parts[0] === 'tag' && /^\d+$/.test(parts[parts.length - 1])) ||
|
(parts.length >= 3 && parts[0] === 'tag' && /^\d+$/.test(parts[parts.length - 1])) ||
|
||||||
(parts.length >= 3 && parts[0] === 'user' && /^\d+$/.test(parts[parts.length - 1]))
|
(parts.length >= 3 && parts[0] === 'user' && /^\d+$/.test(parts[parts.length - 1]))
|
||||||
);
|
);
|
||||||
const isSpecial = p.startsWith('/notifications') || p.startsWith('/tags') || p.startsWith('/user/') || p.startsWith('/subscriptions') || p.startsWith('/ranking');
|
const isSpecial = p.startsWith('/notifications') || p.startsWith('/tags') || p.startsWith('/user/') || p.startsWith('/subscriptions') || p.startsWith('/ranking') || p.startsWith('/chat');
|
||||||
const isGridLike = url.match(/\/p\/\d+/) || url.match(/[?&]page=\d+/) || p === '/';
|
const isGridLike = url.match(/\/p\/\d+/) || url.match(/[?&]page=\d+/) || p === '/';
|
||||||
|
|
||||||
window.f0ckDebug("[popstate] Navigation to:", url, { isItem, isSpecial, isGridLike });
|
window.f0ckDebug("[popstate] Navigation to:", url, { isItem, isSpecial, isGridLike });
|
||||||
@@ -6257,8 +6322,10 @@ window.cancelAnimFrame = (function () {
|
|||||||
if (settled) return;
|
if (settled) return;
|
||||||
settled = true;
|
settled = true;
|
||||||
|
|
||||||
// Freeze all transitions (override CSS !important) so the class toggle
|
// Freeze sidebar AND wrapper transitions so the class toggle
|
||||||
// doesn't trigger an additional right-property animation.
|
// doesn't trigger a second padding-right animation.
|
||||||
|
const wrappers = getSidebarWrappers();
|
||||||
|
wrappers.forEach(w => w.style.setProperty('transition', 'none', 'important'));
|
||||||
sidebar.style.setProperty('transition', 'none', 'important');
|
sidebar.style.setProperty('transition', 'none', 'important');
|
||||||
|
|
||||||
if (stateChanging) {
|
if (stateChanging) {
|
||||||
@@ -6276,6 +6343,11 @@ window.cancelAnimFrame = (function () {
|
|||||||
// Restore CSS-controlled transitions on the next frame
|
// Restore CSS-controlled transitions on the next frame
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
sidebar.style.removeProperty('transition');
|
sidebar.style.removeProperty('transition');
|
||||||
|
wrappers.forEach(w => {
|
||||||
|
w.style.removeProperty('transition');
|
||||||
|
w.style.removeProperty('padding-right');
|
||||||
|
w.style.removeProperty('right');
|
||||||
|
});
|
||||||
swipeRT.isDraggingSidebar = false;
|
swipeRT.isDraggingSidebar = false;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -6402,22 +6474,13 @@ window.cancelAnimFrame = (function () {
|
|||||||
const sidebar = document.querySelector('.global-sidebar-right, .item-sidebar-right, .index-sidebar-right');
|
const sidebar = document.querySelector('.global-sidebar-right, .item-sidebar-right, .index-sidebar-right');
|
||||||
if (!sidebar) return;
|
if (!sidebar) return;
|
||||||
|
|
||||||
mouseRT.active = true;
|
|
||||||
mouseRT.isDragging = false;
|
|
||||||
mouseRT.isCandidate = false;
|
|
||||||
mouseRT.xDown = e.clientX;
|
|
||||||
mouseRT.yDown = e.clientY;
|
|
||||||
mouseRT.xDiff = 0;
|
|
||||||
mouseRT.yDiff = 0;
|
|
||||||
mouseRT.timeDown = Date.now();
|
|
||||||
mouseRT.sidebarEl = sidebar;
|
|
||||||
mouseRT.sidebarWidth = sidebar.offsetWidth;
|
|
||||||
|
|
||||||
const isHidden = document.body.classList.contains('sidebar-right-hidden');
|
const isHidden = document.body.classList.contains('sidebar-right-hidden');
|
||||||
|
let candidate = false;
|
||||||
|
|
||||||
if (isHidden) {
|
if (isHidden) {
|
||||||
// Open zone: 80px from right edge
|
// Open zone: 80px from right edge
|
||||||
if (e.clientX > window.innerWidth - 30) {
|
if (e.clientX > window.innerWidth - 30) {
|
||||||
mouseRT.isCandidate = true;
|
candidate = true;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Close: only from the left-edge grab strip (first 16px of sidebar) OR the edgeZone overhang
|
// Close: only from the left-edge grab strip (first 16px of sidebar) OR the edgeZone overhang
|
||||||
@@ -6425,9 +6488,22 @@ window.cancelAnimFrame = (function () {
|
|||||||
const sidebarLeft = sidebar.getBoundingClientRect().left;
|
const sidebarLeft = sidebar.getBoundingClientRect().left;
|
||||||
const inGrabStrip = e.clientX >= sidebarLeft && e.clientX <= sidebarLeft + 16;
|
const inGrabStrip = e.clientX >= sidebarLeft && e.clientX <= sidebarLeft + 16;
|
||||||
if (inGrabStrip || e.target === dragZone) {
|
if (inGrabStrip || e.target === dragZone) {
|
||||||
mouseRT.isCandidate = true;
|
candidate = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!candidate) return;
|
||||||
|
|
||||||
|
mouseRT.active = true;
|
||||||
|
mouseRT.isDragging = false;
|
||||||
|
mouseRT.isCandidate = true;
|
||||||
|
mouseRT.xDown = e.clientX;
|
||||||
|
mouseRT.yDown = e.clientY;
|
||||||
|
mouseRT.xDiff = 0;
|
||||||
|
mouseRT.yDiff = 0;
|
||||||
|
mouseRT.timeDown = Date.now();
|
||||||
|
mouseRT.sidebarEl = sidebar;
|
||||||
|
mouseRT.sidebarWidth = sidebar.offsetWidth;
|
||||||
});
|
});
|
||||||
|
|
||||||
document.addEventListener('mousemove', e => {
|
document.addEventListener('mousemove', e => {
|
||||||
@@ -6447,6 +6523,8 @@ window.cancelAnimFrame = (function () {
|
|||||||
window.getSelection()?.removeAllRanges(); // clear any selection that formed before threshold
|
window.getSelection()?.removeAllRanges(); // clear any selection that formed before threshold
|
||||||
} else if (absY > absX) {
|
} else if (absY > absX) {
|
||||||
mouseRT.isCandidate = false; // Scrolling — bail
|
mouseRT.isCandidate = false; // Scrolling — bail
|
||||||
|
mouseRT.active = false;
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -6507,6 +6585,10 @@ window.cancelAnimFrame = (function () {
|
|||||||
const settle = () => {
|
const settle = () => {
|
||||||
if (settled) return;
|
if (settled) return;
|
||||||
settled = true;
|
settled = true;
|
||||||
|
// Freeze sidebar AND wrapper transitions so the class toggle
|
||||||
|
// doesn't trigger a second padding-right animation.
|
||||||
|
const wrappers = getSidebarWrappers();
|
||||||
|
wrappers.forEach(w => w.style.setProperty('transition', 'none', 'important'));
|
||||||
sidebar.style.setProperty('transition', 'none', 'important');
|
sidebar.style.setProperty('transition', 'none', 'important');
|
||||||
|
|
||||||
if (stateChanging) {
|
if (stateChanging) {
|
||||||
@@ -6519,6 +6601,11 @@ window.cancelAnimFrame = (function () {
|
|||||||
sidebar.style.transform = '';
|
sidebar.style.transform = '';
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
sidebar.style.removeProperty('transition');
|
sidebar.style.removeProperty('transition');
|
||||||
|
wrappers.forEach(w => {
|
||||||
|
w.style.removeProperty('transition');
|
||||||
|
w.style.removeProperty('padding-right');
|
||||||
|
w.style.removeProperty('right');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -6586,15 +6673,55 @@ window.cancelAnimFrame = (function () {
|
|||||||
});
|
});
|
||||||
window.addEventListener('resize', syncEdgeZone, { passive: true });
|
window.addEventListener('resize', syncEdgeZone, { passive: true });
|
||||||
|
|
||||||
// Touchend on edgeZone: instant tap-to-toggle (no 300ms synthetic click delay)
|
// Returns true if (clientX, clientY) is within the pill's hit area.
|
||||||
|
// The pill is #sidebar-drag-zone::after — 4×40px, centered on pointer:fine,
|
||||||
|
// right-aligned with padding-right:6px on pointer:coarse.
|
||||||
|
const isWithinPillArea = (clientX, clientY) => {
|
||||||
|
const rect = edgeZone.getBoundingClientRect();
|
||||||
|
const pillH = 40;
|
||||||
|
const hitPad = 12; // generous padding so it's easy to hit
|
||||||
|
// Vertical: pill is always centered in the drag zone
|
||||||
|
const pillCenterY = rect.top + rect.height / 2;
|
||||||
|
if (clientY < pillCenterY - pillH / 2 - hitPad || clientY > pillCenterY + pillH / 2 + hitPad) return false;
|
||||||
|
// Horizontal: right-aligned on touch, centered on mouse
|
||||||
|
const isCoarse = window.matchMedia('(pointer: coarse)').matches;
|
||||||
|
const pillCenterX = isCoarse ? rect.right - 6 - 2 : rect.left + rect.width / 2;
|
||||||
|
return clientX >= pillCenterX - 2 - hitPad && clientX <= pillCenterX + 2 + hitPad;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Touchend: tap on the pill toggles sidebar; rest of drag zone only responds to swipe
|
||||||
edgeZone.addEventListener('touchend', e => {
|
edgeZone.addEventListener('touchend', e => {
|
||||||
// Only if it was a clean tap (no significant swipe movement)
|
|
||||||
if (Math.abs(swipeRT.xDiff || 0) < 15 && Math.abs(swipeRT.yDiff || 0) < 15) {
|
if (Math.abs(swipeRT.xDiff || 0) < 15 && Math.abs(swipeRT.yDiff || 0) < 15) {
|
||||||
e.preventDefault();
|
e.preventDefault(); // always prevent 300ms synthetic click
|
||||||
edgeZone.dispatchEvent(new Event('click'));
|
const touch = e.changedTouches[0];
|
||||||
|
if (touch && isWithinPillArea(touch.clientX, touch.clientY)) {
|
||||||
|
window.toggleSidebarRight();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, { passive: false });
|
}, { passive: false });
|
||||||
|
|
||||||
|
// Helper: all layout wrappers that have padding-right animated by sidebar state
|
||||||
|
const getSidebarWrappers = () => {
|
||||||
|
// At ≤999px: sidebar is a pure overlay — only pagination tracks it.
|
||||||
|
if (window.innerWidth <= 599) {
|
||||||
|
return document.querySelectorAll('.pagination-container-fluid');
|
||||||
|
}
|
||||||
|
// At ≥600px: exclude pagewrapper when a dedicated layout wrapper is present.
|
||||||
|
// CSS already zeroes pagewrapper.padding-right via :has on those pages,
|
||||||
|
// so including it here causes double-padding (pagewrapper + index-layout-wrapper both
|
||||||
|
// get 300px snapped, making the grid 600px too narrow, then jumping back on settle).
|
||||||
|
const hasDedicatedWrapper = !!(
|
||||||
|
document.querySelector('.index-layout-wrapper') ||
|
||||||
|
document.querySelector('.item-layout-container') ||
|
||||||
|
document.querySelector('.meme-layout-wrapper') ||
|
||||||
|
document.querySelector('.messages-convo-page')
|
||||||
|
);
|
||||||
|
const sel = hasDedicatedWrapper
|
||||||
|
? '.index-layout-wrapper, .meme-layout-wrapper, .messages-convo-page, .pagination-container-fluid'
|
||||||
|
: '.pagination-container-fluid, .pagewrapper';
|
||||||
|
return document.querySelectorAll(sel);
|
||||||
|
};
|
||||||
|
|
||||||
// Smoothly toggle sidebar open/closed
|
// Smoothly toggle sidebar open/closed
|
||||||
window.toggleSidebarRight = () => {
|
window.toggleSidebarRight = () => {
|
||||||
const sidebar = document.querySelector('.global-sidebar-right, .item-sidebar-right, .index-sidebar-right');
|
const sidebar = document.querySelector('.global-sidebar-right, .item-sidebar-right, .index-sidebar-right');
|
||||||
@@ -6604,6 +6731,29 @@ window.cancelAnimFrame = (function () {
|
|||||||
const sidebarWidth = sidebar.offsetWidth || 300;
|
const sidebarWidth = sidebar.offsetWidth || 300;
|
||||||
const targetTranslate = isHidden ? -sidebarWidth : sidebarWidth;
|
const targetTranslate = isHidden ? -sidebarWidth : sidebarWidth;
|
||||||
|
|
||||||
|
// At ≤599px the sidebar is a pure overlay — never animate wrapper padding
|
||||||
|
const animateWrappers = window.innerWidth > 599;
|
||||||
|
|
||||||
|
// Grid/pagewrapper: snap instantly in both directions (no animated reflow).
|
||||||
|
// Pagination always animates — it tracks sidebar position with no reflow cost.
|
||||||
|
const wrappers = getSidebarWrappers();
|
||||||
|
if (animateWrappers) {
|
||||||
|
wrappers.forEach(w => {
|
||||||
|
const isPagination = w.classList.contains('pagination-container-fluid');
|
||||||
|
const isMessages = w.classList.contains('messages-convo-page');
|
||||||
|
|
||||||
|
const prop = (isPagination || isMessages) ? 'right' : 'padding-right';
|
||||||
|
const targetVal = isHidden ? sidebarWidth + 'px' : '0px';
|
||||||
|
|
||||||
|
// Lightweight layouts (pagination, messages) animate smoothly.
|
||||||
|
// Heavy/reflow-intensive layouts snap instantly to avoid reflow lag.
|
||||||
|
const shouldAnimate = (isPagination || isMessages);
|
||||||
|
w.style.setProperty('transition', shouldAnimate ? `${prop} 0.3s ease-in-out` : 'none', 'important');
|
||||||
|
w.style.setProperty(prop, targetVal, 'important');
|
||||||
|
});
|
||||||
|
void sidebar.offsetWidth; // settle layout before sidebar transform begins
|
||||||
|
}
|
||||||
|
|
||||||
sidebar.style.setProperty('transition', 'transform 0.3s ease-in-out', 'important');
|
sidebar.style.setProperty('transition', 'transform 0.3s ease-in-out', 'important');
|
||||||
sidebar.style.transform = `translateX(${targetTranslate}px)`;
|
sidebar.style.transform = `translateX(${targetTranslate}px)`;
|
||||||
|
|
||||||
@@ -6611,13 +6761,22 @@ window.cancelAnimFrame = (function () {
|
|||||||
const settle = () => {
|
const settle = () => {
|
||||||
if (settled) return;
|
if (settled) return;
|
||||||
settled = true;
|
settled = true;
|
||||||
|
// Freeze wrapper transitions so the class commit doesn't re-trigger them
|
||||||
|
wrappers.forEach(w => w.style.setProperty('transition', 'none', 'important'));
|
||||||
sidebar.style.setProperty('transition', 'none', 'important');
|
sidebar.style.setProperty('transition', 'none', 'important');
|
||||||
const toHidden = !isHidden;
|
const toHidden = !isHidden;
|
||||||
document.body.classList.toggle('sidebar-right-hidden', toHidden);
|
document.body.classList.toggle('sidebar-right-hidden', toHidden);
|
||||||
localStorage.setItem('sidebarRightHidden', toHidden);
|
localStorage.setItem('sidebarRightHidden', toHidden);
|
||||||
void sidebar.offsetWidth;
|
void sidebar.offsetWidth;
|
||||||
sidebar.style.transform = '';
|
sidebar.style.transform = '';
|
||||||
requestAnimationFrame(() => sidebar.style.removeProperty('transition'));
|
requestAnimationFrame(() => {
|
||||||
|
sidebar.style.removeProperty('transition');
|
||||||
|
wrappers.forEach(w => {
|
||||||
|
w.style.removeProperty('transition');
|
||||||
|
w.style.removeProperty('padding-right');
|
||||||
|
w.style.removeProperty('right');
|
||||||
|
});
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const onEnd = ev => {
|
const onEnd = ev => {
|
||||||
@@ -6629,9 +6788,11 @@ window.cancelAnimFrame = (function () {
|
|||||||
setTimeout(settle, 350);
|
setTimeout(settle, 350);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Click on edgeZone: smoothly toggle sidebar open/closed
|
// Click on edgeZone: only toggle when the click lands on the pill
|
||||||
edgeZone.addEventListener('click', () => {
|
edgeZone.addEventListener('click', (e) => {
|
||||||
window.toggleSidebarRight();
|
if (isWithinPillArea(e.clientX, e.clientY)) {
|
||||||
|
window.toggleSidebarRight();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
// </mouse-sidebar-drag>
|
// </mouse-sidebar-drag>
|
||||||
|
|
||||||
@@ -6651,7 +6812,9 @@ window.cancelAnimFrame = (function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const isLSActive = () =>
|
const isLSActive = () =>
|
||||||
document.body.classList.contains('layout-modern') && !!document.querySelector('.item-sidebar-left');
|
document.body.classList.contains('layout-modern') &&
|
||||||
|
!!document.querySelector('.item-sidebar-left') &&
|
||||||
|
window.innerWidth >= 1000;
|
||||||
|
|
||||||
// Prepare sidebar for drag — keeps it in the CSS grid so overflow:hidden clips translateX.
|
// Prepare sidebar for drag — keeps it in the CSS grid so overflow:hidden clips translateX.
|
||||||
// OPEN: removes sidebar-hidden/display:none, places sidebar at translateX(-sw), reflows twice.
|
// OPEN: removes sidebar-hidden/display:none, places sidebar at translateX(-sw), reflows twice.
|
||||||
@@ -6887,7 +7050,10 @@ window.cancelAnimFrame = (function () {
|
|||||||
// ── Edge zone & sync ────────────────────────────────────────────────────
|
// ── Edge zone & sync ────────────────────────────────────────────────────
|
||||||
const syncLeftEdgeZone = () => {
|
const syncLeftEdgeZone = () => {
|
||||||
const { sb, layout } = getLS();
|
const { sb, layout } = getLS();
|
||||||
if (!sb || !layout || !document.body.classList.contains('layout-modern')) {
|
// The left drag zone only applies to the 3-column desktop grid (≥1000px).
|
||||||
|
// On mobile the sidebar becomes in-flow, so hide the zone to avoid it
|
||||||
|
// landing next to the right-sidebar drag zone on narrow screens.
|
||||||
|
if (!sb || !layout || !document.body.classList.contains('layout-modern') || window.innerWidth < 1000) {
|
||||||
leftEdgeZone.style.display = 'none';
|
leftEdgeZone.style.display = 'none';
|
||||||
leftEdgeZone.style.pointerEvents = 'none';
|
leftEdgeZone.style.pointerEvents = 'none';
|
||||||
return;
|
return;
|
||||||
@@ -6907,6 +7073,7 @@ window.cancelAnimFrame = (function () {
|
|||||||
};
|
};
|
||||||
|
|
||||||
window.toggleSidebarLeft = () => {
|
window.toggleSidebarLeft = () => {
|
||||||
|
if (window.innerWidth < 1000) return;
|
||||||
const { sb, layout } = getLS();
|
const { sb, layout } = getLS();
|
||||||
if (!sb || !layout) return;
|
if (!sb || !layout) return;
|
||||||
const isHidden = layout.classList.contains('sidebar-hidden');
|
const isHidden = layout.classList.contains('sidebar-hidden');
|
||||||
@@ -6944,6 +7111,19 @@ window.cancelAnimFrame = (function () {
|
|||||||
// Export loaders for external systems (like NotificationSystem)
|
// Export loaders for external systems (like NotificationSystem)
|
||||||
window.loadPageAjax = loadPageAjax;
|
window.loadPageAjax = loadPageAjax;
|
||||||
window.loadItemAjax = loadItemAjax;
|
window.loadItemAjax = loadItemAjax;
|
||||||
|
|
||||||
|
// Invalidate all itemCacheMap entries that contain a given item ID.
|
||||||
|
// Call this after mutating an item (e.g. adding a tag) so the next
|
||||||
|
// AJAX navigation fetches fresh HTML instead of serving stale cache.
|
||||||
|
window.invalidateItemCache = (itemId) => {
|
||||||
|
if (!itemId) return;
|
||||||
|
const needle = `/${itemId}`;
|
||||||
|
for (const key of itemCacheMap.keys()) {
|
||||||
|
if (key.includes(needle)) {
|
||||||
|
itemCacheMap.delete(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
// Continued...
|
// Continued...
|
||||||
|
|
||||||
const sbtForm = document.getElementById('sbtForm');
|
const sbtForm = document.getElementById('sbtForm');
|
||||||
@@ -9382,22 +9562,24 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
};
|
};
|
||||||
window.startHoverPreview = startPreview;
|
window.startHoverPreview = startPreview;
|
||||||
|
|
||||||
document.addEventListener('mouseover', (e) => {
|
const handleEnter = (e) => {
|
||||||
const thumb = e.target.closest('.thumb');
|
const thumb = e.target.closest('.thumb');
|
||||||
|
|
||||||
// If we are hovering a thumb, and it's NOT the active one
|
|
||||||
if (thumb && thumb !== activeThumb) {
|
if (thumb && thumb !== activeThumb) {
|
||||||
startPreview(thumb, 150);
|
startPreview(thumb, 150);
|
||||||
}
|
}
|
||||||
});
|
};
|
||||||
|
document.addEventListener('mouseover', handleEnter);
|
||||||
|
document.addEventListener('focusin', handleEnter);
|
||||||
|
|
||||||
document.addEventListener('mouseout', (e) => {
|
const handleLeave = (e) => {
|
||||||
const thumb = e.target.closest('.thumb');
|
const thumb = e.target.closest('.thumb');
|
||||||
if (thumb) {
|
if (thumb) {
|
||||||
if (e.relatedTarget && thumb.contains(e.relatedTarget)) return;
|
if (e.type === 'mouseout' && e.relatedTarget && thumb.contains(e.relatedTarget)) return;
|
||||||
clearPreview();
|
clearPreview();
|
||||||
}
|
}
|
||||||
});
|
};
|
||||||
|
document.addEventListener('mouseout', handleLeave);
|
||||||
|
document.addEventListener('focusout', handleLeave);
|
||||||
|
|
||||||
// Touch handling to support "tap to activate" behavior
|
// Touch handling to support "tap to activate" behavior
|
||||||
// 1. First tap: Activate (visuals + preview) AND prevent navigation
|
// 1. First tap: Activate (visuals + preview) AND prevent navigation
|
||||||
@@ -10188,14 +10370,21 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Only expand if the image is actually being clipped (not fully visible).
|
// Only expand if the image is actually being clipped (not fully visible).
|
||||||
// Compare the image's rendered height to its natural height —
|
|
||||||
// if rendered >= natural, the image already fits fully in the box.
|
|
||||||
const img = wrapper.querySelector('img#f0ck-image');
|
const img = wrapper.querySelector('img#f0ck-image');
|
||||||
if (img && img.complete && img.naturalHeight > 0) {
|
if (img && img.complete && img.naturalHeight > 0 && img.naturalWidth > 0) {
|
||||||
const rendered = img.getBoundingClientRect();
|
const rendered = img.getBoundingClientRect();
|
||||||
// Allow a 2px tolerance for subpixel rounding
|
|
||||||
|
// Calculate the max height the image can take given the container width
|
||||||
|
const expandedHeight = rendered.width * (img.naturalHeight / img.naturalWidth);
|
||||||
|
|
||||||
if (rendered.height + 2 >= img.naturalHeight) {
|
if (rendered.height + 2 >= img.naturalHeight) {
|
||||||
// Fully visible — do nothing (no expand needed, no modal)
|
// Image is unscaled or upscaled. No detail to gain by expanding.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rendered.height + 2 >= expandedHeight) {
|
||||||
|
// Expanding inline will not increase display size because it is width-constrained.
|
||||||
|
openImageModal(elfe.href);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -11268,3 +11457,90 @@ document.addEventListener('click', (e) => {
|
|||||||
}
|
}
|
||||||
}, true);
|
}, true);
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
// --- Grid Keyboard Accessibility ---
|
||||||
|
document.addEventListener('keydown', (e) => {
|
||||||
|
const isTab = e.key === 'Tab';
|
||||||
|
const isArrow = ['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(e.key);
|
||||||
|
if (!isTab && !isArrow) return;
|
||||||
|
|
||||||
|
const thumbs = Array.from(document.querySelectorAll('.posts .thumb'));
|
||||||
|
if (thumbs.length === 0) return;
|
||||||
|
|
||||||
|
if (isTab && document.activeElement === document.body) {
|
||||||
|
e.preventDefault();
|
||||||
|
thumbs[0].focus();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const active = document.activeElement;
|
||||||
|
if (isArrow && active && active.classList.contains('thumb')) {
|
||||||
|
const activeRect = active.getBoundingClientRect();
|
||||||
|
const activeCenter = {
|
||||||
|
x: activeRect.left + activeRect.width / 2,
|
||||||
|
y: activeRect.top + activeRect.height / 2
|
||||||
|
};
|
||||||
|
|
||||||
|
let bestMatch = null;
|
||||||
|
let minDistance = Infinity;
|
||||||
|
|
||||||
|
thumbs.forEach(thumb => {
|
||||||
|
if (thumb === active) return;
|
||||||
|
|
||||||
|
const rect = thumb.getBoundingClientRect();
|
||||||
|
if (rect.width === 0 && rect.height === 0) return;
|
||||||
|
|
||||||
|
const center = {
|
||||||
|
x: rect.left + rect.width / 2,
|
||||||
|
y: rect.top + rect.height / 2
|
||||||
|
};
|
||||||
|
|
||||||
|
const dxCenter = center.x - activeCenter.x;
|
||||||
|
const dyCenter = center.y - activeCenter.y;
|
||||||
|
const absDx = Math.abs(dxCenter);
|
||||||
|
const absDy = Math.abs(dyCenter);
|
||||||
|
|
||||||
|
let isCandidate = false;
|
||||||
|
let primaryDist = 0;
|
||||||
|
let secondaryGap = 0;
|
||||||
|
|
||||||
|
if (e.key === 'ArrowRight') {
|
||||||
|
// Must be to the right, and within a 60 degree cone
|
||||||
|
isCandidate = dxCenter > 0 && absDx >= absDy * 0.5;
|
||||||
|
primaryDist = absDx;
|
||||||
|
// Gap on the Y axis. If they overlap, gap is 0
|
||||||
|
secondaryGap = Math.max(0, Math.max(activeRect.top, rect.top) - Math.min(activeRect.bottom, rect.bottom));
|
||||||
|
} else if (e.key === 'ArrowLeft') {
|
||||||
|
isCandidate = dxCenter < 0 && absDx >= absDy * 0.5;
|
||||||
|
primaryDist = absDx;
|
||||||
|
secondaryGap = Math.max(0, Math.max(activeRect.top, rect.top) - Math.min(activeRect.bottom, rect.bottom));
|
||||||
|
} else if (e.key === 'ArrowDown') {
|
||||||
|
isCandidate = dyCenter > 0 && absDy >= absDx * 0.5;
|
||||||
|
primaryDist = absDy;
|
||||||
|
secondaryGap = Math.max(0, Math.max(activeRect.left, rect.left) - Math.min(activeRect.right, rect.right));
|
||||||
|
} else if (e.key === 'ArrowUp') {
|
||||||
|
isCandidate = dyCenter < 0 && absDy >= absDx * 0.5;
|
||||||
|
primaryDist = absDy;
|
||||||
|
secondaryGap = Math.max(0, Math.max(activeRect.left, rect.left) - Math.min(activeRect.right, rect.right));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isCandidate) {
|
||||||
|
// Massive penalty for gap to force it to pick overlapping items (same row/col)
|
||||||
|
// Tiny penalty for center misalignment so ties are broken cleanly
|
||||||
|
const centerMisalignment = (e.key === 'ArrowLeft' || e.key === 'ArrowRight') ? absDy : absDx;
|
||||||
|
const distance = primaryDist + (secondaryGap * 15) + (centerMisalignment * 0.1);
|
||||||
|
|
||||||
|
if (distance < minDistance) {
|
||||||
|
minDistance = distance;
|
||||||
|
bestMatch = thumb;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (bestMatch) {
|
||||||
|
e.preventDefault();
|
||||||
|
bestMatch.focus();
|
||||||
|
bestMatch.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1976,6 +1976,73 @@ if (window.__dmLoaded) {
|
|||||||
dmGridArea = document.createElement('div');
|
dmGridArea = document.createElement('div');
|
||||||
dmGridArea.className = 'emoji-picker-grid';
|
dmGridArea.className = 'emoji-picker-grid';
|
||||||
|
|
||||||
|
// Helper: reliable touch handling for tab buttons on mobile (supports hold-to-action)
|
||||||
|
let lastDmHoldTime = 0;
|
||||||
|
const addDmTabTouch = (btn, cb, onHold) => {
|
||||||
|
let ts = null;
|
||||||
|
let holdTimer = null;
|
||||||
|
let holdFired = false;
|
||||||
|
btn.addEventListener('touchstart', (e) => {
|
||||||
|
holdFired = false;
|
||||||
|
ts = { x: e.touches[0].clientX, y: e.touches[0].clientY };
|
||||||
|
if (onHold) {
|
||||||
|
holdTimer = setTimeout(() => {
|
||||||
|
holdFired = true;
|
||||||
|
lastDmHoldTime = Date.now();
|
||||||
|
try { navigator.vibrate?.(40); } catch(err) {}
|
||||||
|
onHold();
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
e.stopPropagation();
|
||||||
|
}, { passive: true });
|
||||||
|
|
||||||
|
btn.addEventListener('touchmove', (e) => {
|
||||||
|
if (!ts) return;
|
||||||
|
const dx = Math.abs(e.touches[0].clientX - ts.x);
|
||||||
|
const dy = Math.abs(e.touches[0].clientY - ts.y);
|
||||||
|
if (dx > 10 || dy > 10) {
|
||||||
|
clearTimeout(holdTimer);
|
||||||
|
ts = null;
|
||||||
|
}
|
||||||
|
}, { passive: true });
|
||||||
|
|
||||||
|
btn.addEventListener('touchend', (e) => {
|
||||||
|
clearTimeout(holdTimer);
|
||||||
|
if (holdFired) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
holdFired = false;
|
||||||
|
ts = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!ts) return;
|
||||||
|
const dx = Math.abs(e.changedTouches[0].clientX - ts.x);
|
||||||
|
const dy = Math.abs(e.changedTouches[0].clientY - ts.y);
|
||||||
|
ts = null;
|
||||||
|
if (dx < 10 && dy < 10) { e.preventDefault(); cb(); }
|
||||||
|
});
|
||||||
|
|
||||||
|
btn.addEventListener('touchcancel', () => {
|
||||||
|
clearTimeout(holdTimer);
|
||||||
|
holdFired = false;
|
||||||
|
ts = null;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleDmFavDefault = (tabEl) => {
|
||||||
|
const KEY = 'f0ck_emoji_default_tab';
|
||||||
|
const isDefault = localStorage.getItem(KEY) === '__favs__';
|
||||||
|
if (isDefault) {
|
||||||
|
localStorage.removeItem(KEY);
|
||||||
|
tabEl.classList.remove('ep-tab-default');
|
||||||
|
window._showEmojiToast?.('Favorites unset as default tab');
|
||||||
|
} else {
|
||||||
|
localStorage.setItem(KEY, '__favs__');
|
||||||
|
tabEl.classList.add('ep-tab-default');
|
||||||
|
window._showEmojiToast?.('Favorites set as default tab');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// ── Favorites tab ──
|
// ── Favorites tab ──
|
||||||
const favsTab = document.createElement('button');
|
const favsTab = document.createElement('button');
|
||||||
favsTab.className = 'ep-tab ep-tab-favs';
|
favsTab.className = 'ep-tab ep-tab-favs';
|
||||||
@@ -1985,22 +2052,13 @@ if (window.__dmLoaded) {
|
|||||||
favsTab.innerHTML = '<i class="fa-solid fa-star" style="font-size:14px;color:var(--emoji-fav-color,#f5c518);"></i>';
|
favsTab.innerHTML = '<i class="fa-solid fa-star" style="font-size:14px;color:var(--emoji-fav-color,#f5c518);"></i>';
|
||||||
favsTab.addEventListener('mousedown', e => e.preventDefault());
|
favsTab.addEventListener('mousedown', e => e.preventDefault());
|
||||||
favsTab.addEventListener('click', () => showDmTab('__favs__', packs));
|
favsTab.addEventListener('click', () => showDmTab('__favs__', packs));
|
||||||
|
addDmTabTouch(favsTab, () => showDmTab('__favs__', packs), () => toggleDmFavDefault(favsTab));
|
||||||
// Right-click on fav tab → set/unset as default
|
// Right-click on fav tab → set/unset as default
|
||||||
favsTab.addEventListener('contextmenu', (e) => {
|
favsTab.addEventListener('contextmenu', (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
if (navigator.maxTouchPoints > 0) return;
|
if (Date.now() - lastDmHoldTime < 1000) return;
|
||||||
const KEY = 'f0ck_emoji_default_tab';
|
toggleDmFavDefault(favsTab);
|
||||||
const isDefault = localStorage.getItem(KEY) === '__favs__';
|
|
||||||
if (isDefault) {
|
|
||||||
localStorage.removeItem(KEY);
|
|
||||||
favsTab.classList.remove('ep-tab-default');
|
|
||||||
window._showEmojiToast?.('Favorites unset as default tab');
|
|
||||||
} else {
|
|
||||||
localStorage.setItem(KEY, '__favs__');
|
|
||||||
favsTab.classList.add('ep-tab-default');
|
|
||||||
window._showEmojiToast?.('Favorites set as default tab');
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
// Mark as default if already set
|
// Mark as default if already set
|
||||||
if (localStorage.getItem('f0ck_emoji_default_tab') === '__favs__') {
|
if (localStorage.getItem('f0ck_emoji_default_tab') === '__favs__') {
|
||||||
@@ -2028,6 +2086,7 @@ if (window.__dmLoaded) {
|
|||||||
}
|
}
|
||||||
tab.addEventListener('mousedown', e => e.preventDefault());
|
tab.addEventListener('mousedown', e => e.preventDefault());
|
||||||
tab.addEventListener('click', () => showDmTab(pack.id ?? null, packs));
|
tab.addEventListener('click', () => showDmTab(pack.id ?? null, packs));
|
||||||
|
addDmTabTouch(tab, () => showDmTab(pack.id ?? null, packs));
|
||||||
dmTabBar.appendChild(tab);
|
dmTabBar.appendChild(tab);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1000,9 +1000,10 @@
|
|||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
const itemId = slide.dataset.id;
|
const itemId = slide.dataset.id;
|
||||||
try {
|
try {
|
||||||
|
const csrfToken = window.f0ckSession?.csrf_token || window.scrollerCsrf || '';
|
||||||
const resp = await fetch(`/api/v2/tags/${itemId}/${encodeURIComponent(tag)}`, {
|
const resp = await fetch(`/api/v2/tags/${itemId}/${encodeURIComponent(tag)}`, {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
headers: { 'x-csrf-token': window.scrollerCsrf || '' }
|
headers: { ...(csrfToken ? { 'X-CSRF-Token': csrfToken } : {}) }
|
||||||
});
|
});
|
||||||
const data = await resp.json();
|
const data = await resp.json();
|
||||||
if (data.success) pill.remove();
|
if (data.success) pill.remove();
|
||||||
@@ -1157,10 +1158,14 @@
|
|||||||
if (nowFaved) flashFav(slide);
|
if (nowFaved) flashFav(slide);
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
|
const csrfToken = window.f0ckSession?.csrf_token || window.scrollerCsrf || '';
|
||||||
const resp = await fetch('/api/v2/togglefav', {
|
const resp = await fetch('/api/v2/togglefav', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
headers: {
|
||||||
body: `postid=${id}`
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
...(csrfToken ? { 'X-CSRF-Token': csrfToken } : {})
|
||||||
|
},
|
||||||
|
body: `postid=${id}${csrfToken ? `&csrf_token=${encodeURIComponent(csrfToken)}` : ''}`
|
||||||
});
|
});
|
||||||
const data = await resp.json();
|
const data = await resp.json();
|
||||||
// Sync count to server truth (handles race conditions)
|
// Sync count to server truth (handles race conditions)
|
||||||
@@ -1582,9 +1587,10 @@
|
|||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
const itemId = slide.dataset.id;
|
const itemId = slide.dataset.id;
|
||||||
try {
|
try {
|
||||||
|
const csrfToken = window.f0ckSession?.csrf_token || window.scrollerCsrf || '';
|
||||||
const resp = await fetch(`/api/v2/tags/${itemId}/${encodeURIComponent(t)}`, {
|
const resp = await fetch(`/api/v2/tags/${itemId}/${encodeURIComponent(t)}`, {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
headers: { 'x-csrf-token': window.scrollerCsrf || '' }
|
headers: { ...(csrfToken ? { 'X-CSRF-Token': csrfToken } : {}) }
|
||||||
});
|
});
|
||||||
const data = await resp.json();
|
const data = await resp.json();
|
||||||
if (data.success) pill.remove();
|
if (data.success) pill.remove();
|
||||||
@@ -1688,7 +1694,7 @@
|
|||||||
avatar: '/a/default.png',
|
avatar: '/a/default.png',
|
||||||
stamp: p.time,
|
stamp: p.time,
|
||||||
timeago: timeAgo(p.time * 1000),
|
timeago: timeAgo(p.time * 1000),
|
||||||
tags: `4chan, /${data.board}/`,
|
tags: ``,
|
||||||
is_video: isVideo,
|
is_video: isVideo,
|
||||||
is_image: isImage,
|
is_image: isImage,
|
||||||
is_audio: false,
|
is_audio: false,
|
||||||
@@ -1850,16 +1856,17 @@
|
|||||||
else if (item.external_board === 'gif') rating = 'nsfw';
|
else if (item.external_board === 'gif') rating = 'nsfw';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const csrfToken = window.f0ckSession?.csrf_token || window.scrollerCsrf || '';
|
||||||
const resp = await fetch('/api/v2/scroller/rehost', {
|
const resp = await fetch('/api/v2/scroller/rehost', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/x-www-form-urlencoded',
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
'x-csrf-token': window.scrollerCsrf || ''
|
...(csrfToken ? { 'X-CSRF-Token': csrfToken } : {})
|
||||||
},
|
},
|
||||||
body: new URLSearchParams({
|
body: new URLSearchParams({
|
||||||
url: item.external_media_url || item.dest,
|
url: item.external_media_url || item.dest,
|
||||||
rating: rating,
|
rating: rating,
|
||||||
tags: '4chan',
|
tags: '',
|
||||||
comment: `Rehosted from 4chan thread: ${applied.externalUrl || 'unknown'}`,
|
comment: `Rehosted from 4chan thread: ${applied.externalUrl || 'unknown'}`,
|
||||||
...(item.original_filename ? { original_filename: item.original_filename } : {})
|
...(item.original_filename ? { original_filename: item.original_filename } : {})
|
||||||
})
|
})
|
||||||
@@ -1966,9 +1973,10 @@
|
|||||||
showShareToast(info.toast);
|
showShareToast(info.toast);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const csrfToken = window.f0ckSession?.csrf_token || window.scrollerCsrf || '';
|
||||||
fetch(`/api/v2/tags/${id}/cycle-rating`, {
|
fetch(`/api/v2/tags/${id}/cycle-rating`, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
headers: { 'x-csrf-token': window.scrollerCsrf || '' }
|
headers: { ...(csrfToken ? { 'X-CSRF-Token': csrfToken } : {}) }
|
||||||
})
|
})
|
||||||
.then(r => r.json())
|
.then(r => r.json())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
@@ -2119,7 +2127,12 @@
|
|||||||
const contentEl = commentEl.querySelector('.comment-content');
|
const contentEl = commentEl.querySelector('.comment-content');
|
||||||
if (!contentEl) return;
|
if (!contentEl) return;
|
||||||
|
|
||||||
const raw = (contentEl.dataset.raw || '').replace(/<br\s*\/?>/gi, '\n').trim();
|
let raw = (contentEl.dataset.raw || '').replace(/<br\s*\/?>/gi, '\n').trim();
|
||||||
|
if (raw.includes('>') || raw.includes('<') || raw.includes('&')) {
|
||||||
|
const txt = document.createElement('textarea');
|
||||||
|
txt.innerHTML = raw;
|
||||||
|
raw = txt.value;
|
||||||
|
}
|
||||||
const lines = raw.split('\n');
|
const lines = raw.split('\n');
|
||||||
const quote = `>>${id}\n${lines.map(line => `>${line}`).join('\n')}\n`;
|
const quote = `>>${id}\n${lines.map(line => `>${line}`).join('\n')}\n`;
|
||||||
|
|
||||||
@@ -2787,11 +2800,16 @@
|
|||||||
if (!content || !commentsItemId || commentsPosting) return;
|
if (!content || !commentsItemId || commentsPosting) return;
|
||||||
commentsPosting = true; commentSendBtn.disabled = true;
|
commentsPosting = true; commentSendBtn.disabled = true;
|
||||||
try {
|
try {
|
||||||
|
const csrfToken = window.f0ckSession?.csrf_token || window.scrollerCsrf || '';
|
||||||
let postBody = `item_id=${commentsItemId}&content=${encodeURIComponent(content)}`;
|
let postBody = `item_id=${commentsItemId}&content=${encodeURIComponent(content)}`;
|
||||||
if (replyToCommentId) postBody += `&parent_id=${replyToCommentId}`;
|
if (replyToCommentId) postBody += `&parent_id=${replyToCommentId}`;
|
||||||
|
if (csrfToken) postBody += `&csrf_token=${encodeURIComponent(csrfToken)}`;
|
||||||
const resp = await fetch('/api/comments', {
|
const resp = await fetch('/api/comments', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
...(csrfToken ? { 'X-CSRF-Token': csrfToken } : {})
|
||||||
|
},
|
||||||
body: postBody
|
body: postBody
|
||||||
});
|
});
|
||||||
const data = await resp.json();
|
const data = await resp.json();
|
||||||
@@ -2899,10 +2917,14 @@
|
|||||||
if (addTagSendBtn) addTagSendBtn.disabled = true;
|
if (addTagSendBtn) addTagSendBtn.disabled = true;
|
||||||
closeSugg();
|
closeSugg();
|
||||||
try {
|
try {
|
||||||
|
const csrfToken = window.f0ckSession?.csrf_token || window.scrollerCsrf || '';
|
||||||
const resp = await fetch(`/api/v2/tags/${targetId}`, {
|
const resp = await fetch(`/api/v2/tags/${targetId}`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
headers: {
|
||||||
body: `tagname=${encodeURIComponent(tag)}`
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
...(csrfToken ? { 'X-CSRF-Token': csrfToken } : {})
|
||||||
|
},
|
||||||
|
body: `tagname=${encodeURIComponent(tag)}${csrfToken ? `&csrf_token=${encodeURIComponent(csrfToken)}` : ''}`
|
||||||
});
|
});
|
||||||
const data = await resp.json();
|
const data = await resp.json();
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
@@ -3658,7 +3680,11 @@
|
|||||||
if (sMarkAll) {
|
if (sMarkAll) {
|
||||||
sMarkAll.addEventListener('click', async () => {
|
sMarkAll.addEventListener('click', async () => {
|
||||||
try {
|
try {
|
||||||
await fetch('/api/notifications/read', { method: 'POST' });
|
const csrfToken = window.f0ckSession?.csrf_token || window.scrollerCsrf || '';
|
||||||
|
await fetch('/api/notifications/read', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { ...(csrfToken ? { 'X-CSRF-Token': csrfToken } : {}) }
|
||||||
|
});
|
||||||
updateScrollerNotifBadge(0);
|
updateScrollerNotifBadge(0);
|
||||||
sCachedNotifs = sCachedNotifs.map(n => ({ ...n, is_read: true }));
|
sCachedNotifs = sCachedNotifs.map(n => ({ ...n, is_read: true }));
|
||||||
updateScrollerTabBadges(sCachedNotifs);
|
updateScrollerTabBadges(sCachedNotifs);
|
||||||
@@ -3673,7 +3699,12 @@
|
|||||||
if (!item) return;
|
if (!item) return;
|
||||||
const nid = item.dataset.id;
|
const nid = item.dataset.id;
|
||||||
if (nid && item.classList.contains('unread')) {
|
if (nid && item.classList.contains('unread')) {
|
||||||
fetch(`/api/notifications/${nid}/read`, { method: 'POST', keepalive: true }).catch(() => {});
|
const csrfToken = window.f0ckSession?.csrf_token || window.scrollerCsrf || '';
|
||||||
|
fetch(`/api/notifications/${nid}/read`, {
|
||||||
|
method: 'POST',
|
||||||
|
keepalive: true,
|
||||||
|
headers: { ...(csrfToken ? { 'X-CSRF-Token': csrfToken } : {}) }
|
||||||
|
}).catch(() => {});
|
||||||
item.classList.remove('unread');
|
item.classList.remove('unread');
|
||||||
// Update cache
|
// Update cache
|
||||||
const cached = sCachedNotifs.find(n => String(n.id) === String(nid));
|
const cached = sCachedNotifs.find(n => String(n.id) === String(nid));
|
||||||
|
|||||||
@@ -47,7 +47,7 @@
|
|||||||
if (!unsafe) return '';
|
if (!unsafe) return '';
|
||||||
const div = document.createElement('div');
|
const div = document.createElement('div');
|
||||||
div.textContent = unsafe;
|
div.textContent = unsafe;
|
||||||
return div.innerHTML;
|
return div.innerHTML.replace(/"/g, '"').replace(/'/g, ''');
|
||||||
};
|
};
|
||||||
|
|
||||||
const playSidebarEmojiVideos = (container) => {
|
const playSidebarEmojiVideos = (container) => {
|
||||||
@@ -271,7 +271,7 @@
|
|||||||
const rendered = quoteEmojis
|
const rendered = quoteEmojis
|
||||||
? quoteContent.replace(/:([a-z0-9_]+):/g, (m, n) => renderEmoji(m, n))
|
? quoteContent.replace(/:([a-z0-9_]+):/g, (m, n) => renderEmoji(m, n))
|
||||||
: quoteContent;
|
: quoteContent;
|
||||||
return `<span class="greentext">>${rendered}</span>`;
|
return `<span class="greentext">>${rendered.replace(/>/g, '>')}</span>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Per-line limit to prevent marked.parse recursion on single giant lines
|
// Per-line limit to prevent marked.parse recursion on single giant lines
|
||||||
@@ -496,8 +496,8 @@
|
|||||||
|| rawContent.split('\n').length > 2
|
|| rawContent.split('\n').length > 2
|
||||||
|| (c.files && c.files.length > 0);
|
|| (c.files && c.files.length > 0);
|
||||||
}
|
}
|
||||||
// Always force isLong if there are inline media tags that expand vertically
|
// Always force isLong if there are inline media tags that expand vertically or files are attached
|
||||||
if (!isLong && (/\[(video|audio|youtube|img)\]|!\[|https?:\/\//i.test(rawContent) || displayContent.includes('<video') || displayContent.includes('<img'))) {
|
if (!isLong && ((c.files && c.files.length > 0) || /\[(video|audio|youtube|img)\]|!\[|https?:\/\//i.test(rawContent) || displayContent.includes('<video') || displayContent.includes('<img'))) {
|
||||||
isLong = true;
|
isLong = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -587,7 +587,7 @@
|
|||||||
const isExpanded = container.classList.contains('expanded');
|
const isExpanded = container.classList.contains('expanded');
|
||||||
const scrollHeight = inner.scrollHeight;
|
const scrollHeight = inner.scrollHeight;
|
||||||
const clientHeight = inner.clientHeight;
|
const clientHeight = inner.clientHeight;
|
||||||
const hasUnloadedImages = Array.from(inner.querySelectorAll('img:not(.emoji), video')).some(
|
const hasUnloadedImages = Array.from(inner.querySelectorAll('img, video')).some(
|
||||||
media => {
|
media => {
|
||||||
if (media.tagName === 'IMG') {
|
if (media.tagName === 'IMG') {
|
||||||
return (!media.complete || media.naturalHeight === 0) && media.dataset.error !== 'true';
|
return (!media.complete || media.naturalHeight === 0) && media.dataset.error !== 'true';
|
||||||
@@ -639,8 +639,8 @@
|
|||||||
|
|
||||||
const attachMediaLoadListeners = (element) => {
|
const attachMediaLoadListeners = (element) => {
|
||||||
// Only target images and videos inside comment-content-inner.
|
// Only target images and videos inside comment-content-inner.
|
||||||
// Avatars, emojis, and item preview thumbnails have fixed sizes and do not affect text overflow.
|
// Avatars and item preview thumbnails have fixed sizes and do not affect text overflow.
|
||||||
element.querySelectorAll('.comment-content-inner img:not(.emoji), .comment-content-inner video').forEach(media => {
|
element.querySelectorAll('.comment-content-inner img, .comment-content-inner video').forEach(media => {
|
||||||
if (media.dataset.loadListenerBound) return;
|
if (media.dataset.loadListenerBound) return;
|
||||||
media.dataset.loadListenerBound = 'true';
|
media.dataset.loadListenerBound = 'true';
|
||||||
|
|
||||||
|
|||||||
@@ -120,7 +120,13 @@
|
|||||||
postid,
|
postid,
|
||||||
existingTags: tags,
|
existingTags: tags,
|
||||||
anchorEl: anchor,
|
anchorEl: anchor,
|
||||||
onSubmit: async (tag) => post("/api/v2/tags/" + postid, { tagname: tag }),
|
onSubmit: async (tag) => {
|
||||||
|
const res = await post("/api/v2/tags/" + postid, { tagname: tag });
|
||||||
|
if (res.success && window.invalidateItemCache) {
|
||||||
|
window.invalidateItemCache(postid);
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
},
|
||||||
renderTags
|
renderTags
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -306,7 +306,7 @@ if (!window.UserCommentSystem) {
|
|||||||
const trimmed = line.trimStart();
|
const trimmed = line.trimStart();
|
||||||
if (trimmed.startsWith('>') && !trimmed.match(/^>>\d+/)) {
|
if (trimmed.startsWith('>') && !trimmed.match(/^>>\d+/)) {
|
||||||
const quoteContent = line.substring(line.indexOf('>') + 1);
|
const quoteContent = line.substring(line.indexOf('>') + 1);
|
||||||
return `<span class="greentext">>${quoteContent}</span>`;
|
return `<span class="greentext">>${quoteContent.replace(/>/g, '>')}</span>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Per-line limit
|
// Per-line limit
|
||||||
@@ -450,7 +450,7 @@ if (!window.UserCommentSystem) {
|
|||||||
if (!unsafe) return '';
|
if (!unsafe) return '';
|
||||||
const div = document.createElement('div');
|
const div = document.createElement('div');
|
||||||
div.textContent = unsafe;
|
div.textContent = unsafe;
|
||||||
return div.innerHTML;
|
return div.innerHTML.replace(/"/g, '"').replace(/'/g, ''');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,33 +5,33 @@ const tpl_player = (svg, size) => `<div class="v0ck_player_controls">
|
|||||||
<div class="v0ck_progress_filled"></div>
|
<div class="v0ck_progress_filled"></div>
|
||||||
<div class="v0ck_seek_marker"></div>
|
<div class="v0ck_seek_marker"></div>
|
||||||
</div>
|
</div>
|
||||||
<button class="v0ck_player_button v0ck_tplay v0ck_toggle" title="Play">
|
<button class="v0ck_player_button v0ck_tplay v0ck_toggle" title="Play" tabindex="-1">
|
||||||
<svg style="width: 20px; height: 20px;">
|
<svg style="width: 20px; height: 20px;">
|
||||||
<use id="v0ck_svg_play" href="${svg}#play"></use>
|
<use id="v0ck_svg_play" href="${svg}#play"></use>
|
||||||
<use id="v0ck_svg_pause" class="v0ck_hidden" href="${svg}#pause"></use>
|
<use id="v0ck_svg_pause" class="v0ck_hidden" href="${svg}#pause"></use>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<div class="v0ck_volume_group">
|
<div class="v0ck_volume_group">
|
||||||
<button class="v0ck_player_button v0ck_volume">
|
<button class="v0ck_player_button v0ck_volume" tabindex="-1">
|
||||||
<svg style="width: 20px; height: 20px;">
|
<svg style="width: 20px; height: 20px;">
|
||||||
<use id="v0ck_svg_volume_full" href="${svg}#volume_full"></use>
|
<use id="v0ck_svg_volume_full" href="${svg}#volume_full"></use>
|
||||||
<use id="v0ck_svg_volume_mid" class="v0ck_hidden" href="${svg}#volume_mid"></use>
|
<use id="v0ck_svg_volume_mid" class="v0ck_hidden" href="${svg}#volume_mid"></use>
|
||||||
<use id="v0ck_svg_volume_mute" class="v0ck_hidden" href="${svg}#volume_mute"></use>
|
<use id="v0ck_svg_volume_mute" class="v0ck_hidden" href="${svg}#volume_mute"></use>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<input type="range" name="volume" min="0" max="1" step="0.01" value="1" />
|
<input type="range" name="volume" min="0" max="1" step="0.01" value="1" tabindex="-1" />
|
||||||
</div>
|
</div>
|
||||||
<button class="v0ck_player_button v0ck_playtime">00:00 / 00:00</button>
|
<button class="v0ck_player_button v0ck_playtime" tabindex="-1">00:00 / 00:00</button>
|
||||||
<span style="flex: 30"></span>
|
<span style="flex: 30"></span>
|
||||||
|
|
||||||
<div class="v0ck_settings_container">
|
<div class="v0ck_settings_container">
|
||||||
<button class="v0ck_player_button v0ck_settings_btn" title="Settings">
|
<button class="v0ck_player_button v0ck_settings_btn" title="Settings" tabindex="-1">
|
||||||
<svg viewBox="0 0 24 24" style="width: 20px; height: 20px;">
|
<svg viewBox="0 0 24 24" style="width: 20px; height: 20px;">
|
||||||
<path d="M19.14,12.94c0.04-0.3,0.06-0.61,0.06-0.94c0-0.32-0.02-0.64-0.06-0.94l2.03-1.58c0.18-0.14,0.23-0.41,0.12-0.61 l-1.92-3.32c-0.12-0.22-0.37-0.29-0.59-0.22l-2.39,0.96c-0.5-0.38-1.03-0.7-1.62-0.94L14.4,2.81c-0.04-0.24-0.24-0.41-0.48-0.41 h-3.84c-0.24,0-0.43,0.17-0.47,0.41L9.25,5.35C8.66,5.59,8.12,5.92,7.63,6.29L5.24,5.33c-0.22-0.08-0.47,0-0.59,0.22L2.74,8.87 C2.62,9.08,2.66,9.34,2.86,9.48l2.03,1.58C4.84,11.36,4.8,11.69,4.8,12s0.02,0.64,0.06,0.94l-2.03,1.58 c-0.18,0.14-0.23,0.41-0.12,0.61l1.92,3.32c0.12,0.22,0.37,0.29,0.59,0.22l2.39-0.96c0.5,0.38,1.03,0.7,1.62,0.94l0.36,2.54 c0.05,0.24,0.24,0.41,0.48,0.41h3.84c0.24,0,0.44-0.17,0.47-0.41l0.36-2.54c0.59-0.24,1.13-0.56,1.62-0.94l2.39,0.96 c0.22,0.08,0.47,0,0.59-0.22l1.92-3.32c0.12-0.22,0.07-0.47-0.12-0.61L19.14,12.94z M12,15.6c-1.98,0-3.6-1.62-3.6-3.6 s1.62-3.6,3.6-3.6s3.6,1.62,3.6,3.6S13.98,15.6,12,15.6z"/>
|
<path d="M19.14,12.94c0.04-0.3,0.06-0.61,0.06-0.94c0-0.32-0.02-0.64-0.06-0.94l2.03-1.58c0.18-0.14,0.23-0.41,0.12-0.61 l-1.92-3.32c-0.12-0.22-0.37-0.29-0.59-0.22l-2.39,0.96c-0.5-0.38-1.03-0.7-1.62-0.94L14.4,2.81c-0.04-0.24-0.24-0.41-0.48-0.41 h-3.84c-0.24,0-0.43,0.17-0.47,0.41L9.25,5.35C8.66,5.59,8.12,5.92,7.63,6.29L5.24,5.33c-0.22-0.08-0.47,0-0.59,0.22L2.74,8.87 C2.62,9.08,2.66,9.34,2.86,9.48l2.03,1.58C4.84,11.36,4.8,11.69,4.8,12s0.02,0.64,0.06,0.94l-2.03,1.58 c-0.18,0.14-0.23,0.41-0.12,0.61l1.92,3.32c0.12,0.22,0.37,0.29,0.59,0.22l2.39-0.96c0.5,0.38,1.03,0.7,1.62,0.94l0.36,2.54 c0.05,0.24,0.24,0.41,0.48,0.41h3.84c0.24,0,0.44-0.17,0.47-0.41l0.36-2.54c0.59-0.24,1.13-0.56,1.62-0.94l2.39,0.96 c0.22,0.08,0.47,0,0.59-0.22l1.92-3.32c0.12-0.22,0.07-0.47-0.12-0.61L19.14,12.94z M12,15.6c-1.98,0-3.6-1.62-3.6-3.6 s1.62-3.6,3.6-3.6s3.6,1.62,3.6,3.6S13.98,15.6,12,15.6z"/>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<div class="v0ck_settings_menu v0ck_hidden">
|
<div class="v0ck_settings_menu v0ck_hidden">
|
||||||
<button id="toggleswf" class="v0ck_menu_item" title="Flash Yank">SWF</button>
|
<button id="toggleswf" class="v0ck_menu_item" title="Flash Yank" tabindex="-1">SWF</button>
|
||||||
<div class="v0ck_menu_item v0ck_bg_row">
|
<div class="v0ck_menu_item v0ck_bg_row">
|
||||||
<span class="v0ck_switch_label">Background</span>
|
<span class="v0ck_switch_label">Background</span>
|
||||||
<div id="togglebg" class="v0ck_cool_switch" title="Toggle Background"></div>
|
<div id="togglebg" class="v0ck_cool_switch" title="Toggle Background"></div>
|
||||||
@@ -44,11 +44,11 @@ const tpl_player = (svg, size) => `<div class="v0ck_player_controls">
|
|||||||
<span class="v0ck_switch_label">Danmaku</span>
|
<span class="v0ck_switch_label">Danmaku</span>
|
||||||
<div id="toggledanmaku" class="v0ck_cool_switch" title="Toggle Danmaku comments"></div>
|
<div id="toggledanmaku" class="v0ck_cool_switch" title="Toggle Danmaku comments"></div>
|
||||||
</div>
|
</div>
|
||||||
<button id="v0ck_download" class="v0ck_menu_item" title="Download File">Download${size ? ` (${size})` : ''}</button>
|
<button id="v0ck_download" class="v0ck_menu_item" title="Download File" tabindex="-1">Download${size ? ` (${size})` : ''}</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button class="v0ck_player_button v0ck_toggle v0ck_fs_btn" title="Full Screen">
|
<button class="v0ck_player_button v0ck_toggle v0ck_fs_btn" title="Full Screen" tabindex="-1">
|
||||||
<svg style="width: 20px; height: 20px;"><use id="v0ck_svg_fullscreen" href="${svg}#fullscreen"></use></svg>
|
<svg style="width: 20px; height: 20px;"><use id="v0ck_svg_fullscreen" href="${svg}#fullscreen"></use></svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ const resolvePath = (defaultRel) => {
|
|||||||
const absStorage = path.resolve(storage);
|
const absStorage = path.resolve(storage);
|
||||||
if (defaultRel.startsWith('public/')) {
|
if (defaultRel.startsWith('public/')) {
|
||||||
const sub = defaultRel.replace('public/', '');
|
const sub = defaultRel.replace('public/', '');
|
||||||
if (sub === 's/emojis' || sub === 's/koepfe') {
|
if (sub === 's/emojis' || sub === 's/koepfe' || sub === 's/fonts') {
|
||||||
const storagePath = path.join(absStorage, sub.split('/').pop());
|
const storagePath = path.join(absStorage, sub.split('/').pop());
|
||||||
if (fs.existsSync(storagePath)) return path.resolve(storagePath);
|
if (fs.existsSync(storagePath)) return path.resolve(storagePath);
|
||||||
return local;
|
return local;
|
||||||
@@ -51,6 +51,7 @@ config.paths = {
|
|||||||
s: path.join(base, 'public/s'),
|
s: path.join(base, 'public/s'),
|
||||||
emojis: resolvePath('public/s/emojis'),
|
emojis: resolvePath('public/s/emojis'),
|
||||||
koepfe: resolvePath('public/s/koepfe'),
|
koepfe: resolvePath('public/s/koepfe'),
|
||||||
|
fonts: resolvePath('public/s/fonts'),
|
||||||
memes: resolvePath('public/memes'),
|
memes: resolvePath('public/memes'),
|
||||||
e: resolvePath('e'),
|
e: resolvePath('e'),
|
||||||
pending: resolvePath('pending'),
|
pending: resolvePath('pending'),
|
||||||
|
|||||||
@@ -132,6 +132,7 @@ export default new class {
|
|||||||
|
|
||||||
// Build suffix with query params
|
// Build suffix with query params
|
||||||
let suffix = env.strict ? '?strict=1' : '';
|
let suffix = env.strict ? '?strict=1' : '';
|
||||||
|
if (env.tagger) suffix += (suffix ? '&' : '?') + `tagger=${encodeURIComponent(env.tagger)}`;
|
||||||
|
|
||||||
// mainDisplay: decoded for human-readable display (e.g. div.location)
|
// mainDisplay: decoded for human-readable display (e.g. div.location)
|
||||||
// main: keeps percent-encoding for use in href attributes
|
// main: keeps percent-encoding for use in href attributes
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
"meme": "Meme",
|
"meme": "Meme",
|
||||||
"halls": "Hallen",
|
"halls": "Hallen",
|
||||||
"tags": "Tags",
|
"tags": "Tags",
|
||||||
|
"chat": "Chat",
|
||||||
"search": "Suchen",
|
"search": "Suchen",
|
||||||
"random": "Zufall",
|
"random": "Zufall",
|
||||||
"profile": "Profil",
|
"profile": "Profil",
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
"meme": "Meme",
|
"meme": "Meme",
|
||||||
"halls": "Halls",
|
"halls": "Halls",
|
||||||
"tags": "Tags",
|
"tags": "Tags",
|
||||||
|
"chat": "Chat",
|
||||||
"search": "Search",
|
"search": "Search",
|
||||||
"random": "Random",
|
"random": "Random",
|
||||||
"profile": "Profile",
|
"profile": "Profile",
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
"meme": "meme",
|
"meme": "meme",
|
||||||
"halls": "Hallen",
|
"halls": "Hallen",
|
||||||
"tags": "Tags",
|
"tags": "Tags",
|
||||||
|
"chat": "Chat",
|
||||||
"search": "Zoeken",
|
"search": "Zoeken",
|
||||||
"random": "Willekeurig",
|
"random": "Willekeurig",
|
||||||
"profile": "profiel",
|
"profile": "profiel",
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
"meme": "Memen",
|
"meme": "Memen",
|
||||||
"halls": "Hallen",
|
"halls": "Hallen",
|
||||||
"tags": "Etiketten",
|
"tags": "Etiketten",
|
||||||
|
"chat": "Geschwafel",
|
||||||
"search": "Suche",
|
"search": "Suche",
|
||||||
"random": "Zufall",
|
"random": "Zufall",
|
||||||
"profile": "Profil",
|
"profile": "Profil",
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ const flashMimes = Object.entries(cfg.mimes || {}).filter(([, ext]) => ext === '
|
|||||||
const COUNT_CACHE_TTL_MS = 90_000;
|
const COUNT_CACHE_TTL_MS = 90_000;
|
||||||
const countCache = new Map(); // key → { total, expiresAt }
|
const countCache = new Map(); // key → { total, expiresAt }
|
||||||
|
|
||||||
function buildCountCacheKey({ modequery, tag, user, hall, mime, fav, session, excludedTags, newerThan, minXd, userHallObj }) {
|
function buildCountCacheKey({ modequery, tag, user, hall, mime, fav, session, excludedTags, newerThan, minXd, userHallObj, tagger }) {
|
||||||
return JSON.stringify([
|
return JSON.stringify([
|
||||||
modequery,
|
modequery,
|
||||||
tag ?? '',
|
tag ?? '',
|
||||||
@@ -30,7 +30,8 @@ function buildCountCacheKey({ modequery, tag, user, hall, mime, fav, session, ex
|
|||||||
excludedTags.slice().sort().join(','),
|
excludedTags.slice().sort().join(','),
|
||||||
newerThan ?? '',
|
newerThan ?? '',
|
||||||
minXd,
|
minXd,
|
||||||
userHallObj?.id ?? ''
|
userHallObj?.id ?? '',
|
||||||
|
tagger ?? ''
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -136,7 +137,7 @@ const xdScoreMeta = (score) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
getf0cks: async ({ user: rawUser, tag: rawTag, hall: rawHall, mime: rawMime, page, mode, ratings, fav, session, limit, strict, newer, exclude, user_id, random, userHall: rawUserHall, userHallOwner: rawUserHallOwner, minXdScore } = {}) => {
|
getf0cks: async ({ user: rawUser, tag: rawTag, hall: rawHall, mime: rawMime, page, mode, ratings, fav, session, limit, strict, newer, exclude, user_id, random, userHall: rawUserHall, userHallOwner: rawUserHallOwner, minXdScore, tagger: rawTagger } = {}) => {
|
||||||
const user = rawUser ? lib.escapeLike(decodeURI(rawUser)) : null;
|
const user = rawUser ? lib.escapeLike(decodeURI(rawUser)) : null;
|
||||||
|
|
||||||
// --- title: prefix — search items.title instead of the tags table ---
|
// --- title: prefix — search items.title instead of the tags table ---
|
||||||
@@ -189,7 +190,8 @@ export default {
|
|||||||
const strictParams = ((strict || (tag && tag.includes(','))) && tag) ? tag.split(',').map(t => lib.slugify(t)).filter(t => t) : [];
|
const strictParams = ((strict || (tag && tag.includes(','))) && tag) ? tag.split(',').map(t => lib.slugify(t)).filter(t => t) : [];
|
||||||
const isStrict = strictParams.length > 0;
|
const isStrict = strictParams.length > 0;
|
||||||
|
|
||||||
const tmp = { user, tag: isTitleSearch ? _decodedTag : tag, hall: hallObj || hall, mime, page: actPage, mode: mode, view_mode: fav ? 'favs' : 'uploads', strict: strict, userHall: userHallObj || userHallSlug, userHallOwner };
|
const tagger = rawTagger ? lib.escapeLike(rawTagger) : null;
|
||||||
|
const tmp = { user, tag: isTitleSearch ? _decodedTag : tag, hall: hallObj || hall, mime, page: actPage, mode: mode, view_mode: fav ? 'favs' : 'uploads', strict: strict, userHall: userHallObj || userHallSlug, userHallOwner, tagger };
|
||||||
// Multi-rating support: if `ratings` array provided, build an OR-based SQL fragment
|
// Multi-rating support: if `ratings` array provided, build an OR-based SQL fragment
|
||||||
const multiRatingSQL = (Array.isArray(ratings) && ratings.length > 0) ? lib.getMultiRatingMode(ratings) : null;
|
const multiRatingSQL = (Array.isArray(ratings) && ratings.length > 0) ? lib.getMultiRatingMode(ratings) : null;
|
||||||
const baseMode = multiRatingSQL ?? lib.getMode(mode ?? 0);
|
const baseMode = multiRatingSQL ?? lib.getMode(mode ?? 0);
|
||||||
@@ -200,6 +202,21 @@ export default {
|
|||||||
if (isTitleSearch && titleQuery) {
|
if (isTitleSearch && titleQuery) {
|
||||||
// Title search: match items.title ILIKE '%query%'
|
// Title search: match items.title ILIKE '%query%'
|
||||||
titleFilter = db`and items.title ILIKE ${'%' + titleQuery + '%'} and items.title IS NOT NULL`;
|
titleFilter = db`and items.title ILIKE ${'%' + titleQuery + '%'} and items.title IS NOT NULL`;
|
||||||
|
} else if (tagger && tag) {
|
||||||
|
// Tagger+tag filter: items where the specific user applied this specific tag
|
||||||
|
const terms = tag.split(',').map(t => t.trim()).filter(Boolean);
|
||||||
|
if (terms.length > 0) {
|
||||||
|
const conditions = terms.map(term => {
|
||||||
|
return db`and items.id in (
|
||||||
|
select ta.item_id from tags_assign ta
|
||||||
|
join tags t on t.id = ta.tag_id
|
||||||
|
join "user" u on u.id = ta.user_id
|
||||||
|
where t.normalized like '%' || slugify(${term}) || '%'
|
||||||
|
and u.user ilike ${tagger}
|
||||||
|
)`;
|
||||||
|
});
|
||||||
|
tagFilter = db`${conditions}`;
|
||||||
|
}
|
||||||
} else if (tag) {
|
} else if (tag) {
|
||||||
const terms = tag.split(',').map(t => t.trim()).filter(Boolean);
|
const terms = tag.split(',').map(t => t.trim()).filter(Boolean);
|
||||||
if (terms.length > 0) {
|
if (terms.length > 0) {
|
||||||
@@ -233,7 +250,7 @@ export default {
|
|||||||
userHallFilter = db`and items.id in (select uha.item_id from user_halls_assign uha where uha.hall_id = ${userHallObj.id})`;
|
userHallFilter = db`and items.id in (select uha.item_id from user_halls_assign uha where uha.hall_id = ${userHallObj.id})`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const cacheKey = buildCountCacheKey({ modequery, tag, user, hall, mime, fav, session, excludedTags, newerThan, minXd, userHallObj });
|
const cacheKey = buildCountCacheKey({ modequery, tag, user, hall, mime, fav, session, excludedTags, newerThan, minXd, userHallObj, tagger });
|
||||||
let total = getCachedCount(cacheKey);
|
let total = getCachedCount(cacheKey);
|
||||||
|
|
||||||
if (total === null) {
|
if (total === null) {
|
||||||
@@ -372,7 +389,7 @@ export default {
|
|||||||
for (let i = Math.max(1, act_page - range); i <= Math.min(act_page + range, pages); i++)
|
for (let i = Math.max(1, act_page - range); i <= Math.min(act_page + range, pages); i++)
|
||||||
cheat.push(i);
|
cheat.push(i);
|
||||||
|
|
||||||
const link = lib.genLink({ user, tag, hall: hallObj ? hallObj.slug : hall, mime, type: fav ? 'favs' : 'uploads', path: 'p/', strict: strict });
|
const link = lib.genLink({ user, tag, hall: hallObj ? hallObj.slug : hall, mime, type: fav ? 'favs' : 'uploads', path: 'p/', strict: strict, tagger });
|
||||||
|
|
||||||
// Override link for title searches — pagination must use the /tag/title:... prefix
|
// Override link for title searches — pagination must use the /tag/title:... prefix
|
||||||
if (isTitleSearch && titleQuery) {
|
if (isTitleSearch && titleQuery) {
|
||||||
|
|||||||
@@ -226,7 +226,8 @@ export default (router, tpl) => {
|
|||||||
strict: query.strict === '1' || query.strict === 'true' || req.session?.strict_mode,
|
strict: query.strict === '1' || query.strict === 'true' || req.session?.strict_mode,
|
||||||
explicitStrict: query.strict === '1' || query.strict === 'true',
|
explicitStrict: query.strict === '1' || query.strict === 'true',
|
||||||
newer: query.newer || null,
|
newer: query.newer || null,
|
||||||
minXdScore: req.session?.min_xd_score || 0
|
minXdScore: req.session?.min_xd_score || 0,
|
||||||
|
tagger: query.tagger || null
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!data.success) {
|
if (!data.success) {
|
||||||
|
|||||||
@@ -486,7 +486,7 @@ export default router => {
|
|||||||
// F-023 Security: Validate font against actual files on disk
|
// F-023 Security: Validate font against actual files on disk
|
||||||
// The font value is rendered into CSS url() in header.html, so it must be a real filename
|
// The font value is rendered into CSS url() in header.html, so it must be a real filename
|
||||||
if (font) {
|
if (font) {
|
||||||
const fontsDir = path.join(path.resolve(), 'public/s/fonts');
|
const fontsDir = cfg.paths.fonts;
|
||||||
try {
|
try {
|
||||||
const available = (await fs.readdir(fontsDir)).filter(f => /\.(ttf|otf|woff2?)$/i.test(f));
|
const available = (await fs.readdir(fontsDir)).filter(f => /\.(ttf|otf|woff2?)$/i.test(f));
|
||||||
if (!available.includes(font)) {
|
if (!available.includes(font)) {
|
||||||
|
|||||||
@@ -25,7 +25,8 @@ export default router => {
|
|||||||
|
|
||||||
group.post(/$/, lib.loggedin, async (req, res) => {
|
group.post(/$/, lib.loggedin, async (req, res) => {
|
||||||
// assign and/or create tag
|
// assign and/or create tag
|
||||||
if (!req.params.postid || !req.post.tagname) {
|
const rawTagname = req.post?.tagname || req.body?.tagname;
|
||||||
|
if (!req.params.postid || !rawTagname) {
|
||||||
return res.json({
|
return res.json({
|
||||||
success: false,
|
success: false,
|
||||||
msg: 'missing postid or tag'
|
msg: 'missing postid or tag'
|
||||||
@@ -33,7 +34,7 @@ export default router => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const postid = +req.params.postid;
|
const postid = +req.params.postid;
|
||||||
const tagname = req.post.tagname?.trim();
|
const tagname = rawTagname.trim();
|
||||||
const protectedTags = ['sfw', 'nsfw', 'nsfl'];
|
const protectedTags = ['sfw', 'nsfw', 'nsfl'];
|
||||||
|
|
||||||
if (protectedTags.includes(tagname.toLowerCase())) {
|
if (protectedTags.includes(tagname.toLowerCase())) {
|
||||||
|
|||||||
@@ -336,7 +336,16 @@ export default router => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const url = inputUrl.trim();
|
let url = inputUrl.trim();
|
||||||
|
try {
|
||||||
|
const parsed = new URL(url);
|
||||||
|
if (parsed.searchParams.has('igsh')) {
|
||||||
|
parsed.searchParams.delete('igsh');
|
||||||
|
url = parsed.toString();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Ignore malformed URL, keep original string
|
||||||
|
}
|
||||||
const ytRegex = /(?:youtube\.com\/\S*(?:(?:\/e(?:mbed))?\/|watch\/?\?(?:\S*?&?v\=))|youtu\.be\/)([a-zA-Z0-9_-]{6,11})/i;
|
const ytRegex = /(?:youtube\.com\/\S*(?:(?:\/e(?:mbed))?\/|watch\/?\?(?:\S*?&?v\=))|youtu\.be\/)([a-zA-Z0-9_-]{6,11})/i;
|
||||||
const ytMatch = url.match(ytRegex);
|
const ytMatch = url.match(ytRegex);
|
||||||
|
|
||||||
|
|||||||
@@ -47,6 +47,25 @@ function buildBgHostRegex() {
|
|||||||
|
|
||||||
export default (router, tpl) => {
|
export default (router, tpl) => {
|
||||||
|
|
||||||
|
// GET /chat — Dedicated site chat page
|
||||||
|
router.get('/chat', async (req, res) => {
|
||||||
|
if (!cfg.websrv.enable_global_chat) {
|
||||||
|
return res.reply({ code: 404, body: 'Not found' });
|
||||||
|
}
|
||||||
|
if (!req.session) {
|
||||||
|
return res.redirect('/login');
|
||||||
|
}
|
||||||
|
if (req.session.banned) {
|
||||||
|
return res.reply({ code: 403, body: 'Access denied: You are banned' });
|
||||||
|
}
|
||||||
|
return res.html(tpl.render('chat', {
|
||||||
|
session: req.session,
|
||||||
|
hidePagination: true,
|
||||||
|
link: { main: '/chat', path: '/chat' },
|
||||||
|
domain: cfg.main.url.domain
|
||||||
|
}, req));
|
||||||
|
});
|
||||||
|
|
||||||
// GET /api/chat — fetch recent messages
|
// GET /api/chat — fetch recent messages
|
||||||
router.get('/api/chat', async (req, res) => {
|
router.get('/api/chat', async (req, res) => {
|
||||||
if (!cfg.websrv.enable_global_chat) {
|
if (!cfg.websrv.enable_global_chat) {
|
||||||
|
|||||||
@@ -244,7 +244,8 @@ export default (router, tpl) => {
|
|||||||
explicitStrict: !!(req.query?.strict || req.url.qs?.strict),
|
explicitStrict: !!(req.query?.strict || req.url.qs?.strict),
|
||||||
random: req.cookies.random_mode === '1',
|
random: req.cookies.random_mode === '1',
|
||||||
minXdScore: req.params.itemid ? 0 : (req.url.qs?.min_xd !== undefined ? +req.url.qs.min_xd : (req.session?.min_xd_score || 0)),
|
minXdScore: req.params.itemid ? 0 : (req.url.qs?.min_xd !== undefined ? +req.url.qs.min_xd : (req.session?.min_xd_score || 0)),
|
||||||
lang: req.lang
|
lang: req.lang,
|
||||||
|
tagger: req.url.qs?.tagger || null
|
||||||
});
|
});
|
||||||
console.log(`[DEBUG] Checking strict mode: query=${req.query?.strict}, session=${req.session?.strict_mode}, effective=${!!(req.query?.strict || req.url.qs?.strict || req.session?.strict_mode)}`);
|
console.log(`[DEBUG] Checking strict mode: query=${req.query?.strict}, session=${req.session?.strict_mode}, effective=${!!(req.query?.strict || req.url.qs?.strict || req.session?.strict_mode)}`);
|
||||||
console.log(`[${new Date().toISOString()}] [ROUTE] Data fetch complete in ${Date.now() - tRouteStart}ms`);
|
console.log(`[${new Date().toISOString()}] [ROUTE] Data fetch complete in ${Date.now() - tRouteStart}ms`);
|
||||||
|
|||||||
@@ -23,6 +23,11 @@ export default (router, tpl) => {
|
|||||||
route: /^\/s\/koepfe\//
|
route: /^\/s\/koepfe\//
|
||||||
});
|
});
|
||||||
|
|
||||||
|
router.static({
|
||||||
|
dir: cfg.paths.fonts,
|
||||||
|
route: /^\/s\/fonts\//
|
||||||
|
});
|
||||||
|
|
||||||
router.static({
|
router.static({
|
||||||
dir: path.join(path.resolve(), 'node_modules/@ruffle-rs/ruffle'),
|
dir: path.join(path.resolve(), 'node_modules/@ruffle-rs/ruffle'),
|
||||||
route: /^\/s\/ruffle\//
|
route: /^\/s\/ruffle\//
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ export default (router, tpl) => {
|
|||||||
if (req.headers['x-requested-with'] === 'XMLHttpRequest' || query.ajax) {
|
if (req.headers['x-requested-with'] === 'XMLHttpRequest' || query.ajax) {
|
||||||
return res.json({
|
return res.json({
|
||||||
success: true,
|
success: true,
|
||||||
html: tpl.render('tag-cards', { toptags: tags, session: (req.session && req.session.user) ? { ...req.session } : false }, req),
|
html: tpl.render('tag-cards', { toptags: tags, user: req.session?.user ? { user: req.session.user } : null, session: (req.session && req.session.user) ? { ...req.session } : false }, req),
|
||||||
currentPage: page,
|
currentPage: page,
|
||||||
hasMore: tags.length === TAGS_PER_PAGE
|
hasMore: tags.length === TAGS_PER_PAGE
|
||||||
});
|
});
|
||||||
@@ -128,6 +128,7 @@ export default (router, tpl) => {
|
|||||||
phrase,
|
phrase,
|
||||||
tmp: null,
|
tmp: null,
|
||||||
hidePagination: true,
|
hidePagination: true,
|
||||||
|
user: req.session?.user ? { user: req.session.user } : null,
|
||||||
session: (req.session && req.session.user) ? { ...req.session } : false,
|
session: (req.session && req.session.user) ? { ...req.session } : false,
|
||||||
page_meta: {
|
page_meta: {
|
||||||
title: 'Tags',
|
title: 'Tags',
|
||||||
|
|||||||
@@ -120,7 +120,7 @@ export default (router, tpl) => {
|
|||||||
|
|
||||||
return res.json({
|
return res.json({
|
||||||
success: true,
|
success: true,
|
||||||
html: tpl.render('tag-cards', { toptags: tags, session: (req.session && req.session.user) ? { ...req.session } : false }, req),
|
html: tpl.render('tag-cards', { toptags: tags, user: { user: userName }, session: (req.session && req.session.user) ? { ...req.session } : false }, req),
|
||||||
currentPage: page,
|
currentPage: page,
|
||||||
pagination: paginationHtml,
|
pagination: paginationHtml,
|
||||||
hasMore: tags.length === TAGS_PER_PAGE
|
hasMore: tags.length === TAGS_PER_PAGE
|
||||||
|
|||||||
@@ -505,6 +505,45 @@ process.on('uncaughtException', err => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ─── Session cache ────────────────────────────────────────────────────────
|
||||||
|
// The session middleware runs in parallel with all other app.use() handlers
|
||||||
|
// (flummpress uses Promise.all). With many concurrent requests each doing a
|
||||||
|
// DB SELECT, the pool (max:50) drains and nginx sees 502s. Caching the
|
||||||
|
// session row for 5s and the upload-count for 30s eliminates the hot path.
|
||||||
|
const SESSION_CACHE_TTL = 5_000; // ms — short enough to pick up bans quickly
|
||||||
|
const UPLOAD_COUNT_CACHE_TTL = 30_000; // ms — upload window is 12h, stale for 30s is fine
|
||||||
|
const _sessionCache = new Map(); // sha256(cookie) -> { data: row, exp: number }
|
||||||
|
const _uploadCountCache = new Map(); // username -> { count: number, exp: number }
|
||||||
|
|
||||||
|
const _scGet = (hash) => {
|
||||||
|
const e = _sessionCache.get(hash);
|
||||||
|
if (e && e.exp > Date.now()) return e.data;
|
||||||
|
_sessionCache.delete(hash);
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
const _scSet = (hash, data) => {
|
||||||
|
_sessionCache.set(hash, { data, exp: Date.now() + SESSION_CACHE_TTL });
|
||||||
|
if (_sessionCache.size > 2000) {
|
||||||
|
const now = Date.now();
|
||||||
|
for (const [k, v] of _sessionCache) if (v.exp <= now) _sessionCache.delete(k);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// Exposed so logout/ban routes can force-evict immediately
|
||||||
|
global._invalidateSessionCache = (hash) => _sessionCache.delete(hash);
|
||||||
|
|
||||||
|
const _ucGet = (username) => {
|
||||||
|
const e = _uploadCountCache.get(username);
|
||||||
|
if (e && e.exp > Date.now()) return e.count;
|
||||||
|
_uploadCountCache.delete(username);
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
const _ucSet = (username, count) => {
|
||||||
|
_uploadCountCache.set(username, { count, exp: Date.now() + UPLOAD_COUNT_CACHE_TTL });
|
||||||
|
};
|
||||||
|
// Allow upload handler to bust the cache after a successful upload
|
||||||
|
global._invalidateUploadCountCache = (username) => _uploadCountCache.delete(username);
|
||||||
|
// ──────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
app.use(async (req, res) => {
|
app.use(async (req, res) => {
|
||||||
// This can be used to annoy people on discord sending links to your site lmao, shouldnt be used though since it sucks ass
|
// This can be used to annoy people on discord sending links to your site lmao, shouldnt be used though since it sucks ass
|
||||||
// if (cfg.main.development && req.method === 'POST') console.error(`[BOOT] [DEBUG_POST] ${req.method} ${req.url.pathname}`);
|
// if (cfg.main.development && req.method === 'POST') console.error(`[BOOT] [DEBUG_POST] ${req.method} ${req.url.pathname}`);
|
||||||
@@ -571,14 +610,22 @@ process.on('uncaughtException', err => {
|
|||||||
req.fullscreen = req.cookies.fullscreen || 0;
|
req.fullscreen = req.cookies.fullscreen || 0;
|
||||||
|
|
||||||
if (req.cookies.session) {
|
if (req.cookies.session) {
|
||||||
const user = await db`
|
const _sessionHash = lib.sha256(req.cookies.session);
|
||||||
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
|
let _cachedRow = _scGet(_sessionHash);
|
||||||
from "user_sessions"
|
let user;
|
||||||
left join "user" on "user".id = "user_sessions".user_id
|
if (_cachedRow) {
|
||||||
left join "user_options" on "user_options".user_id = "user_sessions".user_id
|
user = [_cachedRow];
|
||||||
where "user_sessions".session = ${lib.sha256(req.cookies.session)}
|
} else {
|
||||||
limit 1
|
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".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
|
||||||
|
where "user_sessions".session = ${_sessionHash}
|
||||||
|
limit 1
|
||||||
|
`;
|
||||||
|
if (user.length > 0) _scSet(_sessionHash, user[0]);
|
||||||
|
}
|
||||||
|
|
||||||
if (user.length === 0) {
|
if (user.length === 0) {
|
||||||
res.writeHead(307, { // delete session
|
res.writeHead(307, { // delete session
|
||||||
@@ -651,17 +698,22 @@ process.on('uncaughtException', err => {
|
|||||||
req.session.pending_count = pending[0].c;
|
req.session.pending_count = pending[0].c;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate uploads remaining globally for the modal
|
// Calculate uploads remaining globally for the modal (cached 30s per user)
|
||||||
if (!req.session.admin && !req.session.is_moderator) {
|
if (!req.session.admin && !req.session.is_moderator) {
|
||||||
const twelveHoursAgo = ~~(Date.now() / 1000) - (12 * 3600);
|
let cachedUploadCount = _ucGet(req.session.user);
|
||||||
const uploadCount = await db`
|
if (cachedUploadCount === null) {
|
||||||
SELECT count(*) as count
|
const twelveHoursAgo = ~~(Date.now() / 1000) - (12 * 3600);
|
||||||
FROM items
|
const uploadCount = await db`
|
||||||
WHERE username = ${req.session.user}
|
SELECT count(*) as count
|
||||||
AND stamp > ${twelveHoursAgo}
|
FROM items
|
||||||
AND is_deleted = false
|
WHERE username = ${req.session.user}
|
||||||
`;
|
AND stamp > ${twelveHoursAgo}
|
||||||
req.session.uploads_remaining = Math.max(0, cfg.main.upload_limit - parseInt(uploadCount[0].count));
|
AND is_deleted = false
|
||||||
|
`;
|
||||||
|
cachedUploadCount = parseInt(uploadCount[0].count);
|
||||||
|
_ucSet(req.session.user, cachedUploadCount);
|
||||||
|
}
|
||||||
|
req.session.uploads_remaining = Math.max(0, cfg.main.upload_limit - cachedUploadCount);
|
||||||
} else {
|
} else {
|
||||||
req.session.uploads_remaining = undefined; // Unlimited for admins/mods
|
req.session.uploads_remaining = undefined; // Unlimited for admins/mods
|
||||||
}
|
}
|
||||||
@@ -1248,6 +1300,7 @@ process.on('uncaughtException', err => {
|
|||||||
custom_favicon: cfg.websrv.custom_favicon || "",
|
custom_favicon: cfg.websrv.custom_favicon || "",
|
||||||
custom_brand_image: Array.isArray(cfg.websrv.custom_brand_image) ? cfg.websrv.custom_brand_image[0] : (cfg.websrv.custom_brand_image || ""),
|
custom_brand_image: Array.isArray(cfg.websrv.custom_brand_image) ? cfg.websrv.custom_brand_image[0] : (cfg.websrv.custom_brand_image || ""),
|
||||||
custom_navbar_brand_text: cfg.websrv.custom_navbar_brand_text || "",
|
custom_navbar_brand_text: cfg.websrv.custom_navbar_brand_text || "",
|
||||||
|
default_font: cfg.websrv.default_font || "",
|
||||||
site_description: cfg.websrv.description || "The webs dumpster",
|
site_description: cfg.websrv.description || "The webs dumpster",
|
||||||
enable_nsfl: !!cfg.enable_nsfl,
|
enable_nsfl: !!cfg.enable_nsfl,
|
||||||
nsfl_tag_id: cfg.nsfl_tag_id || 3,
|
nsfl_tag_id: cfg.nsfl_tag_id || 3,
|
||||||
@@ -1267,6 +1320,7 @@ process.on('uncaughtException', err => {
|
|||||||
ts: Date.now(),
|
ts: Date.now(),
|
||||||
get default_layout() { return getDefaultLayout(); },
|
get default_layout() { return getDefaultLayout(); },
|
||||||
show_koepfe: !!cfg.websrv.show_koepfe,
|
show_koepfe: !!cfg.websrv.show_koepfe,
|
||||||
|
hide_sidebar_default: !!cfg.websrv.hide_sidebar_default,
|
||||||
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,
|
||||||
enable_dynamic_thumbs: !!cfg.websrv.enable_dynamic_thumbs,
|
enable_dynamic_thumbs: !!cfg.websrv.enable_dynamic_thumbs,
|
||||||
@@ -1302,7 +1356,7 @@ process.on('uncaughtException', err => {
|
|||||||
|
|
||||||
get fonts() {
|
get fonts() {
|
||||||
try {
|
try {
|
||||||
const fontsDir = path.join(path.resolve(), 'public/s/fonts');
|
const fontsDir = cfg.paths.fonts;
|
||||||
if (!fs.existsSync(fontsDir)) return [];
|
if (!fs.existsSync(fontsDir)) return [];
|
||||||
return fs.readdirSync(fontsDir).filter(f => /\.(ttf|otf|woff2?)$/i.test(f)).map(f => ({
|
return fs.readdirSync(fontsDir).filter(f => /\.(ttf|otf|woff2?)$/i.test(f)).map(f => ({
|
||||||
name: f.split('.').shift(),
|
name: f.split('.').shift(),
|
||||||
|
|||||||
9
views/chat.html
Normal file
9
views/chat.html
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
@include(snippets/header)
|
||||||
|
<div class="pagewrapper">
|
||||||
|
<div id="main" class="chat-page">
|
||||||
|
<div id="site-chat-container" class="site-chat-container">
|
||||||
|
<!-- Site chat widget is mounted here by globalchat.js -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@include(snippets/footer)
|
||||||
@@ -39,7 +39,6 @@
|
|||||||
@endif
|
@endif
|
||||||
|
|
||||||
<div class="sidebar-tags-container">
|
<div class="sidebar-tags-container">
|
||||||
<div style="margin-bottom: 8px; font-weight: bold; color: var(--white); font-size: 0.9em; text-transform: uppercase;"></div>
|
|
||||||
<span class="badge badge-dark" id="tags" style="display: flex; flex-wrap: wrap; gap: 5px; background: transparent; padding: 0; text-align: left; white-space: normal;">
|
<span class="badge badge-dark" id="tags" style="display: flex; flex-wrap: wrap; gap: 5px; background: transparent; padding: 0; text-align: left; white-space: normal;">
|
||||||
@if(typeof item.tags !== "undefined")
|
@if(typeof item.tags !== "undefined")
|
||||||
@each(item.tags as tag)
|
@each(item.tags as tag)
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
.meme-title { font-family: 'Impact', 'VCR', sans-serif; }
|
.meme-title { font-family: 'Impact', 'VCR', sans-serif; }
|
||||||
|
|
||||||
/* NUCLEAR VISIBILITY RESET: Override any and all parent constraints for this page */
|
/* NUCLEAR VISIBILITY RESET: Override any and all parent constraints for this page */
|
||||||
@media (max-width: 950px) {
|
@media (max-width: 1250px) {
|
||||||
/* Target all possible shell triggers */
|
/* Target all possible shell triggers */
|
||||||
html, body, .pagewrapper, #main, .layout-modern react-wrapper, .layout-legacy react-wrapper {
|
html, body, .pagewrapper, #main, .layout-modern react-wrapper, .layout-legacy react-wrapper {
|
||||||
height: auto !important;
|
height: auto !important;
|
||||||
|
|||||||
@@ -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 };
|
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.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');
|
||||||
@@ -406,6 +406,7 @@
|
|||||||
dm_unencrypted: @if(dm_unencrypted) true @else false @endif,
|
dm_unencrypted: @if(dm_unencrypted) true @else false @endif,
|
||||||
allow_comment_deletion: @if(allow_comment_deletion) true @else false @endif,
|
allow_comment_deletion: @if(allow_comment_deletion) true @else false @endif,
|
||||||
enable_comment_polls: @if(enable_comment_polls) true @else false @endif,
|
enable_comment_polls: @if(enable_comment_polls) true @else false @endif,
|
||||||
|
hide_sidebar_default: @if(hide_sidebar_default) true @else false @endif,
|
||||||
mode: {{ mode !== undefined ? mode : 0 }}
|
mode: {{ mode !== undefined ? mode : 0 }}
|
||||||
};
|
};
|
||||||
window.f0ckDebug = window.f0ckSession.development ? console.log.bind(console) : () => {};
|
window.f0ckDebug = window.f0ckSession.development ? console.log.bind(console) : () => {};
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
<meta name="mobile-web-app-capable" content="yes">
|
<meta name="mobile-web-app-capable" content="yes">
|
||||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
||||||
<meta name="apple-mobile-web-app-title" content="{{ domain }}">
|
<meta name="apple-mobile-web-app-title" content="{{ domain }}">
|
||||||
|
<script>(function(){var h=document.documentElement;if(localStorage.getItem('hideItemRatings')==='true')h.classList.add('hide-item-ratings-active');if(localStorage.getItem('blurNsfw')==='true')h.classList.add('blur-nsfw-active');if(localStorage.getItem('blurNsfl')==='true')h.classList.add('blur-nsfl-active');if(localStorage.getItem('blurSfw')==='true')h.classList.add('blur-sfw-active');if(localStorage.getItem('blurUntagged')==='true')h.classList.add('blur-untagged-active');if(localStorage.getItem('blurDetail')!=='false')h.classList.add('blur-detail-active');if(localStorage.getItem('imageExpandOnClick')!=='false')h.classList.add('image-expand-active');})();</script>
|
||||||
<style>
|
<style>
|
||||||
html { background-color: #000; color: #fff; }
|
html { background-color: #000; color: #fff; }
|
||||||
@if(session && session.font)
|
@if(session && session.font)
|
||||||
@@ -15,10 +16,18 @@
|
|||||||
font-family: 'CustomUserFont';
|
font-family: 'CustomUserFont';
|
||||||
src: url('/s/fonts/{{ session.font }}');
|
src: url('/s/fonts/{{ session.font }}');
|
||||||
}
|
}
|
||||||
|
@elseif(typeof default_font !== 'undefined' && default_font && default_font.length > 0)
|
||||||
|
{{ "@" }}font-face {
|
||||||
|
font-family: 'SiteDefaultFont';
|
||||||
|
src: url('/s/fonts/{{ default_font }}');
|
||||||
|
}
|
||||||
@endif
|
@endif
|
||||||
:root {
|
:root {
|
||||||
|
--favicon-url: url('@if(custom_favicon && custom_favicon.length > 0){{ custom_favicon }}@else/s/img/favicon.gif@endif');
|
||||||
@if(session && session.font)
|
@if(session && session.font)
|
||||||
--font: 'CustomUserFont', monospace !important;
|
--font: 'CustomUserFont', monospace !important;
|
||||||
|
@elseif(typeof default_font !== 'undefined' && default_font && default_font.length > 0)
|
||||||
|
--font: 'SiteDefaultFont', monospace !important;
|
||||||
@endif
|
@endif
|
||||||
}
|
}
|
||||||
@if(session && session.font)
|
@if(session && session.font)
|
||||||
@@ -30,6 +39,15 @@
|
|||||||
.fas::before, .far::before, .fab::before, .fa::before {
|
.fas::before, .far::before, .fab::before, .fa::before {
|
||||||
font-family: "Font Awesome 6 Free", "Font Awesome 6 Brands" !important;
|
font-family: "Font Awesome 6 Free", "Font Awesome 6 Brands" !important;
|
||||||
}
|
}
|
||||||
|
@elseif(typeof default_font !== 'undefined' && default_font && default_font.length > 0)
|
||||||
|
*:not(canvas):not(.meme-title):not(.force-impact):not(.fa-solid):not(.fa-regular):not(.fa-brands):not(.fa-light):not(.fa-thin):not(.fa-duotone):not([class*=" fa-"]):not([class^="fa-"]) {
|
||||||
|
font-family: 'SiteDefaultFont', monospace !important;
|
||||||
|
}
|
||||||
|
/* Explicitly protect FA pseudo-elements */
|
||||||
|
.fa-solid::before, .fa-regular::before, .fa-brands::before,
|
||||||
|
.fas::before, .far::before, .fab::before, .fa::before {
|
||||||
|
font-family: "Font Awesome 6 Free", "Font Awesome 6 Brands" !important;
|
||||||
|
}
|
||||||
@endif
|
@endif
|
||||||
</style>
|
</style>
|
||||||
<link rel="preload" href="/s/vcr.ttf" as="font" type="font/ttf" crossorigin>
|
<link rel="preload" href="/s/vcr.ttf" as="font" type="font/ttf" crossorigin>
|
||||||
@@ -78,7 +96,7 @@
|
|||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body class="@if(session)@if(session.use_new_layout)layout-modern@else layout-legacy @endif@else @if(default_layout === 'legacy')layout-legacy@else layout-modern@endif @endif @if(private_society && !session)private-gate-active@endif">
|
<body class="@if(session)@if(session.use_new_layout)layout-modern@else layout-legacy @endif@else @if(default_layout === 'legacy')layout-legacy@else layout-modern@endif @endif @if(private_society && !session)private-gate-active@endif">
|
||||||
<script>if(localStorage.getItem('sidebarRightHidden')==='true')document.body.classList.add('sidebar-right-hidden');if(localStorage.getItem('comments_hidden')==='true')document.body.classList.add('sidebar-left-hidden');(function(){var d=window.devicePixelRatio||1;console.log('[DPR] devicePixelRatio='+d);var t='dpr-1x';if(d>=1.55)t='dpr-high';else if(d>=1.39)t='dpr-150';else if(d>=1.26)t='dpr-130';else if(d>=1.20)t='dpr-120';else if(d>=1.05)t='dpr-110';document.documentElement.setAttribute('data-dpr',t);})();</script>
|
<script>var sRH = localStorage.getItem('sidebarRightHidden'); if(sRH==='true' || (sRH===null && @if(hide_sidebar_default) true @else false @endif)) document.body.classList.add('sidebar-right-hidden'); if(localStorage.getItem('comments_hidden')==='true')document.body.classList.add('sidebar-left-hidden');(function(){var d=window.devicePixelRatio||1;console.log('[DPR] devicePixelRatio='+d);var t='dpr-1x';if(d>=1.55)t='dpr-high';else if(d>=1.39)t='dpr-150';else if(d>=1.26)t='dpr-130';else if(d>=1.20)t='dpr-120';else if(d>=1.05)t='dpr-110';document.documentElement.setAttribute('data-dpr',t);})();</script>
|
||||||
@if(!private_society || session)
|
@if(!private_society || session)
|
||||||
<canvas class="hidden-xs" id="bg"></canvas>
|
<canvas class="hidden-xs" id="bg"></canvas>
|
||||||
@endif
|
@endif
|
||||||
|
|||||||
@@ -26,6 +26,9 @@
|
|||||||
</div>
|
</div>
|
||||||
@endif
|
@endif
|
||||||
<a href="/tags" title="{{ t('nav.tags') }}"><i class="fa-solid fa-tags"></i></a>
|
<a href="/tags" title="{{ t('nav.tags') }}"><i class="fa-solid fa-tags"></i></a>
|
||||||
|
@if(enable_global_chat)
|
||||||
|
<a href="/chat" title="{{ t('nav.chat') }}"><i class="fa-solid fa-comments"></i></a>
|
||||||
|
@endif
|
||||||
@if(abyss_enabled)
|
@if(abyss_enabled)
|
||||||
<a href="/abyss" title="{{ t('nav.abyss') }}"><i class="fa-solid fa-dice-d6"></i></a>
|
<a href="/abyss" title="{{ t('nav.abyss') }}"><i class="fa-solid fa-dice-d6"></i></a>
|
||||||
@endif
|
@endif
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
@each(toptags as toptag)
|
@each(toptags as toptag)
|
||||||
<a href="/tag/{{ toptag.safe_tag }}" class="tag-card">
|
<a href="/tag/{{ toptag.safe_tag }}@if(user && user.user)?tagger={!! encodeURIComponent(user.user) !!}@endif" class="tag-card">
|
||||||
<div class="tag-card-image">
|
<div class="tag-card-image">
|
||||||
<img src="/tag_image/{{ toptag.encoded_tag }}?m={{ session.mode }}" loading="lazy" alt="{!! toptag.tag !!}">
|
<img src="/tag_image/{{ toptag.encoded_tag }}?m={{ session.mode }}" loading="lazy" alt="{!! toptag.tag !!}">
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user