Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 162f1b40d0 | |||
| 9d2b0137da | |||
| e179eb1955 | |||
| a80e6dab96 | |||
| c22ae5bde0 | |||
| c9f5dc02fa | |||
| 4fd15ec338 | |||
| feb408338a |
@@ -29,6 +29,10 @@
|
|||||||
],
|
],
|
||||||
"enable_pdf": false,
|
"enable_pdf": false,
|
||||||
"enable_nsfl": false,
|
"enable_nsfl": false,
|
||||||
|
"enable_private_uploads": true,
|
||||||
|
"default_upload_visibility": 0,
|
||||||
|
"allow_user_upload_visibility": true,
|
||||||
|
"enable_item_slugs": true,
|
||||||
"nsfl_tag_id": 4,
|
"nsfl_tag_id": 4,
|
||||||
"allowedMimes": [
|
"allowedMimes": [
|
||||||
"audio",
|
"audio",
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ services:
|
|||||||
- ./f0ckm-data/fonts/:/opt/f0ckm/public/s/fonts/:Z
|
- ./f0ckm-data/fonts/:/opt/f0ckm/public/s/fonts/:Z
|
||||||
- ./f0ckm-data/hall_cache/:/opt/f0ckm/public/hall_cache/:Z
|
- ./f0ckm-data/hall_cache/:/opt/f0ckm/public/hall_cache/:Z
|
||||||
- ./f0ckm-data/hall_custom/:/opt/f0ckm/public/hall_custom/:Z
|
- ./f0ckm-data/hall_custom/:/opt/f0ckm/public/hall_custom/:Z
|
||||||
|
- ./f0ckm-data/koepfe/:/opt/f0ckm/public/s/koepfe/:Z
|
||||||
- ./f0ckm-data/manifest.json:/opt/f0ckm/public/manifest.json:Z
|
- ./f0ckm-data/manifest.json:/opt/f0ckm/public/manifest.json:Z
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
|
|||||||
0
f0ckm-data/koepfe/.gitkeep
Normal file
0
f0ckm-data/koepfe/.gitkeep
Normal file
38
migrations/add_private_unlisted_uploads.sql
Normal file
38
migrations/add_private_unlisted_uploads.sql
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
-- Add visibility and slug columns to items table
|
||||||
|
ALTER TABLE items ADD COLUMN IF NOT EXISTS visibility smallint DEFAULT 0;
|
||||||
|
ALTER TABLE items ADD COLUMN IF NOT EXISTS slug varchar(16) UNIQUE;
|
||||||
|
ALTER TABLE user_options ADD COLUMN IF NOT EXISTS default_upload_visibility smallint DEFAULT 0;
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_items_slug ON items(slug);
|
||||||
|
|
||||||
|
COMMENT ON COLUMN items.visibility IS '0=public, 1=unlisted, 2=private';
|
||||||
|
COMMENT ON COLUMN items.slug IS 'Unique unguessable 11-character URL slug for direct access';
|
||||||
|
COMMENT ON COLUMN user_options.default_upload_visibility IS 'Default upload visibility preference for user (0=public, 1=unlisted, 2=private)';
|
||||||
|
|
||||||
|
-- Populate missing slugs for existing items
|
||||||
|
DO $$
|
||||||
|
DECLARE
|
||||||
|
r RECORD;
|
||||||
|
chars text := 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-';
|
||||||
|
new_slug text;
|
||||||
|
i integer;
|
||||||
|
BEGIN
|
||||||
|
FOR r IN SELECT id FROM items WHERE slug IS NULL LOOP
|
||||||
|
LOOP
|
||||||
|
new_slug := '';
|
||||||
|
FOR i IN 1..11 LOOP
|
||||||
|
new_slug := new_slug || substr(chars, floor(random() * 64 + 1)::integer, 1);
|
||||||
|
END LOOP;
|
||||||
|
BEGIN
|
||||||
|
UPDATE items SET slug = new_slug WHERE id = r.id;
|
||||||
|
EXIT;
|
||||||
|
EXCEPTION WHEN unique_violation THEN
|
||||||
|
END;
|
||||||
|
END LOOP;
|
||||||
|
END LOOP;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
-- Fix any items with NULL visibility (should default to 0 = public)
|
||||||
|
UPDATE items SET visibility = 0 WHERE visibility IS NULL;
|
||||||
|
ALTER TABLE items ALTER COLUMN visibility SET NOT NULL;
|
||||||
|
ALTER TABLE items ALTER COLUMN visibility SET DEFAULT 0;
|
||||||
@@ -114,6 +114,7 @@ DROP INDEX IF EXISTS public.idx_items_username;
|
|||||||
DROP INDEX IF EXISTS public.idx_items_pinned_id;
|
DROP INDEX IF EXISTS public.idx_items_pinned_id;
|
||||||
DROP INDEX IF EXISTS public.idx_items_is_purged;
|
DROP INDEX IF EXISTS public.idx_items_is_purged;
|
||||||
DROP INDEX IF EXISTS public.idx_items_is_deleted;
|
DROP INDEX IF EXISTS public.idx_items_is_deleted;
|
||||||
|
DROP INDEX IF EXISTS public.idx_items_slug;
|
||||||
DROP INDEX IF EXISTS public.idx_items_active_id;
|
DROP INDEX IF EXISTS public.idx_items_active_id;
|
||||||
DROP INDEX IF EXISTS public.idx_global_chat_created_at;
|
DROP INDEX IF EXISTS public.idx_global_chat_created_at;
|
||||||
DROP INDEX IF EXISTS public.idx_discord_queue_sent;
|
DROP INDEX IF EXISTS public.idx_discord_queue_sent;
|
||||||
@@ -910,12 +911,18 @@ CREATE TABLE public.items (
|
|||||||
original_filename text,
|
original_filename text,
|
||||||
title text,
|
title text,
|
||||||
width integer,
|
width integer,
|
||||||
height integer
|
height integer,
|
||||||
|
visibility smallint DEFAULT 0 NOT NULL,
|
||||||
|
slug character varying(16)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
ALTER TABLE public.items OWNER TO f0ckm;
|
ALTER TABLE public.items OWNER TO f0ckm;
|
||||||
|
|
||||||
|
COMMENT ON COLUMN public.items.visibility IS '0=public, 1=unlisted, 2=private';
|
||||||
|
COMMENT ON COLUMN public.items.slug IS 'Unique unguessable 11-character URL slug for direct access';
|
||||||
|
|
||||||
|
|
||||||
--
|
--
|
||||||
-- Name: COLUMN items.src; Type: COMMENT; Schema: public; Owner: f0ckm
|
-- Name: COLUMN items.src; Type: COMMENT; Schema: public; Owner: f0ckm
|
||||||
--
|
--
|
||||||
@@ -1500,12 +1507,23 @@ CREATE TABLE public.user_options (
|
|||||||
comment_display_mode integer DEFAULT 1,
|
comment_display_mode integer DEFAULT 1,
|
||||||
force_comment_display_mode integer DEFAULT 0,
|
force_comment_display_mode integer DEFAULT 0,
|
||||||
favorites_private boolean DEFAULT false,
|
favorites_private boolean DEFAULT false,
|
||||||
hide_fav_badge boolean DEFAULT false
|
hide_fav_badge boolean DEFAULT false,
|
||||||
|
default_upload_visibility smallint DEFAULT 0,
|
||||||
|
banner_file character varying(255) DEFAULT NULL::character varying,
|
||||||
|
banner_position character varying(50) DEFAULT 'center'::character varying,
|
||||||
|
banner_size character varying(50) DEFAULT 'cover'::character varying
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
ALTER TABLE public.user_options OWNER TO f0ckm;
|
ALTER TABLE public.user_options OWNER TO f0ckm;
|
||||||
|
|
||||||
|
COMMENT ON COLUMN public.user_options.default_upload_visibility IS 'Default upload visibility preference for user (0=public, 1=unlisted, 2=private)';
|
||||||
|
COMMENT ON COLUMN public.user_options.banner_file IS 'Custom uploaded banner image filename, stored in public/a/';
|
||||||
|
COMMENT ON COLUMN public.user_options.banner_position IS 'CSS background-position for the banner';
|
||||||
|
COMMENT ON COLUMN public.user_options.banner_size IS 'CSS background-size for the banner';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
--
|
--
|
||||||
-- Name: COLUMN user_options.avatar_file; Type: COMMENT; Schema: public; Owner: f0ckm
|
-- Name: COLUMN user_options.avatar_file; Type: COMMENT; Schema: public; Owner: f0ckm
|
||||||
--
|
--
|
||||||
@@ -2164,6 +2182,13 @@ CREATE INDEX idx_global_chat_created_at ON public.global_chat USING btree (creat
|
|||||||
CREATE INDEX idx_items_active_id ON public.items USING btree (id) WHERE (active = true);
|
CREATE INDEX idx_items_active_id ON public.items USING btree (id) WHERE (active = true);
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: idx_items_slug; Type: INDEX; Schema: public; Owner: f0ckm
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX idx_items_slug ON public.items USING btree (slug);
|
||||||
|
|
||||||
|
|
||||||
--
|
--
|
||||||
-- Name: idx_items_is_deleted; Type: INDEX; Schema: public; Owner: f0ckm
|
-- Name: idx_items_is_deleted; Type: INDEX; Schema: public; Owner: f0ckm
|
||||||
--
|
--
|
||||||
@@ -2946,4 +2971,9 @@ ALTER TABLE public.user_emoji_favorites OWNER TO f0ckm;
|
|||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_user_emoji_favorites_user_id ON public.user_emoji_favorites (user_id);
|
CREATE INDEX IF NOT EXISTS idx_user_emoji_favorites_user_id ON public.user_emoji_favorites (user_id);
|
||||||
|
|
||||||
|
-- User Banners
|
||||||
|
ALTER TABLE public.user_options ADD COLUMN IF NOT EXISTS banner_file character varying(255) DEFAULT NULL;
|
||||||
|
ALTER TABLE public.user_options ADD COLUMN IF NOT EXISTS banner_position character varying(50) DEFAULT 'center';
|
||||||
|
ALTER TABLE public.user_options ADD COLUMN IF NOT EXISTS banner_size character varying(50) DEFAULT 'cover';
|
||||||
|
|
||||||
\unrestrict RMNKNzVQLV2ZcwmM3bmhglTot5nRoju9FmRyi3eUMfNy6iJUBfHRIgXnbrpJikG
|
\unrestrict RMNKNzVQLV2ZcwmM3bmhglTot5nRoju9FmRyi3eUMfNy6iJUBfHRIgXnbrpJikG
|
||||||
|
|||||||
2
migrations/fix_null_visibility.sql
Normal file
2
migrations/fix_null_visibility.sql
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
-- Fix items with NULL visibility (should default to 0 = public)
|
||||||
|
UPDATE items SET visibility = 0 WHERE visibility IS NULL;
|
||||||
@@ -362,6 +362,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.upload-form.shitpost-mode-active .global-rating-section,
|
.upload-form.shitpost-mode-active .global-rating-section,
|
||||||
|
.upload-form.shitpost-mode-active .global-visibility-section,
|
||||||
.upload-form.shitpost-mode-active .global-comment-section,
|
.upload-form.shitpost-mode-active .global-comment-section,
|
||||||
.upload-form.shitpost-mode-active .global-tag-section {
|
.upload-form.shitpost-mode-active .global-tag-section {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
@@ -419,6 +420,26 @@
|
|||||||
border-color: var(--badge-nsfl);
|
border-color: var(--badge-nsfl);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Visibility Container - Only checked option is colored, unchecked options are gray */
|
||||||
|
.item-visibility-container .item-rating-option input:checked + .item-rating-label.sfw {
|
||||||
|
background: rgba(81, 207, 102, 0.2);
|
||||||
|
color: #51cf66;
|
||||||
|
border-color: rgba(81, 207, 102, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-visibility-container .item-rating-option input:checked + .item-rating-label.nsfw {
|
||||||
|
background: rgba(255, 187, 51, 0.2);
|
||||||
|
color: #ffbb33;
|
||||||
|
border-color: rgba(255, 187, 51, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-visibility-container .item-rating-option input:checked + .item-rating-label.nsfl {
|
||||||
|
background: rgba(255, 68, 68, 0.2);
|
||||||
|
color: #ff4444;
|
||||||
|
border-color: rgba(255, 68, 68, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
.item-rating-label:hover {
|
.item-rating-label:hover {
|
||||||
@@ -662,6 +683,32 @@
|
|||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Global Visibility Section styling - unchecked options stay muted, checked options show colors */
|
||||||
|
.global-visibility-section .rating-option input:checked + .rating-label.sfw {
|
||||||
|
background: rgba(81, 207, 102, 0.2);
|
||||||
|
border-color: #51cf66;
|
||||||
|
color: #51cf66;
|
||||||
|
box-shadow: none;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.global-visibility-section .rating-option input:checked + .rating-label.nsfw {
|
||||||
|
background: rgba(255, 187, 51, 0.2);
|
||||||
|
border-color: #ffbb33;
|
||||||
|
color: #ffbb33;
|
||||||
|
box-shadow: none;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.global-visibility-section .rating-option input:checked + .rating-label.nsfl {
|
||||||
|
background: rgba(255, 68, 68, 0.2);
|
||||||
|
border-color: #ff4444;
|
||||||
|
color: #ff4444;
|
||||||
|
box-shadow: none;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/* Tags */
|
/* Tags */
|
||||||
.tag-input-container {
|
.tag-input-container {
|
||||||
background: rgba(255, 255, 255, 0.05);
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
|||||||
@@ -1,18 +1,24 @@
|
|||||||
(async () => {
|
(async () => {
|
||||||
// Helper to get dynamic context
|
// Helper to get dynamic context
|
||||||
const getContext = () => {
|
const getContext = () => {
|
||||||
const idLink = document.querySelector("a.id-link");
|
const commentsEl = document.querySelector("#comments-container");
|
||||||
if (!idLink) return null;
|
const favoEl = document.querySelector("#a_favo");
|
||||||
|
const infoEl = document.querySelector("#a_info");
|
||||||
|
const idLinkEl = document.querySelector("a.id-link");
|
||||||
|
|
||||||
|
const rawId = commentsEl?.dataset?.itemId || favoEl?.dataset?.itemId || infoEl?.dataset?.itemId || idLinkEl?.dataset?.itemId || idLinkEl?.innerText;
|
||||||
|
if (!rawId) return null;
|
||||||
|
|
||||||
const tagsContainer = document.querySelector("#tags");
|
const tagsContainer = document.querySelector("#tags");
|
||||||
const inner = tagsContainer.querySelector(".tags-inner") || tagsContainer;
|
const inner = tagsContainer ? (tagsContainer.querySelector(".tags-inner") || tagsContainer) : null;
|
||||||
const usernameEl = document.querySelector("a#a_username");
|
const usernameEl = document.querySelector("a#a_username");
|
||||||
return {
|
return {
|
||||||
postid: +idLink.innerText,
|
postid: /^\d+$/.test(String(rawId).trim()) ? parseInt(rawId, 10) : rawId.trim(),
|
||||||
// data-username holds the raw DB username; data-author-id holds the user's numeric ID.
|
// data-username holds the raw DB username; data-author-id holds the user's numeric ID.
|
||||||
// Never fall back to innerText — it may be a display name or the literal string 'unknown'.
|
// Never fall back to innerText — it may be a display name or the literal string 'unknown'.
|
||||||
poster: (usernameEl?.dataset?.username || '').trim() || null,
|
poster: (usernameEl?.dataset?.username || '').trim() || null,
|
||||||
authorId: (usernameEl?.dataset?.authorId || '').trim() || null,
|
authorId: (usernameEl?.dataset?.authorId || '').trim() || null,
|
||||||
tags: [...inner.querySelectorAll(".badge")].map(t => t.innerText.slice(0, -2))
|
tags: inner ? [...inner.querySelectorAll(".badge")].map(t => t.innerText.slice(0, -2)) : []
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -2405,10 +2405,50 @@ class CommentSystem {
|
|||||||
}
|
}
|
||||||
}, { passive: true });
|
}, { passive: true });
|
||||||
|
|
||||||
// Global click listener to close popups (useful for mobile dismissal)
|
// Global click listener for comment interaction (popups & expanding truncated comments in previews)
|
||||||
document.addEventListener('click', (e) => {
|
document.addEventListener('click', (e) => {
|
||||||
const isLink = e.target.closest('.comment-context-link');
|
const target = e.target;
|
||||||
const isPopup = e.target.closest('.comment-preview-popup');
|
|
||||||
|
// Load full comment (expand truncated)
|
||||||
|
const loadFullBtn = target.closest('.load-full-comment-btn');
|
||||||
|
if (loadFullBtn) {
|
||||||
|
const contentEl = loadFullBtn.closest('.comment-content');
|
||||||
|
if (contentEl) {
|
||||||
|
if (contentEl.querySelector('.collapse-comment-btn')) return;
|
||||||
|
const commentEl = contentEl.closest('.comment');
|
||||||
|
const commentId = commentEl ? (commentEl.dataset.id || (commentEl.id ? commentEl.id.replace(/^c/, '') : null)) : null;
|
||||||
|
const fullContent = contentEl.dataset.raw || (commentId && this.commentCache ? this.commentCache.get(commentId)?.content : null);
|
||||||
|
if (fullContent) {
|
||||||
|
contentEl.innerHTML = this.renderCommentContent(fullContent, null, true);
|
||||||
|
const seeLessLabel = (window.f0ckI18n?.sidebar_see_less) || 'see less';
|
||||||
|
contentEl.insertAdjacentHTML('beforeend',
|
||||||
|
`<span class="item-comment-truncated-notice"><button class="collapse-comment-btn" type="button">${seeLessLabel}</button></span>`
|
||||||
|
);
|
||||||
|
CommentSystem.playEmojiVideos(contentEl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collapse full comment back to truncated view
|
||||||
|
const collapseBtn = target.closest('.collapse-comment-btn');
|
||||||
|
if (collapseBtn) {
|
||||||
|
const contentEl = collapseBtn.closest('.comment-content');
|
||||||
|
if (contentEl) {
|
||||||
|
if (contentEl.querySelector('.load-full-comment-btn')) return;
|
||||||
|
const commentEl = contentEl.closest('.comment');
|
||||||
|
const commentId = commentEl ? (commentEl.dataset.id || (commentEl.id ? commentEl.id.replace(/^c/, '') : null)) : null;
|
||||||
|
const fullContent = contentEl.dataset.raw || (commentId && this.commentCache ? this.commentCache.get(commentId)?.content : null);
|
||||||
|
if (fullContent) {
|
||||||
|
contentEl.innerHTML = this.renderCommentContent(fullContent, null, false);
|
||||||
|
CommentSystem.playEmojiVideos(contentEl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isLink = target.closest('.comment-context-link');
|
||||||
|
const isPopup = target.closest('.comment-preview-popup');
|
||||||
|
|
||||||
if (!isLink && !isPopup) {
|
if (!isLink && !isPopup) {
|
||||||
this.closePreviewsAboveLevel(-1);
|
this.closePreviewsAboveLevel(-1);
|
||||||
@@ -2823,7 +2863,10 @@ class CommentSystem {
|
|||||||
if (loadFullBtn) {
|
if (loadFullBtn) {
|
||||||
const contentEl = loadFullBtn.closest('.comment-content');
|
const contentEl = loadFullBtn.closest('.comment-content');
|
||||||
if (contentEl) {
|
if (contentEl) {
|
||||||
const fullContent = contentEl.dataset.raw;
|
if (contentEl.querySelector('.collapse-comment-btn')) return;
|
||||||
|
const commentEl = contentEl.closest('.comment');
|
||||||
|
const commentId = commentEl ? (commentEl.dataset.id || (commentEl.id ? commentEl.id.replace(/^c/, '') : null)) : null;
|
||||||
|
const fullContent = contentEl.dataset.raw || (commentId && this.commentCache ? this.commentCache.get(commentId)?.content : null);
|
||||||
if (fullContent) {
|
if (fullContent) {
|
||||||
contentEl.innerHTML = this.renderCommentContent(fullContent, null, true);
|
contentEl.innerHTML = this.renderCommentContent(fullContent, null, true);
|
||||||
// Append "see less" button after full content
|
// Append "see less" button after full content
|
||||||
@@ -2831,6 +2874,7 @@ class CommentSystem {
|
|||||||
contentEl.insertAdjacentHTML('beforeend',
|
contentEl.insertAdjacentHTML('beforeend',
|
||||||
`<span class="item-comment-truncated-notice"><button class="collapse-comment-btn" type="button">${seeLessLabel}</button></span>`
|
`<span class="item-comment-truncated-notice"><button class="collapse-comment-btn" type="button">${seeLessLabel}</button></span>`
|
||||||
);
|
);
|
||||||
|
CommentSystem.playEmojiVideos(contentEl);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
@@ -2841,9 +2885,13 @@ class CommentSystem {
|
|||||||
if (collapseBtn) {
|
if (collapseBtn) {
|
||||||
const contentEl = collapseBtn.closest('.comment-content');
|
const contentEl = collapseBtn.closest('.comment-content');
|
||||||
if (contentEl) {
|
if (contentEl) {
|
||||||
const fullContent = contentEl.dataset.raw;
|
if (contentEl.querySelector('.load-full-comment-btn')) return;
|
||||||
|
const commentEl = contentEl.closest('.comment');
|
||||||
|
const commentId = commentEl ? (commentEl.dataset.id || (commentEl.id ? commentEl.id.replace(/^c/, '') : null)) : null;
|
||||||
|
const fullContent = contentEl.dataset.raw || (commentId && this.commentCache ? this.commentCache.get(commentId)?.content : null);
|
||||||
if (fullContent) {
|
if (fullContent) {
|
||||||
contentEl.innerHTML = this.renderCommentContent(fullContent, null, false);
|
contentEl.innerHTML = this.renderCommentContent(fullContent, null, false);
|
||||||
|
CommentSystem.playEmojiVideos(contentEl);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -965,7 +965,7 @@ window.cancelAnimFrame = (function () {
|
|||||||
'login-modal', 'register-modal', 'forgot-modal', 'reset-modal',
|
'login-modal', 'register-modal', 'forgot-modal', 'reset-modal',
|
||||||
'report-modal', 'halls-modal', 'metadata-modal', 'warning-modal',
|
'report-modal', 'halls-modal', 'metadata-modal', 'warning-modal',
|
||||||
'shortcuts-modal', 'upload-drag-modal', 'excluded-tags-overlay',
|
'shortcuts-modal', 'upload-drag-modal', 'excluded-tags-overlay',
|
||||||
'content-warning-modal', 'gchat-img-modal', 'image-modal', 'info-modal'
|
'content-warning-modal', 'gchat-img-modal', 'image-modal', 'info-modal', 'visibility-modal'
|
||||||
];
|
];
|
||||||
modalIds.forEach(id => {
|
modalIds.forEach(id => {
|
||||||
// Don't close the filter modal during a background mime-filter reload
|
// Don't close the filter modal during a background mime-filter reload
|
||||||
@@ -2442,8 +2442,8 @@ window.cancelAnimFrame = (function () {
|
|||||||
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\//) && (
|
||||||
pathname.match(/^\/\d+/) ||
|
pathname.match(/^\/\d+/) || pathname.match(/^\/[a-zA-Z0-9_-]{11}(?:[?#]|$)/) ||
|
||||||
(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]) || /^[a-zA-Z0-9_-]{11}$/.test(parts[parts.length - 1])))
|
||||||
);
|
);
|
||||||
const isMessages = !!pathname.match(/^\/messages(\/|$)/);
|
const isMessages = !!pathname.match(/^\/messages(\/|$)/);
|
||||||
const isAbyss = !!pathname.match(/^\/abyss(\/|$|\?|#)/) || pathname === '/abyss';
|
const isAbyss = !!pathname.match(/^\/abyss(\/|$|\?|#)/) || pathname === '/abyss';
|
||||||
@@ -3356,19 +3356,19 @@ window.cancelAnimFrame = (function () {
|
|||||||
// Extract item ID from URL. Use the last numeric segment to avoid matching context IDs (like tag/1/...)
|
// Extract item ID from URL. Use the last numeric segment to avoid matching context IDs (like tag/1/...)
|
||||||
// Split path, filter numeric, pop last.
|
// Split path, filter numeric, pop last.
|
||||||
const pathSegments = new URL(url, window.location.origin).pathname.split('/');
|
const pathSegments = new URL(url, window.location.origin).pathname.split('/');
|
||||||
const numericSegments = pathSegments.filter(s => /^\d+$/.test(s));
|
const keySegments = pathSegments.filter(s => /^\d+$/.test(s) || /^[a-zA-Z0-9_-]{11}$/.test(s));
|
||||||
|
|
||||||
// Clear and Hide navbar pagination for Item View (Never show grid pagination on item view)
|
// Clear and Hide navbar pagination for Item View (Never show grid pagination on item view)
|
||||||
// document.querySelectorAll('.pagination-wrapper').forEach(el => el.innerHTML = ''); // Don't destroy content (cached grid needs it)
|
// document.querySelectorAll('.pagination-wrapper').forEach(el => el.innerHTML = ''); // Don't destroy content (cached grid needs it)
|
||||||
document.querySelectorAll('.pagination-container-fluid').forEach(el => el.style.display = 'none');
|
document.querySelectorAll('.pagination-container-fluid').forEach(el => el.style.display = 'none');
|
||||||
|
|
||||||
if (numericSegments.length === 0) {
|
if (keySegments.length === 0) {
|
||||||
console.warn("loadItemAjax: No ID match found in URL", url);
|
console.warn("loadItemAjax: No ID or slug match found in URL", url);
|
||||||
// fallback for weird/external links
|
// fallback for weird/external links
|
||||||
window.location.href = url;
|
window.location.href = url;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const itemid = numericSegments.pop();
|
const itemid = keySegments.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, tagger = null;
|
let tag = null, user = null, isFavs = false, mime = null, hall = null, userHall = null, userHallOwner = null, tagger = null;
|
||||||
@@ -3551,18 +3551,19 @@ window.cancelAnimFrame = (function () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const _hash = new URL(url, window.location.origin).hash;
|
const _hash = new URL(url, window.location.origin).hash;
|
||||||
let _pushUrl = `/${itemid}`;
|
const itemKey = _cachedItem.slug || itemid;
|
||||||
if (userHall && userHallOwner) _pushUrl = `/user/${encodeURIComponent(userHallOwner)}/hall/${encodeURIComponent(userHall)}/${itemid}`;
|
let _pushUrl = `/${itemKey}`;
|
||||||
else if (user) { _pushUrl = `/user/${encodeURIComponent(user)}/${itemid}`; if (isFavs) _pushUrl = `/user/${encodeURIComponent(user)}/favs/${itemid}`; }
|
if (userHall && userHallOwner) _pushUrl = `/user/${encodeURIComponent(userHallOwner)}/hall/${encodeURIComponent(userHall)}/${itemKey}`;
|
||||||
else if (tag) _pushUrl = `/tag/${encodeURIComponent(tag).replace(/%2C/g,',').replace(/%20/g,' ')}/${itemid}`;
|
else if (user) { _pushUrl = `/user/${encodeURIComponent(user)}/${itemKey}`; if (isFavs) _pushUrl = `/user/${encodeURIComponent(user)}/favs/${itemKey}`; }
|
||||||
else if (hall) _pushUrl = `/h/${encodeURIComponent(hall).replace(/%20/g,' ')}/${itemid}`;
|
else if (tag) _pushUrl = `/tag/${encodeURIComponent(tag).replace(/%2C/g,',').replace(/%20/g,' ')}/${itemKey}`;
|
||||||
|
else if (hall) _pushUrl = `/h/${encodeURIComponent(hall).replace(/%20/g,' ')}/${itemKey}`;
|
||||||
if (tagger && tag) _pushUrl += `?tagger=${encodeURIComponent(tagger)}`;
|
if (tagger && tag) _pushUrl += `?tagger=${encodeURIComponent(tagger)}`;
|
||||||
if (mime) _pushUrl = _pushUrl.replace(new RegExp(`/${itemid}$`), `/${mime}/${itemid}`);
|
if (mime) _pushUrl = _pushUrl.replace(new RegExp(`/${itemKey}$`), `/${mime}/${itemKey}`);
|
||||||
if (_hash) _pushUrl += _hash;
|
if (_hash) _pushUrl += _hash;
|
||||||
|
|
||||||
if (!options.keepMedia && !options.skipPush) history.pushState({}, '', _pushUrl);
|
if (!options.keepMedia && !options.skipPush) history.pushState({}, '', _pushUrl);
|
||||||
|
|
||||||
document.title = `${window.f0ckDomain} - ${itemid}`;
|
document.title = `${window.f0ckDomain} - ${itemKey}`;
|
||||||
if (navbar) navbar.classList.remove('pbwork');
|
if (navbar) navbar.classList.remove('pbwork');
|
||||||
|
|
||||||
if (!options.keepMedia) {
|
if (!options.keepMedia) {
|
||||||
@@ -3587,14 +3588,14 @@ window.cancelAnimFrame = (function () {
|
|||||||
.then(r => r.ok ? r.text() : null)
|
.then(r => r.ok ? r.text() : null)
|
||||||
.then(freshText => {
|
.then(freshText => {
|
||||||
if (!freshText) return;
|
if (!freshText) return;
|
||||||
try { JSON.parse(freshText); } catch(_) {}
|
|
||||||
// Re-parse to get html string
|
|
||||||
let freshHtml = freshText;
|
let freshHtml = freshText;
|
||||||
|
let freshSlug = null;
|
||||||
try {
|
try {
|
||||||
const d = JSON.parse(freshText);
|
const d = JSON.parse(freshText);
|
||||||
if (d && typeof d.html === 'string') freshHtml = d.html;
|
if (d && typeof d.html === 'string') freshHtml = d.html;
|
||||||
|
if (d && (d.slug || d.item?.slug)) freshSlug = d.slug || d.item?.slug;
|
||||||
} catch(_) {}
|
} catch(_) {}
|
||||||
itemCacheMap.set(_itemCacheKey, { html: freshHtml, ts: Date.now() });
|
itemCacheMap.set(_itemCacheKey, { html: freshHtml, slug: freshSlug, ts: Date.now() });
|
||||||
window.f0ckDebug('[itemCache] Background revalidation complete for', _itemCacheKey);
|
window.f0ckDebug('[itemCache] Background revalidation complete for', _itemCacheKey);
|
||||||
})
|
})
|
||||||
.catch(() => {});
|
.catch(() => {});
|
||||||
@@ -3619,7 +3620,7 @@ window.cancelAnimFrame = (function () {
|
|||||||
- Total Network: ${(tBody - tStart).toFixed(2)}ms
|
- Total Network: ${(tBody - tStart).toFixed(2)}ms
|
||||||
- Content Size: ${(rawText.length / 1024).toFixed(2)} KB`);
|
- Content Size: ${(rawText.length / 1024).toFixed(2)} KB`);
|
||||||
|
|
||||||
let html, paginationHtml;
|
let html, paginationHtml, responseSlug = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Optimistically try to parse as JSON first
|
// Optimistically try to parse as JSON first
|
||||||
@@ -3632,6 +3633,7 @@ window.cancelAnimFrame = (function () {
|
|||||||
if (data && typeof data.html === 'string') {
|
if (data && typeof data.html === 'string') {
|
||||||
html = data.html;
|
html = data.html;
|
||||||
paginationHtml = data.pagination;
|
paginationHtml = data.pagination;
|
||||||
|
responseSlug = data.slug || data.item?.slug || null;
|
||||||
} else {
|
} else {
|
||||||
html = rawText;
|
html = rawText;
|
||||||
}
|
}
|
||||||
@@ -3642,7 +3644,7 @@ window.cancelAnimFrame = (function () {
|
|||||||
|
|
||||||
// ── Store in item cache (stale-while-revalidate) ───────────────────────
|
// ── Store in item cache (stale-while-revalidate) ───────────────────────
|
||||||
if (html && !options.skipCache) {
|
if (html && !options.skipCache) {
|
||||||
itemCacheMap.set(_itemCacheKey, { html, ts: Date.now() });
|
itemCacheMap.set(_itemCacheKey, { html, slug: responseSlug, ts: Date.now() });
|
||||||
if (itemCacheMap.size > ITEM_CACHE_MAX) {
|
if (itemCacheMap.size > ITEM_CACHE_MAX) {
|
||||||
// Evict oldest entry
|
// Evict oldest entry
|
||||||
itemCacheMap.delete(itemCacheMap.keys().next().value);
|
itemCacheMap.delete(itemCacheMap.keys().next().value);
|
||||||
@@ -3749,26 +3751,25 @@ window.cancelAnimFrame = (function () {
|
|||||||
// Construct proper History URL (Context Aware)
|
// Construct proper History URL (Context Aware)
|
||||||
// If we inherited context, we should reflect it in the URL
|
// If we inherited context, we should reflect it in the URL
|
||||||
const hash = new URL(url, window.location.origin).hash;
|
const hash = new URL(url, window.location.origin).hash;
|
||||||
let pushUrl = `/${itemid}`;
|
const itemKey = responseSlug || itemid;
|
||||||
|
let pushUrl = `/${itemKey}`;
|
||||||
// Logic from ajax.mjs context reconstruction:
|
// Logic from ajax.mjs context reconstruction:
|
||||||
if (userHall && userHallOwner) {
|
if (userHall && userHallOwner) {
|
||||||
pushUrl = `/user/${encodeURIComponent(userHallOwner)}/hall/${encodeURIComponent(userHall)}/${itemid}`;
|
pushUrl = `/user/${encodeURIComponent(userHallOwner)}/hall/${encodeURIComponent(userHall)}/${itemKey}`;
|
||||||
} else if (user) {
|
} else if (user) {
|
||||||
pushUrl = `/user/${encodeURIComponent(user)}/${itemid}`;
|
pushUrl = `/user/${encodeURIComponent(user)}/${itemKey}`;
|
||||||
if (isFavs) pushUrl = `/user/${encodeURIComponent(user)}/favs/${itemid}`;
|
if (isFavs) pushUrl = `/user/${encodeURIComponent(user)}/favs/${itemKey}`;
|
||||||
}
|
}
|
||||||
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, ' ')}/${itemKey}`;
|
||||||
else if (hall) pushUrl = `/h/${encodeURIComponent(hall).replace(/%20/g, ' ')}/${itemid}`;
|
else if (hall) pushUrl = `/h/${encodeURIComponent(hall).replace(/%20/g, ' ')}/${itemKey}`;
|
||||||
// Append tagger filter so item nav stays in tagger context
|
// Append tagger filter so item nav stays in tagger context
|
||||||
if (tagger && tag) pushUrl += `?tagger=${encodeURIComponent(tagger)}`;
|
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 itemKey at the end, insert mime before it
|
||||||
pushUrl = pushUrl.replace(new RegExp(`/${itemid}$`), `/${mime}/${itemid}`);
|
pushUrl = pushUrl.replace(new RegExp(`/${itemKey}$`), `/${mime}/${itemKey}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Re-append hash if present
|
// Re-append hash if present
|
||||||
if (hash) pushUrl += hash;
|
if (hash) pushUrl += hash;
|
||||||
|
|
||||||
@@ -3788,8 +3789,7 @@ window.cancelAnimFrame = (function () {
|
|||||||
if (window.initVisualizer) window.initVisualizer();
|
if (window.initVisualizer) window.initVisualizer();
|
||||||
}
|
}
|
||||||
// Try to extract ID from response if possible or just use itemid
|
// Try to extract ID from response if possible or just use itemid
|
||||||
document.title = `${window.f0ckDomain} - ${itemid}`;
|
document.title = `${window.f0ckDomain} - ${itemKey}`;
|
||||||
if (navbar) navbar.classList.remove("pbwork");
|
|
||||||
window.f0ckDebug("AJAX load complete");
|
window.f0ckDebug("AJAX load complete");
|
||||||
|
|
||||||
// Notify extensions — also triggers CommentSystem init which renders comments
|
// Notify extensions — also triggers CommentSystem init which renders comments
|
||||||
@@ -3821,6 +3821,7 @@ window.cancelAnimFrame = (function () {
|
|||||||
console.error("AJAX load failed:", err);
|
console.error("AJAX load failed:", err);
|
||||||
} finally {
|
} finally {
|
||||||
isNavigating = false;
|
isNavigating = false;
|
||||||
|
if (navbar) navbar.classList.remove("pbwork");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -3999,15 +4000,16 @@ window.cancelAnimFrame = (function () {
|
|||||||
fetch(randomUrl)
|
fetch(randomUrl)
|
||||||
.then(r => r.json())
|
.then(r => r.json())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
if (data.success && data.items && data.items.id) {
|
if (data.success && data.items && (data.items.slug || data.items.id)) {
|
||||||
|
const targetKey = data.items.slug || data.items.id;
|
||||||
// Navigate in the same context (user hall, favs, tag, etc.)
|
// Navigate in the same context (user hall, favs, tag, etc.)
|
||||||
if (wUserHall && wUserHallOwner) {
|
if (wUserHall && wUserHallOwner) {
|
||||||
loadItemAjax(`/user/${encodeURIComponent(wUserHallOwner)}/hall/${encodeURIComponent(wUserHall)}/${data.items.id}`, true);
|
loadItemAjax(`/user/${encodeURIComponent(wUserHallOwner)}/hall/${encodeURIComponent(wUserHall)}/${targetKey}`, true);
|
||||||
} else if (wFavsUser) {
|
} else if (wFavsUser) {
|
||||||
// Preserve /user/:name/favs/:id context so next/prev arrows stay within favs
|
// Preserve /user/:name/favs/:id context so next/prev arrows stay within favs
|
||||||
loadItemAjax(`/user/${encodeURIComponent(wFavsUser)}/favs/${data.items.id}`, true);
|
loadItemAjax(`/user/${encodeURIComponent(wFavsUser)}/favs/${targetKey}`, true);
|
||||||
} else {
|
} else {
|
||||||
loadItemAjax(`/${data.items.id}`, true);
|
loadItemAjax(`/${targetKey}`, true);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
window.location.href = link.href;
|
window.location.href = link.href;
|
||||||
@@ -4079,7 +4081,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(/^\/[a-zA-Z0-9_-]{11}(?:[?#]|$)/) || pathname.match(/^\/(about|rules|terms|upload|subscriptions|stats|docs|settings|admin|mod|ranking|messages|meme|memes)/) || pathname.startsWith('/abyss'))) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopImmediatePropagation();
|
e.stopImmediatePropagation();
|
||||||
|
|
||||||
@@ -4092,9 +4094,10 @@ window.cancelAnimFrame = (function () {
|
|||||||
const parts = pathname.split('/').filter(Boolean);
|
const parts = pathname.split('/').filter(Boolean);
|
||||||
const isItemLink = !pathname.match(/\/p\//) && (
|
const isItemLink = !pathname.match(/\/p\//) && (
|
||||||
pathname.match(/^\/\d+/) ||
|
pathname.match(/^\/\d+/) ||
|
||||||
(parts.length >= 3 && parts[0] === 'tag' && /^\d+$/.test(parts[parts.length - 1])) ||
|
pathname.match(/^\/[a-zA-Z0-9_-]{11}(?:[?#]|$)/) ||
|
||||||
(parts.length >= 3 && parts[0] === 'user' && /^\d+$/.test(parts[parts.length - 1])) ||
|
(parts.length >= 3 && parts[0] === 'tag' && (/^\d+$/.test(parts[parts.length - 1]) || /^[a-zA-Z0-9_-]{11}$/.test(parts[parts.length - 1]))) ||
|
||||||
(parts.length >= 3 && parts[0] === 'h' && /^\d+$/.test(parts[parts.length - 1]))
|
(parts.length >= 3 && parts[0] === 'user' && (/^\d+$/.test(parts[parts.length - 1]) || /^[a-zA-Z0-9_-]{11}$/.test(parts[parts.length - 1]))) ||
|
||||||
|
(parts.length >= 3 && parts[0] === 'h' && (/^\d+$/.test(parts[parts.length - 1]) || /^[a-zA-Z0-9_-]{11}$/.test(parts[parts.length - 1])))
|
||||||
);
|
);
|
||||||
if (isItemLink) {
|
if (isItemLink) {
|
||||||
// Links inside comment bodies or MOTD should not inherit tag/hall/user context
|
// Links inside comment bodies or MOTD should not inherit tag/hall/user context
|
||||||
@@ -4162,6 +4165,25 @@ window.cancelAnimFrame = (function () {
|
|||||||
})
|
})
|
||||||
.catch(console.error);
|
.catch(console.error);
|
||||||
|
|
||||||
|
} else if (target.closest('#a_visibility') || target.closest('#info-visibility-edit-btn')) {
|
||||||
|
e.preventDefault();
|
||||||
|
const visTrigger = target.closest('#a_visibility') || target.closest('#info-visibility-edit-btn');
|
||||||
|
const id = visTrigger.dataset.itemId || document.getElementById('visibility-item-id')?.value;
|
||||||
|
const visBtnMain = document.getElementById('a_visibility');
|
||||||
|
const rawVis = visTrigger.dataset.visibility ?? visBtnMain?.dataset.visibility ?? '0';
|
||||||
|
const currentVis = parseInt(rawVis, 10);
|
||||||
|
const modal = document.getElementById('visibility-modal');
|
||||||
|
if (modal) {
|
||||||
|
const inputId = document.getElementById('visibility-item-id');
|
||||||
|
if (inputId && id) inputId.value = id;
|
||||||
|
const radios = modal.querySelectorAll('input[name="visibility"]');
|
||||||
|
radios.forEach(r => {
|
||||||
|
r.checked = (parseInt(r.value, 10) === currentVis);
|
||||||
|
});
|
||||||
|
modal.style.display = 'flex';
|
||||||
|
document.body.classList.add('modal-open');
|
||||||
|
}
|
||||||
|
|
||||||
} else if (target.closest('#a_rethumb')) {
|
} else if (target.closest('#a_rethumb')) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const reBtn = target.closest('#a_rethumb');
|
const reBtn = target.closest('#a_rethumb');
|
||||||
@@ -4530,8 +4552,10 @@ window.cancelAnimFrame = (function () {
|
|||||||
const parts = p.split('/').filter(Boolean);
|
const parts = p.split('/').filter(Boolean);
|
||||||
const isItem = !p.match(/\/p\//) && (
|
const isItem = !p.match(/\/p\//) && (
|
||||||
p.match(/^\/\d+/) ||
|
p.match(/^\/\d+/) ||
|
||||||
(parts.length >= 3 && parts[0] === 'tag' && /^\d+$/.test(parts[parts.length - 1])) ||
|
p.match(/^\/[a-zA-Z0-9_-]{11}(?:[?#]|$)/) ||
|
||||||
(parts.length >= 3 && parts[0] === 'user' && /^\d+$/.test(parts[parts.length - 1]))
|
(parts.length >= 3 && parts[0] === 'tag' && (/^\d+$/.test(parts[parts.length - 1]) || /^[a-zA-Z0-9_-]{11}$/.test(parts[parts.length - 1]))) ||
|
||||||
|
(parts.length >= 3 && parts[0] === 'user' && (/^\d+$/.test(parts[parts.length - 1]) || /^[a-zA-Z0-9_-]{11}$/.test(parts[parts.length - 1]))) ||
|
||||||
|
(parts.length >= 3 && parts[0] === 'h' && (/^\d+$/.test(parts[parts.length - 1]) || /^[a-zA-Z0-9_-]{11}$/.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');
|
||||||
const isGridLike = url.match(/\/p\/\d+/) || url.match(/[?&]page=\d+/) || p === '/';
|
const isGridLike = url.match(/\/p\/\d+/) || url.match(/[?&]page=\d+/) || p === '/';
|
||||||
@@ -8103,7 +8127,7 @@ class NotificationSystem {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof window.loadItemAjax === 'function' && href.match(/^\/\d+/)) {
|
if (typeof window.loadItemAjax === 'function' && (href.match(/^\/\d+/) || href.match(/^\/[a-zA-Z0-9_-]{11}(?:[?#]|$)/) || href.match(/\/(?:user|tag|h)\/.*?\/(?:\d+|[a-zA-Z0-9_-]{11})/))) {
|
||||||
window.loadItemAjax(href, false);
|
window.loadItemAjax(href, false);
|
||||||
} else if (typeof window.loadPageAjax === 'function') {
|
} else if (typeof window.loadPageAjax === 'function') {
|
||||||
window.loadPageAjax(href, true);
|
window.loadPageAjax(href, true);
|
||||||
@@ -8389,7 +8413,8 @@ class NotificationSystem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
renderHistoryItem(n) {
|
renderHistoryItem(n) {
|
||||||
let link = `/${n.item_id}`;
|
const itemKey = n.item_slug || n.slug || n.item_id;
|
||||||
|
let link = `/${itemKey}`;
|
||||||
let msg = '';
|
let msg = '';
|
||||||
let user = n.from_display_name || n.from_user || 'System';
|
let user = n.from_display_name || n.from_user || 'System';
|
||||||
|
|
||||||
@@ -8398,14 +8423,14 @@ class NotificationSystem {
|
|||||||
const isDeleted = n.type === 'item_deleted';
|
const isDeleted = n.type === 'item_deleted';
|
||||||
const label = isDeleted ? (i18n.notif_upload_deleted || 'A moderator deleted your upload') : (i18n.notif_upload_denied || 'Your Upload was denied');
|
const label = isDeleted ? (i18n.notif_upload_deleted || 'A moderator deleted your upload') : (i18n.notif_upload_denied || 'Your Upload was denied');
|
||||||
const userLabel = isDeleted ? (i18n.notif_moderation || 'Moderation') : (i18n.notif_system || 'System');
|
const userLabel = isDeleted ? (i18n.notif_moderation || 'Moderation') : (i18n.notif_system || 'System');
|
||||||
const itemLink = `/${n.item_id}`;
|
const itemLink = `/${itemKey}`;
|
||||||
return `
|
return `
|
||||||
<a href="${itemLink}" class="notif-item ${n.is_read ? '' : 'unread'} notif-with-thumb" data-id="${n.id}">
|
<a href="${itemLink}" class="notif-item ${n.is_read ? '' : 'unread'} notif-with-thumb" data-id="${n.id}">
|
||||||
<div class="notif-thumb"${n.item_mode ? ` data-mode="${n.item_mode}"` : ''}><img src="/mod/deleted/t/${n.item_id}.webp" alt="thumb" onerror="this.onerror=null;this.src='/t/${n.item_id}.webp';this.onerror=function(){this.style.display='none';}"></div>
|
<div class="notif-thumb"${n.item_mode ? ` data-mode="${n.item_mode}"` : ''}><img src="/mod/deleted/t/${n.item_id}.webp" alt="thumb" onerror="this.onerror=null;this.src='/t/${n.item_id}.webp';this.onerror=function(){this.style.display='none';}"></div>
|
||||||
<div class="notif-content">
|
<div class="notif-content">
|
||||||
<div class="notif-user"><strong>${userLabel}</strong></div>
|
<div class="notif-user"><strong>${userLabel}</strong></div>
|
||||||
<div class="notif-msg">
|
<div class="notif-msg">
|
||||||
<strong>${label} #${n.item_id}</strong>
|
<strong>${label} #${itemKey}</strong>
|
||||||
<div class="notif-reason">${i18n.notif_reason_label || 'Reason:'} ${n.reason || (i18n.notif_no_reason || 'No reason provided')}</div>
|
<div class="notif-reason">${i18n.notif_reason_label || 'Reason:'} ${n.reason || (i18n.notif_no_reason || 'No reason provided')}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="notif-time">${new Date(n.created_at).toLocaleString()}</div>
|
<div class="notif-time">${new Date(n.created_at).toLocaleString()}</div>
|
||||||
@@ -8425,7 +8450,7 @@ class NotificationSystem {
|
|||||||
if (n.data?.msg) msg += ` <br><span style="color: #ff6060; font-size: 0.9em;">${n.data.msg}</span>`;
|
if (n.data?.msg) msg += ` <br><span style="color: #ff6060; font-size: 0.9em;">${n.data.msg}</span>`;
|
||||||
if (n.data?.url) msg += ` <br><small style="opacity: 0.6; word-break: break-all;">${n.data.url}</small>`;
|
if (n.data?.url) msg += ` <br><small style="opacity: 0.6; word-break: break-all;">${n.data.url}</small>`;
|
||||||
user = (window.f0ckI18n && window.f0ckI18n.notif_system) || 'System';
|
user = (window.f0ckI18n && window.f0ckI18n.notif_system) || 'System';
|
||||||
link = n.item_id ? `/${n.item_id}` : '#';
|
link = n.item_id ? `/${itemKey}` : '#';
|
||||||
} else if (n.type === 'admin_pending') {
|
} else if (n.type === 'admin_pending') {
|
||||||
link = '/mod/approve';
|
link = '/mod/approve';
|
||||||
user = (window.f0ckI18n && window.f0ckI18n.notif_admin) || 'Admin';
|
user = (window.f0ckI18n && window.f0ckI18n.notif_admin) || 'Admin';
|
||||||
@@ -8441,7 +8466,7 @@ class NotificationSystem {
|
|||||||
if (n.reason) msg += `<br><div class="notif-reason" style="font-size: 0.85em; color: #ffb8b8; margin-top: 3px;">${n.reason}</div>`;
|
if (n.reason) msg += `<br><div class="notif-reason" style="font-size: 0.85em; color: #ffb8b8; margin-top: 3px;">${n.reason}</div>`;
|
||||||
} else {
|
} else {
|
||||||
// Comment notification
|
// Comment notification
|
||||||
link = `/${n.item_id}#c${n.comment_id || n.reference_id}`;
|
link = `/${itemKey}#c${n.comment_id || n.reference_id}`;
|
||||||
if (n.type === 'comment_reply') msg = (window.f0ckI18n && window.f0ckI18n.notif_replied) || 'replied to you';
|
if (n.type === 'comment_reply') msg = (window.f0ckI18n && window.f0ckI18n.notif_replied) || 'replied to you';
|
||||||
else if (n.type === 'subscription') msg = (window.f0ckI18n && window.f0ckI18n.notif_subscribed) || 'commented in a thread you follow';
|
else if (n.type === 'subscription') msg = (window.f0ckI18n && window.f0ckI18n.notif_subscribed) || 'commented in a thread you follow';
|
||||||
else if (n.type === 'mention') msg = (window.f0ckI18n && window.f0ckI18n.notif_mentioned) || 'highlighted you';
|
else if (n.type === 'mention') msg = (window.f0ckI18n && window.f0ckI18n.notif_mentioned) || 'highlighted you';
|
||||||
@@ -8482,8 +8507,9 @@ class NotificationSystem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
renderItem(n) {
|
renderItem(n) {
|
||||||
|
const itemKey = n.item_slug || n.slug || n.item_id;
|
||||||
if (n.type === 'approve') {
|
if (n.type === 'approve') {
|
||||||
const link = `/${n.item_id}`;
|
const link = `/${itemKey}`;
|
||||||
return `
|
return `
|
||||||
<a href="${link}" class="notif-item ${n.is_read ? '' : 'unread'} notif-with-thumb" data-id="${n.id}">
|
<a href="${link}" class="notif-item ${n.is_read ? '' : 'unread'} notif-with-thumb" data-id="${n.id}">
|
||||||
<div class="notif-thumb"${n.item_mode ? ` data-mode="${n.item_mode}"` : ''}><img src="/t/${n.item_id}.webp" alt="thumb" onerror="this.onerror=null;this.src='/mod/pending/t/${n.item_id}.webp';this.onerror=function(){this.onerror=null;this.src='/mod/deleted/t/${n.item_id}.webp';this.onerror=function(){this.style.display='none';};}"></div>
|
<div class="notif-thumb"${n.item_mode ? ` data-mode="${n.item_mode}"` : ''}><img src="/t/${n.item_id}.webp" alt="thumb" onerror="this.onerror=null;this.src='/mod/pending/t/${n.item_id}.webp';this.onerror=function(){this.onerror=null;this.src='/mod/deleted/t/${n.item_id}.webp';this.onerror=function(){this.style.display='none';};}"></div>
|
||||||
@@ -8498,7 +8524,7 @@ class NotificationSystem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (n.type === 'upload_success') {
|
if (n.type === 'upload_success') {
|
||||||
const link = `/${n.item_id}`;
|
const link = `/${itemKey}`;
|
||||||
return `
|
return `
|
||||||
<a href="${link}" class="notif-item ${n.is_read ? '' : 'unread'} notif-with-thumb" data-id="${n.id}">
|
<a href="${link}" class="notif-item ${n.is_read ? '' : 'unread'} notif-with-thumb" data-id="${n.id}">
|
||||||
<div class="notif-thumb"${n.item_mode ? ` data-mode="${n.item_mode}"` : ''}><img src="/t/${n.item_id}.webp" alt="thumb" onerror="this.style.display='none';"></div>
|
<div class="notif-thumb"${n.item_mode ? ` data-mode="${n.item_mode}"` : ''}><img src="/t/${n.item_id}.webp" alt="thumb" onerror="this.style.display='none';"></div>
|
||||||
@@ -8517,7 +8543,7 @@ class NotificationSystem {
|
|||||||
const errMsg = n.data?.msg || '';
|
const errMsg = n.data?.msg || '';
|
||||||
const errDisplay = errMsg ? `<div style="color: #ffbaba; font-size: 0.85em; margin-top: 2px;">${errMsg}</div>` : '';
|
const errDisplay = errMsg ? `<div style="color: #ffbaba; font-size: 0.85em; margin-top: 2px;">${errMsg}</div>` : '';
|
||||||
const urlDisplay = url ? `<div style="font-size: 0.8em; opacity: 0.7; margin-top: 4px; word-break: break-all; max-height: 3.2em; overflow: hidden;">${url}</div>` : '';
|
const urlDisplay = url ? `<div style="font-size: 0.8em; opacity: 0.7; margin-top: 4px; word-break: break-all; max-height: 3.2em; overflow: hidden;">${url}</div>` : '';
|
||||||
const link = n.item_id ? `/${n.item_id}` : '#';
|
const link = n.item_id ? `/${itemKey}` : '#';
|
||||||
return `
|
return `
|
||||||
<a href="${link}" class="notif-item ${n.is_read ? '' : 'unread'}" data-id="${n.id}">
|
<a href="${link}" class="notif-item ${n.is_read ? '' : 'unread'}" data-id="${n.id}">
|
||||||
<div class="notif-content">
|
<div class="notif-content">
|
||||||
@@ -8541,7 +8567,7 @@ class NotificationSystem {
|
|||||||
<div class="notif-thumb"${n.item_mode ? ` data-mode="${n.item_mode}"` : ''}><img src="/mod/deleted/t/${n.item_id}.webp" alt="thumb" onerror="this.onerror=null;this.src='/t/${n.item_id}.webp';this.onerror=function(){this.style.display='none';}"></div>
|
<div class="notif-thumb"${n.item_mode ? ` data-mode="${n.item_mode}"` : ''}><img src="/mod/deleted/t/${n.item_id}.webp" alt="thumb" onerror="this.onerror=null;this.src='/t/${n.item_id}.webp';this.onerror=function(){this.style.display='none';}"></div>
|
||||||
<div class="notif-content">
|
<div class="notif-content">
|
||||||
<div>
|
<div>
|
||||||
<strong>${label} #${n.item_id}</strong>
|
<strong>${label} #${itemKey}</strong>
|
||||||
<div style="font-size: 0.85em; color: #ffb8b8; margin-top: 3px;">${(window.f0ckI18n && window.f0ckI18n.notif_click_reason) || 'Click to see reason'}</div>
|
<div style="font-size: 0.85em; color: #ffb8b8; margin-top: 3px;">${(window.f0ckI18n && window.f0ckI18n.notif_click_reason) || 'Click to see reason'}</div>
|
||||||
</div>
|
</div>
|
||||||
<small class="notif-time">${new Date(n.created_at).toLocaleString()}</small>
|
<small class="notif-time">${new Date(n.created_at).toLocaleString()}</small>
|
||||||
@@ -8600,10 +8626,10 @@ class NotificationSystem {
|
|||||||
if (n.type === 'comment_reply') typeText = (window.f0ckI18n && window.f0ckI18n.notif_replied) || 'replied to you';
|
if (n.type === 'comment_reply') typeText = (window.f0ckI18n && window.f0ckI18n.notif_replied) || 'replied to you';
|
||||||
else if (n.type === 'subscription') typeText = (window.f0ckI18n && window.f0ckI18n.notif_subscribed) || 'commented in a thread you follow';
|
else if (n.type === 'subscription') typeText = (window.f0ckI18n && window.f0ckI18n.notif_subscribed) || 'commented in a thread you follow';
|
||||||
else if (n.type === 'mention') typeText = (window.f0ckI18n && window.f0ckI18n.notif_mentioned) || 'highlighted you';
|
else if (n.type === 'mention') typeText = (window.f0ckI18n && window.f0ckI18n.notif_mentioned) || 'highlighted you';
|
||||||
else if (n.type === 'upload_comment') typeText = `${(window.f0ckI18n && window.f0ckI18n.notif_commented) || 'commented on your upload'} #${n.item_id}`;
|
else if (n.type === 'upload_comment') typeText = `${(window.f0ckI18n && window.f0ckI18n.notif_commented) || 'commented on your upload'} #${itemKey}`;
|
||||||
|
|
||||||
const cid = n.comment_id || n.reference_id;
|
const cid = n.comment_id || n.reference_id;
|
||||||
const link = `/${n.item_id}#c${cid}`;
|
const link = `/${itemKey}#c${cid}`;
|
||||||
|
|
||||||
const thumb = n.item_id ? `<div class="notif-thumb"${n.item_mode ? ` data-mode="${n.item_mode}"` : ''}><img src="/t/${n.item_id}.webp" alt="thumb" onerror="this.style.display='none'"></div>` : '';
|
const thumb = n.item_id ? `<div class="notif-thumb"${n.item_mode ? ` data-mode="${n.item_mode}"` : ''}><img src="/t/${n.item_id}.webp" alt="thumb" onerror="this.style.display='none'"></div>` : '';
|
||||||
return `
|
return `
|
||||||
@@ -8768,10 +8794,11 @@ class NotificationSystem {
|
|||||||
// Build DOM nodes imperatively (avoids Sanitizer stripping inline border-color)
|
// Build DOM nodes imperatively (avoids Sanitizer stripping inline border-color)
|
||||||
const fragment = document.createDocumentFragment();
|
const fragment = document.createDocumentFragment();
|
||||||
data.favs.forEach(fav => {
|
data.favs.forEach(fav => {
|
||||||
if (fav.hide_fav_badge) {
|
const isSelf = window.f0ckSession && window.f0ckSession.user && (window.f0ckSession.user === fav.user);
|
||||||
|
if (fav.hide_fav_badge && !isSelf) {
|
||||||
const a = document.createElement('a');
|
const a = document.createElement('a');
|
||||||
a.className = 'ghost-fav';
|
a.className = 'ghost-fav';
|
||||||
a.setAttribute('tooltip', 'Ghost Fav');
|
a.setAttribute('tooltip', '?');
|
||||||
a.setAttribute('flow', 'up');
|
a.setAttribute('flow', 'up');
|
||||||
a.style.cursor = 'default';
|
a.style.cursor = 'default';
|
||||||
|
|
||||||
@@ -8884,12 +8911,14 @@ class NotificationSystem {
|
|||||||
|
|
||||||
if (!isOwnUpload) return; // Silently drop as before for other users' uploads
|
if (!isOwnUpload) return; // Silently drop as before for other users' uploads
|
||||||
|
|
||||||
|
const itemKey = data.slug || data.id;
|
||||||
|
|
||||||
// Don't add duplicates
|
// Don't add duplicates
|
||||||
if (grid.querySelector(`a[href$="/${data.id}"]`)) return;
|
if (grid.querySelector(`a[href$="/${itemKey}"]`) || grid.querySelector(`a[href$="/${data.id}"]`)) return;
|
||||||
|
|
||||||
// Determine the link prefix from existing items
|
// Determine the link prefix from existing items
|
||||||
const firstThumb = grid.querySelector('a.thumb, a.lazy-thumb');
|
const firstThumb = grid.querySelector('a.thumb, a.lazy-thumb');
|
||||||
const linkBase = firstThumb ? firstThumb.getAttribute('href').replace(/\d+$/, '') : '/';
|
const linkBase = firstThumb ? firstThumb.getAttribute('href').replace(/(\d+|[a-zA-Z0-9_-]{11})$/, '') : '/';
|
||||||
|
|
||||||
// Build filter-reason label
|
// Build filter-reason label
|
||||||
const i18n = window.f0ckI18n || {};
|
const i18n = window.f0ckI18n || {};
|
||||||
@@ -8904,7 +8933,7 @@ class NotificationSystem {
|
|||||||
const mode = data.tag_id ? (data.tag_id === 1 ? 'sfw' : (data.tag_id === 2 ? 'nsfw' : (data.tag_id == nsflId ? 'nsfl' : 'null'))) : 'null';
|
const mode = data.tag_id ? (data.tag_id === 1 ? 'sfw' : (data.tag_id === 2 ? 'nsfw' : (data.tag_id == nsflId ? 'nsfl' : 'null'))) : 'null';
|
||||||
|
|
||||||
const ghost = document.createElement('a');
|
const ghost = document.createElement('a');
|
||||||
ghost.href = `${linkBase}${data.id}`;
|
ghost.href = `${linkBase}${itemKey}`;
|
||||||
ghost.className = 'thumb lazy-thumb filtered-upload-ghost loaded';
|
ghost.className = 'thumb lazy-thumb filtered-upload-ghost loaded';
|
||||||
ghost.dataset.file = data.dest;
|
ghost.dataset.file = data.dest;
|
||||||
ghost.dataset.mime = data.mime;
|
ghost.dataset.mime = data.mime;
|
||||||
@@ -8935,19 +8964,21 @@ class NotificationSystem {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const itemKey = data.slug || data.id;
|
||||||
|
|
||||||
// Don't add duplicates
|
// Don't add duplicates
|
||||||
if (grid.querySelector(`a[href$="/${data.id}"]`)) return;
|
if (grid.querySelector(`a[href$="/${itemKey}"]`) || grid.querySelector(`a[href$="/${data.id}"]`)) return;
|
||||||
|
|
||||||
// Determine the link prefix from existing items
|
// Determine the link prefix from existing items
|
||||||
const firstThumb = grid.querySelector('a.thumb, a.lazy-thumb');
|
const firstThumb = grid.querySelector('a.thumb, a.lazy-thumb');
|
||||||
const linkBase = firstThumb ? firstThumb.getAttribute('href').replace(/\d+$/, '') : '/';
|
const linkBase = firstThumb ? firstThumb.getAttribute('href').replace(/(\d+|[a-zA-Z0-9_-]{11})$/, '') : '/';
|
||||||
|
|
||||||
// Respect mode filter
|
// Respect mode filter
|
||||||
const nsflId = window.f0ckSession?.nsfl_tag_id;
|
const nsflId = window.f0ckSession?.nsfl_tag_id;
|
||||||
const mode = data.tag_id ? (data.tag_id === 1 ? 'sfw' : (data.tag_id === 2 ? 'nsfw' : (data.tag_id == nsflId ? 'nsfl' : 'null'))) : 'null';
|
const mode = data.tag_id ? (data.tag_id === 1 ? 'sfw' : (data.tag_id === 2 ? 'nsfw' : (data.tag_id == nsflId ? 'nsfl' : 'null'))) : 'null';
|
||||||
|
|
||||||
const thumb = document.createElement('a');
|
const thumb = document.createElement('a');
|
||||||
thumb.href = `${linkBase}${data.id}`;
|
thumb.href = `${linkBase}${itemKey}`;
|
||||||
thumb.className = 'thumb lazy-thumb';
|
thumb.className = 'thumb lazy-thumb';
|
||||||
thumb.dataset.file = data.dest;
|
thumb.dataset.file = data.dest;
|
||||||
thumb.dataset.mime = data.mime;
|
thumb.dataset.mime = data.mime;
|
||||||
@@ -9129,10 +9160,11 @@ class NotificationSystem {
|
|||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const itemKey = c.item_slug || c.slug || c.item_id;
|
||||||
itemPreview = `
|
itemPreview = `
|
||||||
<div class="item-preview" style="margin-top: 10px; display: flex; align-items: center; gap: 10px; background: rgba(0,0,0,0.2); padding: 5px; border-radius: 4px; border: 1px solid rgba(255,255,255,0.05);">
|
<div class="item-preview" style="margin-top: 10px; display: flex; align-items: center; gap: 10px; background: rgba(0,0,0,0.2); padding: 5px; border-radius: 4px; border: 1px solid rgba(255,255,255,0.05);">
|
||||||
<a href="/${c.item_id}">${mediaHtml}</a>
|
<a href="/${itemKey}">${mediaHtml}</a>
|
||||||
<a href="/${c.item_id}#c${c.id}" style="font-size: 0.8em; color: var(--accent); text-decoration: none;">View Context »</a>
|
<a href="/${itemKey}#c${c.id}" style="font-size: 0.8em; color: var(--accent); text-decoration: none;">View Context »</a>
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -11302,6 +11334,83 @@ document.addEventListener('click', (e) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const visCancelBtn = e.target.closest('#visibility-modal-cancel');
|
||||||
|
if (visCancelBtn || e.target.id === 'visibility-modal') {
|
||||||
|
const modal = document.getElementById('visibility-modal');
|
||||||
|
if (modal) {
|
||||||
|
modal.style.display = 'none';
|
||||||
|
document.body.classList.remove('modal-open');
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const visSaveBtn = e.target.closest('#visibility-modal-save');
|
||||||
|
if (visSaveBtn) {
|
||||||
|
e.preventDefault();
|
||||||
|
const modal = document.getElementById('visibility-modal');
|
||||||
|
const inputId = document.getElementById('visibility-item-id');
|
||||||
|
const selectedRadio = modal ? modal.querySelector('input[name="visibility"]:checked') : null;
|
||||||
|
if (!inputId || !selectedRadio) return;
|
||||||
|
|
||||||
|
const id = inputId.value;
|
||||||
|
const nextVis = parseInt(selectedRadio.value, 10);
|
||||||
|
|
||||||
|
visSaveBtn.disabled = true;
|
||||||
|
const origText = visSaveBtn.textContent;
|
||||||
|
visSaveBtn.textContent = 'Saving...';
|
||||||
|
|
||||||
|
fetch('/api/v2/item/visibility', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
'X-CSRF-Token': window.f0ckSession?.csrf_token
|
||||||
|
},
|
||||||
|
body: new URLSearchParams({ postid: id, id: id, visibility: nextVis })
|
||||||
|
})
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(data => {
|
||||||
|
visSaveBtn.disabled = false;
|
||||||
|
visSaveBtn.textContent = origText;
|
||||||
|
if (data.success) {
|
||||||
|
const visVal = parseInt(data.visibility, 10);
|
||||||
|
const titles = ['Public', 'Unlisted', 'Private'];
|
||||||
|
const icons = ['fa-globe', 'fa-link', 'fa-lock'];
|
||||||
|
const colors = ['var(--color-success, #00C851)', 'var(--color-warning, #ffbb33)', 'var(--color-danger, #ff4444)'];
|
||||||
|
|
||||||
|
const visBtn = document.getElementById('a_visibility');
|
||||||
|
if (visBtn) {
|
||||||
|
visBtn.dataset.visibility = visVal;
|
||||||
|
visBtn.setAttribute('title', `Visibility: ${titles[visVal]} (Click to change)`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const infoVisBtn = document.getElementById('info-visibility-edit-btn');
|
||||||
|
if (infoVisBtn) {
|
||||||
|
infoVisBtn.dataset.visibility = visVal;
|
||||||
|
}
|
||||||
|
|
||||||
|
const infoVisLabel = document.getElementById('info-visibility-label');
|
||||||
|
if (infoVisLabel) {
|
||||||
|
infoVisLabel.innerHTML = `<i class="fa-solid ${icons[visVal]}" style="color: ${colors[visVal]};"></i> ${titles[visVal]}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (modal) {
|
||||||
|
modal.style.display = 'none';
|
||||||
|
document.body.classList.remove('modal-open');
|
||||||
|
}
|
||||||
|
if (window.flashMessage) window.flashMessage(`VISIBILITY SET TO ${titles[visVal].toUpperCase()}`);
|
||||||
|
} else {
|
||||||
|
if (window.flashError) window.flashError(data.msg || 'Failed to update visibility');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
visSaveBtn.disabled = false;
|
||||||
|
visSaveBtn.textContent = origText;
|
||||||
|
console.error('Error changing visibility:', err);
|
||||||
|
if (window.flashError) window.flashError('Network error');
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Title save button
|
// Title save button
|
||||||
const saveBtn = e.target.closest('#info-title-save');
|
const saveBtn = e.target.closest('#info-title-save');
|
||||||
if (saveBtn) {
|
if (saveBtn) {
|
||||||
|
|||||||
@@ -1181,6 +1181,35 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const defaultUploadVisSelect = document.getElementById('default_upload_visibility_select');
|
||||||
|
if (defaultUploadVisSelect) {
|
||||||
|
defaultUploadVisSelect.addEventListener('change', async function() {
|
||||||
|
const vis = parseInt(defaultUploadVisSelect.value, 10);
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/v2/settings/default_upload_visibility', {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
'X-CSRF-Token': window.f0ckSession ? window.f0ckSession.csrf_token : ''
|
||||||
|
},
|
||||||
|
body: new URLSearchParams({ default_upload_visibility: vis })
|
||||||
|
});
|
||||||
|
const data = await res.json();
|
||||||
|
if (data.success) {
|
||||||
|
showStatus('Default upload visibility updated!', 'success');
|
||||||
|
if (window.f0ckSession) {
|
||||||
|
window.f0ckSession.default_upload_visibility = vis;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
alert(data.msg || 'Error saving preference');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Update Default Upload Visibility error:', err);
|
||||||
|
alert('Connection error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// New Dual Column Layout Toggle
|
// New Dual Column Layout Toggle
|
||||||
const layoutToggle = document.getElementById('use_new_layout_toggle');
|
const layoutToggle = document.getElementById('use_new_layout_toggle');
|
||||||
if (layoutToggle) {
|
if (layoutToggle) {
|
||||||
|
|||||||
@@ -479,8 +479,9 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
const renderActivityItem = (c) => {
|
const renderActivityItem = (c) => {
|
||||||
|
const itemKey = c.item_slug || c.slug || c.item_id;
|
||||||
const rawContent = c.content || c.body || '';
|
const rawContent = c.content || c.body || '';
|
||||||
let displayContent = renderCommentContent(rawContent, c.id, c.item_id);
|
let displayContent = renderCommentContent(rawContent, c.id, itemKey);
|
||||||
|
|
||||||
displayContent = window.f0cklib?.processMentions ? window.f0cklib.processMentions(displayContent) : displayContent;
|
displayContent = window.f0cklib?.processMentions ? window.f0cklib.processMentions(displayContent) : displayContent;
|
||||||
|
|
||||||
@@ -497,7 +498,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
const attachmentsHtml = renderCommentAttachments(c.files, rawContent);
|
const attachmentsHtml = renderCommentAttachments(c.files, rawContent);
|
||||||
const pollHtml = renderSidebarPoll(c.poll, c.id, c.item_id);
|
const pollHtml = renderSidebarPoll(c.poll, c.id, itemKey);
|
||||||
|
|
||||||
// Build avatar URL — same priority as the rest of the app
|
// Build avatar URL — same priority as the rest of the app
|
||||||
let avatarSrc = '/a/default.png';
|
let avatarSrc = '/a/default.png';
|
||||||
@@ -542,8 +543,8 @@
|
|||||||
|
|
||||||
itemPreview = `
|
itemPreview = `
|
||||||
<div class="item-preview">
|
<div class="item-preview">
|
||||||
<a href="/${c.item_id}" class="sidebar-thumb-link" data-mode="${rClass}">${mediaHtml}</a>
|
<a href="/${itemKey}" class="sidebar-thumb-link" data-mode="${rClass}">${mediaHtml}</a>
|
||||||
<a href="/${c.item_id}#c${c.id}" style="font-size: 0.8em; color: var(--accent); text-decoration: none;">${(window.f0ckI18n && window.f0ckI18n.sidebar_view) || 'View'} »</a>
|
<a href="/${itemKey}#c${c.id}" style="font-size: 0.8em; color: var(--accent); text-decoration: none;">${(window.f0ckI18n && window.f0ckI18n.sidebar_view) || 'View'} »</a>
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -908,7 +908,7 @@ window.initUploadForm = (selector) => {
|
|||||||
}
|
}
|
||||||
lines.forEach(url => {
|
lines.forEach(url => {
|
||||||
if (!selectedFiles.some(item => item.type === 'url' && item.url === url)) {
|
if (!selectedFiles.some(item => item.type === 'url' && item.url === url)) {
|
||||||
selectedFiles.push({ type: 'url', url, rating: '', tags: [], comment: '', title: '', is_oc: false });
|
selectedFiles.push({ type: 'url', url, rating: '', visibility: '', tags: [], comment: '', title: '', is_oc: false });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
urlInput.value = '';
|
urlInput.value = '';
|
||||||
@@ -931,7 +931,7 @@ window.initUploadForm = (selector) => {
|
|||||||
const val = urlInput.value.trim();
|
const val = urlInput.value.trim();
|
||||||
if (!val || !/^https?:\/\//i.test(val)) return;
|
if (!val || !/^https?:\/\//i.test(val)) return;
|
||||||
if (!selectedFiles.some(item => item.type === 'url' && item.url === val)) {
|
if (!selectedFiles.some(item => item.type === 'url' && item.url === val)) {
|
||||||
selectedFiles.push({ type: 'url', url: val, rating: '', tags: [], comment: '', title: '', is_oc: false });
|
selectedFiles.push({ type: 'url', url: val, rating: '', visibility: '', tags: [], comment: '', title: '', is_oc: false });
|
||||||
}
|
}
|
||||||
urlInput.value = '';
|
urlInput.value = '';
|
||||||
if (urlBadge) urlBadge.style.display = 'none';
|
if (urlBadge) urlBadge.style.display = 'none';
|
||||||
@@ -1177,7 +1177,7 @@ window.initUploadForm = (selector) => {
|
|||||||
|
|
||||||
if (!selectedFiles.some(f => (f.file || f).name === file.name && (f.file || f).size === file.size)) {
|
if (!selectedFiles.some(f => (f.file || f).name === file.name && (f.file || f).size === file.size)) {
|
||||||
if (isShitpost) {
|
if (isShitpost) {
|
||||||
selectedFiles.push({ type: 'file', file: file, rating: '', tags: [], comment: '', title: '', is_oc: false });
|
selectedFiles.push({ type: 'file', file: file, rating: '', visibility: '', tags: [], comment: '', title: '', is_oc: false });
|
||||||
} else {
|
} else {
|
||||||
selectedFiles.push(file); // Legacy single file mode uses raw File
|
selectedFiles.push(file); // Legacy single file mode uses raw File
|
||||||
}
|
}
|
||||||
@@ -1490,6 +1490,7 @@ window.initUploadForm = (selector) => {
|
|||||||
infoRow.className = 'file-meta-row-small';
|
infoRow.className = 'file-meta-row-small';
|
||||||
|
|
||||||
let ratingSwitch = '';
|
let ratingSwitch = '';
|
||||||
|
let visibilitySwitch = '';
|
||||||
let tagsUI = '';
|
let tagsUI = '';
|
||||||
let ocUI = '';
|
let ocUI = '';
|
||||||
let commentUI = '';
|
let commentUI = '';
|
||||||
@@ -1517,6 +1518,31 @@ window.initUploadForm = (selector) => {
|
|||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
const hasVisSection = !!form.querySelector('.global-visibility-section');
|
||||||
|
if (hasVisSection) {
|
||||||
|
const globalVis = form.querySelector('input[name="visibility"]:checked')?.value || '0';
|
||||||
|
const visValue = (item.visibility !== undefined && item.visibility !== '') ? item.visibility : globalVis;
|
||||||
|
item.visibility = visValue;
|
||||||
|
visibilitySwitch = `
|
||||||
|
<div class="item-visibility-container item-rating-container" style="margin-top: 4px;">
|
||||||
|
<label class="item-rating-option">
|
||||||
|
<input type="radio" name="visibility_${index}" value="0" ${visValue === '0' || visValue === 0 ? 'checked' : ''}>
|
||||||
|
<span class="item-rating-label sfw" style="display: inline-flex; align-items: center; gap: 4px;"><i class="fa-solid fa-globe"></i> Public</span>
|
||||||
|
</label>
|
||||||
|
<label class="item-rating-option">
|
||||||
|
<input type="radio" name="visibility_${index}" value="1" ${visValue === '1' || visValue === 1 ? 'checked' : ''}>
|
||||||
|
<span class="item-rating-label nsfw" style="display: inline-flex; align-items: center; gap: 4px;"><i class="fa-solid fa-link"></i> Unlisted</span>
|
||||||
|
</label>
|
||||||
|
<label class="item-rating-option">
|
||||||
|
<input type="radio" name="visibility_${index}" value="2" ${visValue === '2' || visValue === 2 ? 'checked' : ''}>
|
||||||
|
<span class="item-rating-label nsfl" style="display: inline-flex; align-items: center; gap: 4px;"><i class="fa-solid fa-lock"></i> Private</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
} else {
|
||||||
|
visibilitySwitch = '';
|
||||||
|
}
|
||||||
|
|
||||||
const tagsPlaceholder = window.f0ckI18n?.upload_tags_placeholder || 'Tags...';
|
const tagsPlaceholder = window.f0ckI18n?.upload_tags_placeholder || 'Tags...';
|
||||||
const minTagsHint = shitpostMinTags > 0 ? ` (min ${shitpostMinTags})` : '';
|
const minTagsHint = shitpostMinTags > 0 ? ` (min ${shitpostMinTags})` : '';
|
||||||
tagsUI = `
|
tagsUI = `
|
||||||
@@ -1560,19 +1586,27 @@ window.initUploadForm = (selector) => {
|
|||||||
</div>
|
</div>
|
||||||
${titleUI}
|
${titleUI}
|
||||||
${ratingSwitch}
|
${ratingSwitch}
|
||||||
|
${visibilitySwitch}
|
||||||
${tagsUI}
|
${tagsUI}
|
||||||
${commentUI}
|
${commentUI}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
if (isShitpost) {
|
if (isShitpost) {
|
||||||
// Handle Rating
|
// Handle Rating
|
||||||
infoRow.querySelectorAll('.item-rating-option input').forEach(radio => {
|
infoRow.querySelectorAll('.item-rating-container:not(.item-visibility-container) input').forEach(radio => {
|
||||||
radio.onchange = () => {
|
radio.onchange = () => {
|
||||||
item.rating = radio.value;
|
item.rating = radio.value;
|
||||||
updateSubmitButton();
|
updateSubmitButton();
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Handle Visibility
|
||||||
|
infoRow.querySelectorAll('.item-visibility-container input').forEach(radio => {
|
||||||
|
radio.onchange = () => {
|
||||||
|
item.visibility = radio.value;
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
// Handle Comment
|
// Handle Comment
|
||||||
const commentInput = infoRow.querySelector('.item-comment-input');
|
const commentInput = infoRow.querySelector('.item-comment-input');
|
||||||
const emojiTrigger = infoRow.querySelector('.item-emoji-trigger');
|
const emojiTrigger = infoRow.querySelector('.item-emoji-trigger');
|
||||||
@@ -2436,6 +2470,8 @@ window.initUploadForm = (selector) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const globalVisEl = form.querySelector('input[name="visibility"]:checked');
|
||||||
|
const visibilityVal = globalVisEl ? globalVisEl.value : '0';
|
||||||
const resp = await fetch('/api/v2/upload-url', {
|
const resp = await fetch('/api/v2/upload-url', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
@@ -2445,7 +2481,8 @@ window.initUploadForm = (selector) => {
|
|||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
url,
|
url,
|
||||||
rating: globalRatingEl.value,
|
rating: globalRatingEl ? globalRatingEl.value : 'sfw',
|
||||||
|
visibility: visibilityVal,
|
||||||
tags: tags.join(','),
|
tags: tags.join(','),
|
||||||
comment: comment,
|
comment: comment,
|
||||||
is_oc: isOc,
|
is_oc: isOc,
|
||||||
@@ -2552,9 +2589,11 @@ window.initUploadForm = (selector) => {
|
|||||||
|
|
||||||
for (let i = 0; i < selectedFiles.length; i++) {
|
for (let i = 0; i < selectedFiles.length; i++) {
|
||||||
const item = selectedFiles[i];
|
const item = selectedFiles[i];
|
||||||
|
const globalVisEl = form.querySelector('input[name="visibility"]:checked');
|
||||||
const isUrlItem = isShitpost && item.type === 'url';
|
const isUrlItem = isShitpost && item.type === 'url';
|
||||||
const file = !isUrlItem ? (isShitpost ? item.file : item) : null;
|
const file = !isUrlItem ? (isShitpost ? item.file : item) : null;
|
||||||
const fileRating = isShitpost ? item.rating : (globalRatingEl ? globalRatingEl.value : 'sfw');
|
const fileRating = isShitpost ? item.rating : (globalRatingEl ? globalRatingEl.value : 'sfw');
|
||||||
|
const fileVisibility = isShitpost ? (item.visibility || globalVisEl?.value || '0') : (globalVisEl?.value || '0');
|
||||||
const fileTags = isShitpost ? item.tags : tags;
|
const fileTags = isShitpost ? item.tags : tags;
|
||||||
const fileComment = isShitpost ? item.comment : comment;
|
const fileComment = isShitpost ? item.comment : comment;
|
||||||
const fileTitle = isShitpost ? (item.title || '') : titleVal;
|
const fileTitle = isShitpost ? (item.title || '') : titleVal;
|
||||||
@@ -2571,6 +2610,7 @@ window.initUploadForm = (selector) => {
|
|||||||
formData.append('file', file);
|
formData.append('file', file);
|
||||||
}
|
}
|
||||||
formData.append('rating', fileRating);
|
formData.append('rating', fileRating);
|
||||||
|
formData.append('visibility', fileVisibility);
|
||||||
formData.append('tags', fileTags.join(','));
|
formData.append('tags', fileTags.join(','));
|
||||||
formData.append('is_oc', (isShitpost ? item.is_oc : isOc) ? 'true' : 'false');
|
formData.append('is_oc', (isShitpost ? item.is_oc : isOc) ? 'true' : 'false');
|
||||||
if (isShitpost) formData.append('is_shitpost', 'true');
|
if (isShitpost) formData.append('is_shitpost', 'true');
|
||||||
@@ -2622,6 +2662,7 @@ window.initUploadForm = (selector) => {
|
|||||||
xhr.send(JSON.stringify({
|
xhr.send(JSON.stringify({
|
||||||
url: item.url,
|
url: item.url,
|
||||||
rating: fileRating,
|
rating: fileRating,
|
||||||
|
visibility: fileVisibility,
|
||||||
tags: fileTags.join(','),
|
tags: fileTags.join(','),
|
||||||
is_oc: (isShitpost ? item.is_oc : isOc),
|
is_oc: (isShitpost ? item.is_oc : isOc),
|
||||||
comment: fileComment,
|
comment: fileComment,
|
||||||
@@ -2738,17 +2779,14 @@ window.initUploadForm = (selector) => {
|
|||||||
// Skip redirect if every item was a background URL job
|
// Skip redirect if every item was a background URL job
|
||||||
const allPending = lastData?.pending && selectedFiles.every(i => i.type === 'url');
|
const allPending = lastData?.pending && selectedFiles.every(i => i.type === 'url');
|
||||||
if (!allPending) {
|
if (!allPending) {
|
||||||
// Inject now if the grid is already in the DOM (upload modal open on main page)
|
const targetUrl = (lastData && (lastData.visibility > 0 || lastData.redirect || lastData.slug))
|
||||||
injectNewItem();
|
? (lastData.redirect || `/${lastData.slug || lastData.itemid}`)
|
||||||
// Navigate to main page, then inject again after the grid has loaded.
|
: '/';
|
||||||
// Awaiting loadPageAjax ensures the .posts grid DOM is present before the
|
|
||||||
// handleNewItem call — this covers the item-page drag-and-upload scenario
|
|
||||||
// where no grid exists until after navigation completes.
|
|
||||||
if (typeof window.loadPageAjax === 'function') {
|
if (typeof window.loadPageAjax === 'function') {
|
||||||
await window.loadPageAjax('/', true, { bypassCache: true });
|
await window.loadPageAjax(targetUrl, true, { bypassCache: true });
|
||||||
injectNewItem();
|
if (targetUrl === '/') injectNewItem();
|
||||||
} else {
|
} else {
|
||||||
window.location.href = '/';
|
window.location.href = targetUrl;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -1,14 +1,20 @@
|
|||||||
(async () => {
|
(async () => {
|
||||||
// Helper to get dynamic context from the DOM
|
// Helper to get dynamic context from the DOM
|
||||||
const getContext = () => {
|
const getContext = () => {
|
||||||
const idLink = document.querySelector("a.id-link");
|
const commentsEl = document.querySelector("#comments-container");
|
||||||
if (!idLink) return null;
|
const favoEl = document.querySelector("#a_favo");
|
||||||
|
const infoEl = document.querySelector("#a_info");
|
||||||
|
const idLinkEl = document.querySelector("a.id-link");
|
||||||
|
|
||||||
|
const rawId = commentsEl?.dataset?.itemId || favoEl?.dataset?.itemId || infoEl?.dataset?.itemId || idLinkEl?.dataset?.itemId || idLinkEl?.innerText;
|
||||||
|
if (!rawId) return null;
|
||||||
|
|
||||||
const tagsContainer = document.querySelector("#tags");
|
const tagsContainer = document.querySelector("#tags");
|
||||||
const inner = tagsContainer.querySelector(".tags-inner") || tagsContainer;
|
const inner = tagsContainer ? (tagsContainer.querySelector(".tags-inner") || tagsContainer) : null;
|
||||||
return {
|
return {
|
||||||
postid: +idLink.innerText,
|
postid: /^\d+$/.test(String(rawId).trim()) ? parseInt(rawId, 10) : rawId.trim(),
|
||||||
poster: document.querySelector("a#a_username")?.innerText,
|
poster: document.querySelector("a#a_username")?.innerText,
|
||||||
tags: [...inner.querySelectorAll(".badge")].map(t => t.innerText.slice(0, -2))
|
tags: inner ? [...inner.querySelectorAll(".badge")].map(t => t.innerText.slice(0, -2)) : []
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -410,11 +410,12 @@ if (!window.UserCommentSystem) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
renderComment(c) {
|
renderComment(c) {
|
||||||
|
const itemKey = c.item_slug || c.slug || c.item_id;
|
||||||
const timeAgo = this.timeAgo(c.created_at);
|
const timeAgo = this.timeAgo(c.created_at);
|
||||||
const fullDate = new Date(c.created_at).toISOString();
|
const fullDate = new Date(c.created_at).toISOString();
|
||||||
const content = this.renderCommentContent(c.content, c.item_id);
|
const content = this.renderCommentContent(c.content, itemKey);
|
||||||
|
|
||||||
return `<div class="comment" id="c${c.id}"><div class="comment-avatar"><a href="/${c.item_id}"><img src="/t/${c.item_id}.webp" alt=""></a></div><div class="comment-body"><div class="comment-header"><div class="comment-header-left"><span class="comment-author" tooltip="ID: ${c.user_id}" ${this.userColor ? `style="color: ${this.userColor}"` : ''}>${this.username}</span></div><span class="comment-time timeago" title="${fullDate}">${timeAgo}</span></div><div class="comment-content" data-raw="${this.escapeHtml(c.content)}">${content}</div>${this.renderCommentAttachments(c.files, c.content)}${this.renderCommentPoll(c.poll, c.id)}<div class="comment-footer"><div class="comment-footer-right"><div class="comment-actions">${window.f0ckSession && window.f0ckSession.logged_in ? `<button class="report-comment-btn" data-id="${c.id}" title="Report Comment" style="background:none;border:none;color:inherit;cursor:pointer;opacity:0.75;padding:0;"><svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 512 512" fill="currentColor"><path d="M506.3 417l-213.3-364c-16.3-28-57.5-28-73.8 0l-213.2 364C-10.6 445.1 9.7 480 42.7 480h426.6C502.5 480 522.6 445.1 506.3 417zM256 384c-14.1 0-25.6-11.5-25.6-25.6 0-14.1 11.5-25.6 25.6-25.6 14.1 0 25.6 11.5 25.6 25.6C281.6 372.5 270.1 384 256 384zM281.6 264.4c0 14.1-11.5 25.6-25.6 25.6-14.1 0-25.6-11.5-25.6-25.6v-96c0-14.1 11.5-25.6 25.6-25.6 14.1 0 25.6 11.5 25.6 25.6V264.4z"/></svg></button>` : ''}</div></div></div></div><a href="/${c.item_id}#c${c.id}" class="comment-permalink" title="Permalink">#${c.id}</a></div>`;
|
return `<div class="comment" id="c${c.id}"><div class="comment-avatar"><a href="/${itemKey}"><img src="/t/${c.item_id}.webp" alt=""></a></div><div class="comment-body"><div class="comment-header"><div class="comment-header-left"><span class="comment-author" tooltip="ID: ${c.user_id}" ${this.userColor ? `style="color: ${this.userColor}"` : ''}>${this.username}</span></div><span class="comment-time timeago" title="${fullDate}">${timeAgo}</span></div><div class="comment-content" data-raw="${this.escapeHtml(c.content)}">${content}</div>${this.renderCommentAttachments(c.files, c.content)}${this.renderCommentPoll(c.poll, c.id)}<div class="comment-footer"><div class="comment-footer-right"><div class="comment-actions">${window.f0ckSession && window.f0ckSession.logged_in ? `<button class="report-comment-btn" data-id="${c.id}" title="Report Comment" style="background:none;border:none;color:inherit;cursor:pointer;opacity:0.75;padding:0;"><svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 512 512" fill="currentColor"><path d="M506.3 417l-213.3-364c-16.3-28-57.5-28-73.8 0l-213.2 364C-10.6 445.1 9.7 480 42.7 480h426.6C502.5 480 522.6 445.1 506.3 417zM256 384c-14.1 0-25.6-11.5-25.6-25.6 0-14.1 11.5-25.6 25.6-25.6 14.1 0 25.6 11.5 25.6 25.6C281.6 372.5 270.1 384 256 384zM281.6 264.4c0 14.1-11.5 25.6-25.6 25.6-14.1 0-25.6-11.5-25.6-25.6v-96c0-14.1 11.5-25.6 25.6-25.6 14.1 0 25.6 11.5 25.6 25.6V264.4z"/></svg></button>` : ''}</div></div></div></div><a href="/${itemKey}#c${c.id}" class="comment-permalink" title="Permalink">#${c.id}</a></div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
startLiveTimestamps() {
|
startLiveTimestamps() {
|
||||||
|
|||||||
@@ -38,6 +38,16 @@ export default new class {
|
|||||||
.replace(/'/g, "'");
|
.replace(/'/g, "'");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
generateSlug(length = 11) {
|
||||||
|
const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-';
|
||||||
|
let slug = '';
|
||||||
|
const bytes = crypto.randomBytes(length);
|
||||||
|
for (let i = 0; i < length; i++) {
|
||||||
|
slug += chars[bytes[i] % chars.length];
|
||||||
|
}
|
||||||
|
return slug;
|
||||||
|
}
|
||||||
|
|
||||||
formatSize(size, i = ~~(Math.log(size) / Math.log(1024))) {
|
formatSize(size, i = ~~(Math.log(size) / Math.log(1024))) {
|
||||||
return (size / Math.pow(1024, i)).toFixed(2) * 1 + " " + ["B", "kB", "MB", "GB", "TB"][i];
|
return (size / Math.pow(1024, i)).toFixed(2) * 1 + " " + ["B", "kB", "MB", "GB", "TB"][i];
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -169,6 +169,8 @@
|
|||||||
"favorites_private_hint": "Nur du und Administratoren können deine Favoritenliste sehen.",
|
"favorites_private_hint": "Nur du und Administratoren können deine Favoritenliste sehen.",
|
||||||
"hide_fav_badge": "Favoriten-Badge-Avatar verbergen",
|
"hide_fav_badge": "Favoriten-Badge-Avatar verbergen",
|
||||||
"hide_fav_badge_hint": "Zeigt auf Beitragsseiten stattdessen ein Geist-Icon ohne Profilverlinkung an",
|
"hide_fav_badge_hint": "Zeigt auf Beitragsseiten stattdessen ein Geist-Icon ohne Profilverlinkung an",
|
||||||
|
"default_upload_visibility": "Standard Upload-Sichtbarkeit",
|
||||||
|
"default_upload_visibility_hint": "Lege die Standard-Sichtbarkeit für deine neuen Uploads fest.",
|
||||||
"image_expand_on_click": "Bilder beim Klicken inline erweitern",
|
"image_expand_on_click": "Bilder beim Klicken inline erweitern",
|
||||||
"image_expand_on_click_hint": "Anstatt das Scroll-Zoom-Modal zu öffnen, wird ein Bild beim Klicken innerhalb der Seite auf volle Größe erweitert.",
|
"image_expand_on_click_hint": "Anstatt das Scroll-Zoom-Modal zu öffnen, wird ein Bild beim Klicken innerhalb der Seite auf volle Größe erweitert.",
|
||||||
"enable_bg_blur": "Hintergrundunschärfe aktivieren",
|
"enable_bg_blur": "Hintergrundunschärfe aktivieren",
|
||||||
@@ -799,6 +801,12 @@
|
|||||||
"delete_confirm": "Diesen Einladungstoken löschen?",
|
"delete_confirm": "Diesen Einladungstoken löschen?",
|
||||||
"slot_refreshes_on": "Slot erneuert sich am {date}",
|
"slot_refreshes_on": "Slot erneuert sich am {date}",
|
||||||
"slot_refreshed": "Slot erneuert",
|
"slot_refreshed": "Slot erneuert",
|
||||||
"admin_desc": "Du bist Admin, leg los."
|
"admin_desc": "Du bist Admin, leg los.",
|
||||||
|
"visibility": {
|
||||||
|
"public": "Öffentlich",
|
||||||
|
"unlisted": "Nicht gelistet",
|
||||||
|
"private": "Privat",
|
||||||
|
"change_visibility": "Sichtbarkeit ändern"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -169,6 +169,8 @@
|
|||||||
"favorites_private_hint": "Only you and administrators can view your favorites list.",
|
"favorites_private_hint": "Only you and administrators can view your favorites list.",
|
||||||
"hide_fav_badge": "Hide Favorite Badge Avatar",
|
"hide_fav_badge": "Hide Favorite Badge Avatar",
|
||||||
"hide_fav_badge_hint": "Display as a ghost icon on post detail pages without linking to your profile",
|
"hide_fav_badge_hint": "Display as a ghost icon on post detail pages without linking to your profile",
|
||||||
|
"default_upload_visibility": "Default Upload Visibility",
|
||||||
|
"default_upload_visibility_hint": "Set the default visibility level for your new uploads.",
|
||||||
"image_expand_on_click": "Expand images inline on click",
|
"image_expand_on_click": "Expand images inline on click",
|
||||||
"image_expand_on_click_hint": "Instead of opening the scroll zoom modal, clicking an image will expand it to full size within the page.",
|
"image_expand_on_click_hint": "Instead of opening the scroll zoom modal, clicking an image will expand it to full size within the page.",
|
||||||
"enable_bg_blur": "Enable Background blur",
|
"enable_bg_blur": "Enable Background blur",
|
||||||
@@ -801,6 +803,12 @@
|
|||||||
"delete_confirm": "Delete this invite token?",
|
"delete_confirm": "Delete this invite token?",
|
||||||
"slot_refreshes_on": "slot refreshes on {date}",
|
"slot_refreshes_on": "slot refreshes on {date}",
|
||||||
"slot_refreshed": "slot refreshed",
|
"slot_refreshed": "slot refreshed",
|
||||||
"admin_desc": "You are an admin, go ahead."
|
"admin_desc": "You are an admin, go ahead.",
|
||||||
|
"visibility": {
|
||||||
|
"public": "Public",
|
||||||
|
"unlisted": "Unlisted",
|
||||||
|
"private": "Private",
|
||||||
|
"change_visibility": "Change Visibility"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -302,6 +302,9 @@ 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 isOwnerOrAdmin = (session && user && typeof user === 'string' && session.user && session.user.toLowerCase() === user.toLowerCase()) || (session && (session.admin || session.is_moderator));
|
||||||
|
const visibilityFilter = isOwnerOrAdmin ? db`` : db`and coalesce(items.visibility, 0) = 0`;
|
||||||
|
|
||||||
const cacheKey = buildCountCacheKey({ modequery, tag, user, hall, mime, fav, session, excludedTags, newerThan, minXd, userHallObj, tagger });
|
const cacheKey = buildCountCacheKey({ modequery, tag, user, hall, mime, fav, session, excludedTags, newerThan, minXd, userHallObj, tagger });
|
||||||
let total = getCachedCount(cacheKey);
|
let total = getCachedCount(cacheKey);
|
||||||
|
|
||||||
@@ -313,6 +316,7 @@ export default {
|
|||||||
where
|
where
|
||||||
${db.unsafe(modequery)}
|
${db.unsafe(modequery)}
|
||||||
and items.active = true
|
and items.active = true
|
||||||
|
${visibilityFilter}
|
||||||
${tagFilter}
|
${tagFilter}
|
||||||
${titleFilter}
|
${titleFilter}
|
||||||
${fav ? db`and fav_u.user ilike ${user}` : db``}
|
${fav ? db`and fav_u.user ilike ${user}` : db``}
|
||||||
@@ -355,6 +359,7 @@ export default {
|
|||||||
where
|
where
|
||||||
${db.unsafe(modequery)}
|
${db.unsafe(modequery)}
|
||||||
and items.active = true
|
and items.active = true
|
||||||
|
${visibilityFilter}
|
||||||
${tagFilter}
|
${tagFilter}
|
||||||
${titleFilter}
|
${titleFilter}
|
||||||
${fav ? db`and fav_u.user ilike ${user}` : db``}
|
${fav ? db`and fav_u.user ilike ${user}` : db``}
|
||||||
@@ -385,6 +390,8 @@ export default {
|
|||||||
const rows = (await db`
|
const rows = (await db`
|
||||||
select
|
select
|
||||||
items.id,
|
items.id,
|
||||||
|
items.slug,
|
||||||
|
items.visibility,
|
||||||
items.mime,
|
items.mime,
|
||||||
items.dest,
|
items.dest,
|
||||||
items.username as username,
|
items.username as username,
|
||||||
@@ -521,7 +528,17 @@ export default {
|
|||||||
if (uhData.length) userHallObj = uhData[0];
|
if (uhData.length) userHallObj = uhData[0];
|
||||||
}
|
}
|
||||||
const mime = (rawMime ?? "");
|
const mime = (rawMime ?? "");
|
||||||
const itemid = rawItemid ? +rawItemid : null;
|
const rawIdOrSlug = rawItemid ?? null;
|
||||||
|
if (rawIdOrSlug === null || rawIdOrSlug === undefined || rawIdOrSlug === '') {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: "404 - upload not found"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const isNumeric = /^\d+$/.test(String(rawIdOrSlug));
|
||||||
|
const itemLookup = isNumeric ? db`items.id = ${+rawIdOrSlug}` : db`items.slug = ${String(rawIdOrSlug)}`;
|
||||||
|
|
||||||
const mimeParts = (mime || "").split(',').filter(m => ['video', 'audio', 'image', 'flash', 'pdf'].includes(m));
|
const mimeParts = (mime || "").split(',').filter(m => ['video', 'audio', 'image', 'flash', 'pdf'].includes(m));
|
||||||
const mimeSQL = mimeParts.length > 0
|
const mimeSQL = mimeParts.length > 0
|
||||||
? db`and (${mimeParts.map(m => m === 'flash'
|
? db`and (${mimeParts.map(m => m === 'flash'
|
||||||
@@ -535,19 +552,12 @@ 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, mime, itemid, strict: strict, userHall: userHallObj || userHallSlug, userHallOwner };
|
const tmp = { user, tag: isTitleSearch ? _decodedTag : tag, hall, mime, itemid: rawIdOrSlug, strict: strict, userHall: userHallObj || userHallSlug, userHallOwner };
|
||||||
|
|
||||||
const effMode = Number(mode ?? 0);
|
const effMode = Number(mode ?? 0);
|
||||||
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 modequery = multiRatingSQL ?? lib.getMode(effMode);
|
const modequery = multiRatingSQL ?? lib.getMode(effMode);
|
||||||
|
|
||||||
if (itemid === null) {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
message: "404 - upload not found"
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
let tagFilter = db``;
|
let tagFilter = db``;
|
||||||
let titleFilter = db``;
|
let titleFilter = db``;
|
||||||
if (isTitleSearch && titleQuery) {
|
if (isTitleSearch && titleQuery) {
|
||||||
@@ -588,6 +598,7 @@ export default {
|
|||||||
return db`
|
return db`
|
||||||
${db.unsafe(modequery)}
|
${db.unsafe(modequery)}
|
||||||
and items.active = true
|
and items.active = true
|
||||||
|
and coalesce(items.visibility, 0) = 0
|
||||||
${tagFilter}
|
${tagFilter}
|
||||||
${titleFilter}
|
${titleFilter}
|
||||||
${hallFilter}
|
${hallFilter}
|
||||||
@@ -601,11 +612,9 @@ export default {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const startTime = Date.now();
|
const startTime = Date.now();
|
||||||
console.log(`[${new Date().toISOString()}] [GETF0CK_OPT] Starting fetch for itemid=${itemid}`);
|
console.log(`[${new Date().toISOString()}] [GETF0CK_OPT] Starting fetch for rawIdOrSlug=${rawIdOrSlug}`);
|
||||||
|
|
||||||
// 1. Fetch the main item
|
// 1. Fetch the main item
|
||||||
// We only apply the active check and global NSFW filter (for guests) here.
|
|
||||||
// We skip the 'mode' preference filter so that switching modes on an item view doesn't result in a 404 (post not visible).
|
|
||||||
const items = await db`
|
const items = await db`
|
||||||
select distinct on (items.id)
|
select distinct on (items.id)
|
||||||
items.*,
|
items.*,
|
||||||
@@ -629,7 +638,7 @@ export default {
|
|||||||
left join "user_options" uo on uo.user_id = author_u.id
|
left join "user_options" uo on uo.user_id = author_u.id
|
||||||
${user_id ? db`left join user_video_views uvv on uvv.video_id = items.id and uvv.user_id = ${user_id}` : db``}
|
${user_id ? db`left join user_video_views uvv on uvv.video_id = items.id and uvv.user_id = ${user_id}` : db``}
|
||||||
where
|
where
|
||||||
items.id = ${itemid} and
|
${itemLookup} and
|
||||||
items.active = true
|
items.active = true
|
||||||
${!session && getGlobalfilter() ? db`and not exists (select 1 from tags_assign where item_id = items.id and (${db.unsafe(getGlobalfilter())}))` : db``}
|
${!session && getGlobalfilter() ? db`and not exists (select 1 from tags_assign where item_id = items.id and (${db.unsafe(getGlobalfilter())}))` : db``}
|
||||||
limit 1
|
limit 1
|
||||||
@@ -637,7 +646,41 @@ export default {
|
|||||||
|
|
||||||
const actitem = items[0];
|
const actitem = items[0];
|
||||||
|
|
||||||
if (actitem && user_id) {
|
if (!actitem) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: "404 - upload not found"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const itemid = actitem.id;
|
||||||
|
|
||||||
|
// Check visibility permissions:
|
||||||
|
const isOwnerOrAdmin = session && (
|
||||||
|
(session.user && session.user.toLowerCase() === (actitem.username || '').toLowerCase()) ||
|
||||||
|
session.admin || session.is_moderator
|
||||||
|
);
|
||||||
|
|
||||||
|
// If request was by sequential numeric ID (/123) and item visibility > 0 (unlisted/private):
|
||||||
|
// Block numeric enumeration unless viewer is owner/admin
|
||||||
|
if (isNumeric && actitem.visibility > 0 && !isOwnerOrAdmin) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: "404 - upload not found"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// If item is Private (visibility === 2):
|
||||||
|
// Direct link only allowed for owner/admin
|
||||||
|
if (actitem.visibility === 2 && !isOwnerOrAdmin) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
is_private: true,
|
||||||
|
message: "403 - private upload"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user_id) {
|
||||||
db`
|
db`
|
||||||
insert into user_video_views (user_id, video_id, view_count, last_viewed)
|
insert into user_video_views (user_id, video_id, view_count, last_viewed)
|
||||||
values (${user_id}, ${itemid}, 1, now())
|
values (${user_id}, ${itemid}, 1, now())
|
||||||
@@ -646,34 +689,26 @@ export default {
|
|||||||
last_viewed = now()
|
last_viewed = now()
|
||||||
`.catch(e => console.error('Failed to track view:', e));
|
`.catch(e => console.error('Failed to track view:', e));
|
||||||
}
|
}
|
||||||
|
// Guest global filter check if item was filtered out
|
||||||
if (!actitem) {
|
if (!session && getGlobalfilter() && !actitem) {
|
||||||
// Item not found or filtered out - check if it exists but was filtered (for OG meta tags)
|
|
||||||
if (!session && getGlobalfilter()) {
|
|
||||||
const unfilteredItem = await db`
|
const unfilteredItem = await db`
|
||||||
select id from items where id = ${itemid} and active = true limit 1
|
select id from items where ${itemLookup} and active = true limit 1
|
||||||
`;
|
`;
|
||||||
if (unfilteredItem[0]) {
|
if (unfilteredItem[0]) {
|
||||||
// Item exists but was filtered - return minimal data for OG tags with blurred thumbnail
|
|
||||||
const hallSlug = hall && typeof hall === 'object' ? hall.slug : hall;
|
const hallSlug = hall && typeof hall === 'object' ? hall.slug : hall;
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
message: "Sorry, this post is currently not visible.",
|
message: "Sorry, this post is currently not visible.",
|
||||||
item: {
|
item: {
|
||||||
id: itemid,
|
id: unfilteredItem[0].id,
|
||||||
og_thumbnail: `${cfg.websrv.paths.thumbnails}/${itemid}_blur.webp`,
|
og_thumbnail: `${cfg.websrv.paths.thumbnails}/${unfilteredItem[0].id}_blur.webp`,
|
||||||
og_url: hallSlug
|
og_url: hallSlug
|
||||||
? `https://${cfg.main.url.domain}/h/${encodeURIComponent(hallSlug)}/${itemid}`
|
? `https://${cfg.main.url.domain}/h/${encodeURIComponent(hallSlug)}/${unfilteredItem[0].id}`
|
||||||
: `https://${cfg.main.url.domain}/${itemid}`,
|
: `https://${cfg.main.url.domain}/${unfilteredItem[0].id}`,
|
||||||
og_description: `Content not visible in current mode`
|
og_description: `Content not visible in current mode`
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
message: "Sorry, this post is currently not visible."
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Fetch Next/Prev/Start/End/Cheat in parallel
|
// 2. Fetch Next/Prev/Start/End/Cheat in parallel
|
||||||
@@ -685,7 +720,7 @@ export default {
|
|||||||
|
|
||||||
const baseQuery = (whereClause, orderBy, limit = 1) => {
|
const baseQuery = (whereClause, orderBy, limit = 1) => {
|
||||||
return db`
|
return db`
|
||||||
select items.id
|
select items.id, items.slug
|
||||||
from items
|
from items
|
||||||
left join tags_assign on tags_assign.item_id = items.id
|
left join tags_assign on tags_assign.item_id = items.id
|
||||||
left join tags on tags.id = tags_assign.tag_id
|
left join tags on tags.id = tags_assign.tag_id
|
||||||
@@ -696,7 +731,7 @@ export default {
|
|||||||
where
|
where
|
||||||
${buildConditions()}
|
${buildConditions()}
|
||||||
${whereClause}
|
${whereClause}
|
||||||
group by items.id
|
group by items.id, items.slug
|
||||||
${orderBy}
|
${orderBy}
|
||||||
limit ${limit}
|
limit ${limit}
|
||||||
`;
|
`;
|
||||||
@@ -711,7 +746,7 @@ export default {
|
|||||||
const checkFilter = !session && nsfpIds.length > 0;
|
const checkFilter = !session && nsfpIds.length > 0;
|
||||||
|
|
||||||
const query = db`
|
const query = db`
|
||||||
SELECT ta.item_id as id
|
SELECT ta.item_id as id, items.slug
|
||||||
FROM tags_assign ta
|
FROM tags_assign ta
|
||||||
INNER JOIN items ON items.id = ta.item_id
|
INNER JOIN items ON items.id = ta.item_id
|
||||||
${checkFilter
|
${checkFilter
|
||||||
@@ -720,6 +755,7 @@ export default {
|
|||||||
}
|
}
|
||||||
WHERE ${useTagIdOpt ? db`ta.tag_id = ${tagId}` : db`${db.unsafe(modequery)}`}
|
WHERE ${useTagIdOpt ? db`ta.tag_id = ${tagId}` : db`${db.unsafe(modequery)}`}
|
||||||
AND items.active = true
|
AND items.active = true
|
||||||
|
AND coalesce(items.visibility, 0) = 0
|
||||||
${mimeSQL}
|
${mimeSQL}
|
||||||
${checkFilter ? db`AND filter_ta.tag_id IS NULL` : db``}
|
${checkFilter ? db`AND filter_ta.tag_id IS NULL` : db``}
|
||||||
${excludedTags.length > 0 ? db`and not exists (select 1 from tags_assign where item_id = ta.item_id and tag_id = any(${excludedTags}::int[]))` : db``}
|
${excludedTags.length > 0 ? db`and not exists (select 1 from tags_assign where item_id = ta.item_id and tag_id = any(${excludedTags}::int[]))` : db``}
|
||||||
@@ -781,24 +817,24 @@ export default {
|
|||||||
if (actitem.checksum && actitem.checksum.includes('_bypass_')) {
|
if (actitem.checksum && actitem.checksum.includes('_bypass_')) {
|
||||||
const baseChecksum = actitem.checksum.split('_bypass_')[0];
|
const baseChecksum = actitem.checksum.split('_bypass_')[0];
|
||||||
const repostRows = await db`
|
const repostRows = await db`
|
||||||
SELECT id, username, stamp FROM items
|
SELECT id, slug, username, stamp FROM items
|
||||||
WHERE active = true
|
WHERE active = true
|
||||||
AND id != ${itemid}
|
AND id != ${itemid}
|
||||||
AND (checksum = ${baseChecksum} OR checksum LIKE ${baseChecksum + '_bypass_%'})
|
AND (checksum = ${baseChecksum} OR checksum LIKE ${baseChecksum + '_bypass_%'})
|
||||||
ORDER BY id ASC
|
ORDER BY id ASC
|
||||||
`;
|
`;
|
||||||
repostItems = repostRows.map(r => ({ id: r.id, username: r.username, stamp: r.stamp, match_type: 'checksum' }));
|
repostItems = repostRows.map(r => ({ id: r.id, slug: r.slug, username: r.username, stamp: r.stamp, match_type: 'checksum' }));
|
||||||
} else if (actitem.checksum) {
|
} else if (actitem.checksum) {
|
||||||
// Even without bypass, check if other bypass-entries exist with this same hash
|
// Even without bypass, check if other bypass-entries exist with this same hash
|
||||||
const baseChecksum = actitem.checksum;
|
const baseChecksum = actitem.checksum;
|
||||||
const repostRows = await db`
|
const repostRows = await db`
|
||||||
SELECT id, username, stamp FROM items
|
SELECT id, slug, username, stamp FROM items
|
||||||
WHERE active = true
|
WHERE active = true
|
||||||
AND id != ${itemid}
|
AND id != ${itemid}
|
||||||
AND checksum LIKE ${baseChecksum + '_bypass_%'}
|
AND checksum LIKE ${baseChecksum + '_bypass_%'}
|
||||||
ORDER BY id ASC
|
ORDER BY id ASC
|
||||||
`;
|
`;
|
||||||
repostItems = repostRows.map(r => ({ id: r.id, username: r.username, stamp: r.stamp, match_type: 'checksum' }));
|
repostItems = repostRows.map(r => ({ id: r.id, slug: r.slug, username: r.username, stamp: r.stamp, match_type: 'checksum' }));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Also find visually-similar items via phash, merging with checksum results
|
// Also find visually-similar items via phash, merging with checksum results
|
||||||
@@ -808,7 +844,7 @@ export default {
|
|||||||
const existingIds = new Set(repostItems.map(r => r.id));
|
const existingIds = new Set(repostItems.map(r => r.id));
|
||||||
for (const pm of phashMatches) {
|
for (const pm of phashMatches) {
|
||||||
if (!existingIds.has(pm.id)) {
|
if (!existingIds.has(pm.id)) {
|
||||||
repostItems.push({ id: pm.id, username: pm.username, stamp: pm.stamp, match_type: 'phash' });
|
repostItems.push({ id: pm.id, slug: pm.slug, username: pm.username, stamp: pm.stamp, match_type: 'phash' });
|
||||||
existingIds.add(pm.id);
|
existingIds.add(pm.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -843,7 +879,7 @@ export default {
|
|||||||
else if (userMode === 4 && (!cfg.enable_nsfl || !isNsfl)) modeBlocked = true; // NSFL mode, item is not NSFL
|
else if (userMode === 4 && (!cfg.enable_nsfl || !isNsfl)) modeBlocked = true; // NSFL mode, item is not NSFL
|
||||||
else if (userMode === 2 && isTagged) modeBlocked = true; // Untagged mode, item has tags
|
else if (userMode === 2 && isTagged) modeBlocked = true; // Untagged mode, item has tags
|
||||||
|
|
||||||
if (modeBlocked) {
|
if (modeBlocked && !isOwnerOrAdmin && actitem.visibility !== 1) {
|
||||||
const hallSlug = hall && typeof hall === 'object' ? hall.slug : hall;
|
const hallSlug = hall && typeof hall === 'object' ? hall.slug : hall;
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
@@ -871,6 +907,8 @@ export default {
|
|||||||
},
|
},
|
||||||
item: {
|
item: {
|
||||||
id: actitem.id,
|
id: actitem.id,
|
||||||
|
slug: actitem.slug || null,
|
||||||
|
visibility: actitem.visibility !== undefined ? actitem.visibility : 0,
|
||||||
username: actitem.username,
|
username: actitem.username,
|
||||||
author_id: actitem.author_id,
|
author_id: actitem.author_id,
|
||||||
author_color: actitem.author_color,
|
author_color: actitem.author_color,
|
||||||
@@ -933,13 +971,13 @@ export default {
|
|||||||
height: actitem.height || null,
|
height: actitem.height || null,
|
||||||
original_filename: actitem.original_filename || null
|
original_filename: actitem.original_filename || null
|
||||||
},
|
},
|
||||||
title: `${actitem.id} - ${cfg.websrv.domain}`,
|
title: `${(cfg.enable_item_slugs !== false && actitem.slug) ? actitem.slug : actitem.id} - ${cfg.websrv.domain}`,
|
||||||
pagination: {
|
pagination: {
|
||||||
end: endItem[0]?.id || itemid,
|
end: (cfg.enable_item_slugs !== false && endItem[0]?.slug) ? endItem[0].slug : (endItem[0]?.id || itemid),
|
||||||
start: startItem[0]?.id || itemid,
|
start: (cfg.enable_item_slugs !== false && startItem[0]?.slug) ? startItem[0].slug : (startItem[0]?.id || itemid),
|
||||||
next: nextItem[0]?.id || null,
|
next: (cfg.enable_item_slugs !== false && nextItem[0]?.slug) ? nextItem[0].slug : (nextItem[0]?.id || null),
|
||||||
prev: prevItem[0]?.id || null,
|
prev: (cfg.enable_item_slugs !== false && prevItem[0]?.slug) ? prevItem[0].slug : (prevItem[0]?.id || null),
|
||||||
page: actitem.id,
|
page: (cfg.enable_item_slugs !== false && actitem.slug) ? actitem.slug : actitem.id,
|
||||||
cheat: cheat
|
cheat: cheat
|
||||||
},
|
},
|
||||||
phrase: cfg.websrv.phrases[~~(Math.random() * cfg.websrv.phrases.length)],
|
phrase: cfg.websrv.phrases[~~(Math.random() * cfg.websrv.phrases.length)],
|
||||||
@@ -1008,6 +1046,7 @@ export default {
|
|||||||
WHERE
|
WHERE
|
||||||
${db.unsafe(modequery)}
|
${db.unsafe(modequery)}
|
||||||
AND items.active = true
|
AND items.active = true
|
||||||
|
AND coalesce(items.visibility, 0) = 0
|
||||||
AND items.title ILIKE ${'%' + titleQuery + '%'}
|
AND items.title ILIKE ${'%' + titleQuery + '%'}
|
||||||
AND items.title IS NOT NULL
|
AND items.title IS NOT NULL
|
||||||
${mimeSQL}
|
${mimeSQL}
|
||||||
@@ -1030,6 +1069,7 @@ export default {
|
|||||||
${db.unsafe(modequery)}
|
${db.unsafe(modequery)}
|
||||||
and "user".user ilike ${user}
|
and "user".user ilike ${user}
|
||||||
and items.active = true
|
and items.active = true
|
||||||
|
and coalesce(items.visibility, 0) = 0
|
||||||
${mimeSQL}
|
${mimeSQL}
|
||||||
${!session && getGlobalfilter() ? db`and not exists (select 1 from tags_assign where item_id = items.id and (${db.unsafe(getGlobalfilter())}))` : db``}
|
${!session && getGlobalfilter() ? db`and not exists (select 1 from tags_assign where item_id = items.id and (${db.unsafe(getGlobalfilter())}))` : db``}
|
||||||
group by items.id
|
group by items.id
|
||||||
@@ -1069,6 +1109,7 @@ export default {
|
|||||||
where
|
where
|
||||||
${db.unsafe(modequery)}
|
${db.unsafe(modequery)}
|
||||||
and items.active = true
|
and items.active = true
|
||||||
|
and coalesce(items.visibility, 0) = 0
|
||||||
${tagFilter}
|
${tagFilter}
|
||||||
${user ? db`and items.username ilike ${user}` : db``}
|
${user ? db`and items.username ilike ${user}` : db``}
|
||||||
${hall ? db`and items.id in (select item_id from halls_assign ha join halls h on h.id = ha.hall_id where h.slug = ${hall})` : db``}
|
${hall ? db`and items.id in (select item_id from halls_assign ha join halls h on h.id = ha.hall_id where h.slug = ${hall})` : db``}
|
||||||
@@ -1091,6 +1132,7 @@ export default {
|
|||||||
${db.unsafe(modequery)}
|
${db.unsafe(modequery)}
|
||||||
and h.slug = ${hall}
|
and h.slug = ${hall}
|
||||||
and items.active = true
|
and items.active = true
|
||||||
|
and coalesce(items.visibility, 0) = 0
|
||||||
${mimeSQL}
|
${mimeSQL}
|
||||||
${!session && getGlobalfilter() ? db`and not exists (select 1 from tags_assign where item_id = items.id and (${db.unsafe(getGlobalfilter())}))` : db``}
|
${!session && getGlobalfilter() ? db`and not exists (select 1 from tags_assign where item_id = items.id and (${db.unsafe(getGlobalfilter())}))` : db``}
|
||||||
${excludedTags.length > 0 ? db`and not exists (select 1 from tags_assign where item_id = items.id and tag_id = any(${excludedTags}::int[]))` : db``}
|
${excludedTags.length > 0 ? db`and not exists (select 1 from tags_assign where item_id = items.id and tag_id = any(${excludedTags}::int[]))` : db``}
|
||||||
@@ -1107,6 +1149,7 @@ export default {
|
|||||||
${db.unsafe(modequery)}
|
${db.unsafe(modequery)}
|
||||||
and uha.hall_id = ${userHallId}
|
and uha.hall_id = ${userHallId}
|
||||||
and items.active = true
|
and items.active = true
|
||||||
|
and coalesce(items.visibility, 0) = 0
|
||||||
${mimeSQL}
|
${mimeSQL}
|
||||||
${excludedTags.length > 0 ? db`and not exists (select 1 from tags_assign where item_id = items.id and tag_id = any(${excludedTags}::int[]))` : db``}
|
${excludedTags.length > 0 ? db`and not exists (select 1 from tags_assign where item_id = items.id and tag_id = any(${excludedTags}::int[]))` : db``}
|
||||||
order by random()
|
order by random()
|
||||||
@@ -1133,6 +1176,7 @@ export default {
|
|||||||
${useTagIdOpt ? db`INNER JOIN tags_assign ta ON ta.item_id = items.id AND ta.tag_id = ${tagId}` : db``}
|
${useTagIdOpt ? db`INNER JOIN tags_assign ta ON ta.item_id = items.id AND ta.tag_id = ${tagId}` : db``}
|
||||||
${checkFilter ? db`LEFT JOIN tags_assign filter_ta ON filter_ta.item_id = items.id AND filter_ta.tag_id IN ${db(nsfpIds)}` : db``}
|
${checkFilter ? db`LEFT JOIN tags_assign filter_ta ON filter_ta.item_id = items.id AND filter_ta.tag_id IN ${db(nsfpIds)}` : db``}
|
||||||
WHERE items.active = true
|
WHERE items.active = true
|
||||||
|
AND coalesce(items.visibility, 0) = 0
|
||||||
${mimeSQL}
|
${mimeSQL}
|
||||||
${checkFilter ? db`AND filter_ta.tag_id IS NULL` : db``}
|
${checkFilter ? db`AND filter_ta.tag_id IS NULL` : db``}
|
||||||
${excludedTags.length > 0 ? db`AND NOT EXISTS (SELECT 1 FROM tags_assign WHERE item_id = items.id AND tag_id = ANY(${excludedTags}::int[]))` : db``}
|
${excludedTags.length > 0 ? db`AND NOT EXISTS (SELECT 1 FROM tags_assign WHERE item_id = items.id AND tag_id = ANY(${excludedTags}::int[]))` : db``}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import cfg from "../config.mjs";
|
|||||||
import { createI18n } from "../i18n.mjs";
|
import { createI18n } from "../i18n.mjs";
|
||||||
|
|
||||||
export default (router, tpl) => {
|
export default (router, tpl) => {
|
||||||
router.get(/\/ajax\/item\/(?<itemid>\d+)/, async (req, res) => {
|
router.get(/\/ajax\/item\/(?<itemid>[a-zA-Z0-9_-]{11}|\d+)/, async (req, res) => {
|
||||||
const tAjaxStart = Date.now();
|
const tAjaxStart = Date.now();
|
||||||
let query = {};
|
let query = {};
|
||||||
if (typeof req.url === 'string') {
|
if (typeof req.url === 'string') {
|
||||||
@@ -35,12 +35,12 @@ export default (router, tpl) => {
|
|||||||
const ratingsRaw = req.cookies.ratings;
|
const ratingsRaw = req.cookies.ratings;
|
||||||
const ratingsArr = ratingsRaw ? decodeURIComponent(ratingsRaw).split(/[|,]/).filter(r => ['sfw','nsfw','nsfl','untagged'].includes(r)) : null;
|
const ratingsArr = ratingsRaw ? decodeURIComponent(ratingsRaw).split(/[|,]/).filter(r => ['sfw','nsfw','nsfl','untagged'].includes(r)) : null;
|
||||||
|
|
||||||
const itemid = req.params.itemid || req.url.pathname.match(/\/ajax\/item\/(\d+)/)?.[1];
|
const itemid = req.params.itemid || req.url.pathname.match(/\/ajax\/item\/([a-zA-Z0-9_-]{11}|\d+)/)?.[1];
|
||||||
const data = await f0cklib.getf0ck({
|
const data = await f0cklib.getf0ck({
|
||||||
itemid: itemid,
|
itemid: itemid,
|
||||||
mode: query.mode !== undefined ? +query.mode : req.mode,
|
mode: query.mode !== undefined ? +query.mode : req.mode,
|
||||||
ratings: ratingsArr,
|
ratings: ratingsArr,
|
||||||
session: !!req.session,
|
session: req.session,
|
||||||
url: contextUrl,
|
url: contextUrl,
|
||||||
user: query.user,
|
user: query.user,
|
||||||
tag: query.tag,
|
tag: query.tag,
|
||||||
@@ -170,7 +170,8 @@ export default (router, tpl) => {
|
|||||||
html: itemHtml,
|
html: itemHtml,
|
||||||
pagination: paginationHtml,
|
pagination: paginationHtml,
|
||||||
title: data.title,
|
title: data.title,
|
||||||
id: itemid
|
id: itemid,
|
||||||
|
slug: data.item?.slug || null
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -594,10 +594,12 @@ export default router => {
|
|||||||
ratings: ratingsArr && ratingsArr.length > 0 ? ratingsArr : null,
|
ratings: ratingsArr && ratingsArr.length > 0 ? ratingsArr : null,
|
||||||
strict: isStrict,
|
strict: isStrict,
|
||||||
session: !!req.session,
|
session: !!req.session,
|
||||||
exclude: req.session?.excluded_tags || []
|
exclude: req.session?.excluded_tags || [],
|
||||||
|
user_id: req.session?.id,
|
||||||
|
is_admin: req.session?.admin
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!data.itemid) {
|
if (!data || !data.itemid) {
|
||||||
return res.json({
|
return res.json({
|
||||||
success: false,
|
success: false,
|
||||||
items: []
|
items: []
|
||||||
@@ -636,6 +638,7 @@ export default router => {
|
|||||||
items: {
|
items: {
|
||||||
...safeItem,
|
...safeItem,
|
||||||
id: item.id,
|
id: item.id,
|
||||||
|
slug: item.slug || null,
|
||||||
dest: relativeDest,
|
dest: relativeDest,
|
||||||
url: directUrl,
|
url: directUrl,
|
||||||
direct_url: directUrl
|
direct_url: directUrl
|
||||||
@@ -1038,7 +1041,24 @@ export default router => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
group.post(/\/togglefav$/, lib.loggedin, async (req, res) => {
|
group.post(/\/togglefav$/, lib.loggedin, async (req, res) => {
|
||||||
const postid = +req.post.postid;
|
const rawPostid = req.post?.postid ?? req.body?.postid ?? req.url?.qs?.postid;
|
||||||
|
if (rawPostid === undefined || rawPostid === null) {
|
||||||
|
return res.json({ success: false, msg: 'Missing postid' }, 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Support both numeric item ID and string slug
|
||||||
|
const isNumeric = /^\d+$/.test(String(rawPostid));
|
||||||
|
const itemRow = await db`
|
||||||
|
SELECT id FROM items
|
||||||
|
WHERE ${isNumeric ? db`id = ${+rawPostid}` : db`slug = ${String(rawPostid)}`} AND active = true AND is_deleted = false
|
||||||
|
LIMIT 1
|
||||||
|
`;
|
||||||
|
|
||||||
|
if (!itemRow.length) {
|
||||||
|
return res.json({ success: false, msg: 'Item not found' }, 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
const postid = itemRow[0].id;
|
||||||
|
|
||||||
// Check if already faved by this user — compare as numbers to avoid type mismatch
|
// Check if already faved by this user — compare as numbers to avoid type mismatch
|
||||||
const existing = await db`
|
const existing = await db`
|
||||||
@@ -1168,6 +1188,47 @@ export default router => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
group.post(/\/item\/visibility$/, lib.loggedin, async (req, res) => {
|
||||||
|
if (cfg.enable_private_uploads === false) {
|
||||||
|
return res.json({ success: false, msg: 'Private uploads feature disabled' }, 403);
|
||||||
|
}
|
||||||
|
const postid = req.post?.postid || req.post?.id || req.body?.postid || req.body?.id;
|
||||||
|
const visibility = parseInt(req.post?.visibility ?? req.body?.visibility, 10);
|
||||||
|
if (!postid || isNaN(visibility) || ![0, 1, 2].includes(visibility)) {
|
||||||
|
return res.json({ success: false, msg: 'Invalid parameters' }, 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
const isNumeric = /^\d+$/.test(String(postid));
|
||||||
|
const item = await db`
|
||||||
|
SELECT id, slug, username, visibility
|
||||||
|
FROM items
|
||||||
|
WHERE ${isNumeric ? db`id = ${+postid}` : db`slug = ${String(postid)}`} AND active = true AND is_deleted = false
|
||||||
|
LIMIT 1
|
||||||
|
`;
|
||||||
|
|
||||||
|
if (item.length === 0) {
|
||||||
|
return res.json({ success: false, msg: 'Item not found' }, 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
const isOwner = item[0].username === req.session.user;
|
||||||
|
const isAdmin = req.session.admin || req.session.is_moderator;
|
||||||
|
|
||||||
|
if (!isOwner && !isAdmin) {
|
||||||
|
return res.json({ success: false, msg: 'Unauthorized' }, 403);
|
||||||
|
}
|
||||||
|
|
||||||
|
await db`UPDATE items SET visibility = ${visibility} WHERE id = ${item[0].id}`;
|
||||||
|
|
||||||
|
f0cklib.clearCountCache();
|
||||||
|
|
||||||
|
return res.json({
|
||||||
|
success: true,
|
||||||
|
itemid: item[0].id,
|
||||||
|
slug: item[0].slug,
|
||||||
|
visibility: visibility
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
group.post(/\/item\/(?<id>[0-9]+)\/rating$/, lib.loggedin, async (req, res) => {
|
group.post(/\/item\/(?<id>[0-9]+)\/rating$/, lib.loggedin, async (req, res) => {
|
||||||
const itemid = +req.params.id;
|
const itemid = +req.params.id;
|
||||||
if (!itemid) return res.json({ success: false, msg: 'No itemid provided' }, 400);
|
if (!itemid) return res.json({ success: false, msg: 'No itemid provided' }, 400);
|
||||||
|
|||||||
@@ -353,6 +353,30 @@ export default router => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Update Default Upload Visibility preference
|
||||||
|
group.put(/\/default_upload_visibility/, lib.loggedin, async (req, res) => {
|
||||||
|
if (cfg.allow_user_upload_visibility === false || cfg.websrv?.allow_user_upload_visibility === false) {
|
||||||
|
return res.json({ success: false, msg: 'Custom upload visibility is disabled by the administrator' }, 403);
|
||||||
|
}
|
||||||
|
const vis = parseInt(req.post.default_upload_visibility, 10);
|
||||||
|
if (isNaN(vis) || ![0, 1, 2].includes(vis)) {
|
||||||
|
return res.json({ success: false, msg: 'Invalid visibility option' }, 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await db`
|
||||||
|
update user_options
|
||||||
|
set default_upload_visibility = ${vis}
|
||||||
|
where user_id = ${+req.session.id}
|
||||||
|
`;
|
||||||
|
if (req.session) req.session.default_upload_visibility = vis;
|
||||||
|
return res.json({ success: true, default_upload_visibility: vis }, 200);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Update Default Upload Visibility pref error:', e);
|
||||||
|
return res.json({ success: false, msg: 'Error updating preference' }, 500);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Update Username Color preference
|
// Update Username Color preference
|
||||||
group.put(/\/username_color/, lib.loggedin, async (req, res) => {
|
group.put(/\/username_color/, lib.loggedin, async (req, res) => {
|
||||||
const { color } = req.post;
|
const { color } = req.post;
|
||||||
|
|||||||
@@ -137,6 +137,29 @@ const parseMultipart = (buffer, boundary) => {
|
|||||||
|
|
||||||
import { getManualApproval, getMinTags, getBypassDuplicateCheck } from "../../settings.mjs";
|
import { getManualApproval, getMinTags, getBypassDuplicateCheck } from "../../settings.mjs";
|
||||||
|
|
||||||
|
const getTargetVisibility = (req, postVis) => {
|
||||||
|
if (cfg.enable_private_uploads === false) return 0;
|
||||||
|
|
||||||
|
const sysDefault = (typeof cfg.default_upload_visibility === 'number')
|
||||||
|
? cfg.default_upload_visibility
|
||||||
|
: (typeof cfg.websrv?.default_upload_visibility === 'number' ? cfg.websrv.default_upload_visibility : 0);
|
||||||
|
|
||||||
|
const allowUserOverride = cfg.allow_user_upload_visibility !== false && cfg.websrv?.allow_user_upload_visibility !== false;
|
||||||
|
|
||||||
|
if (!allowUserOverride) {
|
||||||
|
return sysDefault;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rawHeader = req.headers ? req.headers['x-upload-visibility'] : null;
|
||||||
|
const val = (rawHeader || postVis || '').toString().trim().toLowerCase();
|
||||||
|
if (val === 'private' || val === '2') return 2;
|
||||||
|
if (val === 'unlisted' || val === '1') return 1;
|
||||||
|
if (val === 'public' || val === '0') return 0;
|
||||||
|
return (req.session?.default_upload_visibility !== undefined && req.session?.default_upload_visibility !== null)
|
||||||
|
? req.session.default_upload_visibility
|
||||||
|
: sysDefault;
|
||||||
|
};
|
||||||
|
|
||||||
// Collect request body as buffer with debug logging
|
// Collect request body as buffer with debug logging
|
||||||
const collectBody = (req) => {
|
const collectBody = (req) => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
@@ -369,6 +392,9 @@ export default router => {
|
|||||||
// Store as a YouTube embed: dest = yt:VIDEO_ID, mime = video/youtube
|
// Store as a YouTube embed: dest = yt:VIDEO_ID, mime = video/youtube
|
||||||
const filename = `yt:${videoId}`;
|
const filename = `yt:${videoId}`;
|
||||||
|
|
||||||
|
const targetVisibility = getTargetVisibility(req, req.post?.visibility);
|
||||||
|
const itemSlug = (cfg.enable_item_slugs !== false) ? lib.generateSlug(11) : null;
|
||||||
|
|
||||||
const [{ id: itemid }] = await db`
|
const [{ id: itemid }] = await db`
|
||||||
insert into items ${db({
|
insert into items ${db({
|
||||||
src: ytUrl,
|
src: ytUrl,
|
||||||
@@ -383,8 +409,10 @@ export default router => {
|
|||||||
stamp: ~~(Date.now() / 1000),
|
stamp: ~~(Date.now() / 1000),
|
||||||
active: !isApprovalRequired,
|
active: !isApprovalRequired,
|
||||||
is_oc: !!is_oc,
|
is_oc: !!is_oc,
|
||||||
title: title
|
title: title,
|
||||||
}, 'src', 'dest', 'mime', 'size', 'checksum', 'phash', 'username', 'userchannel', 'usernetwork', 'stamp', 'active', 'is_oc', 'title')}
|
visibility: targetVisibility,
|
||||||
|
slug: itemSlug
|
||||||
|
}, 'src', 'dest', 'mime', 'size', 'checksum', 'phash', 'username', 'userchannel', 'usernetwork', 'stamp', 'active', 'is_oc', 'title', 'visibility', 'slug')}
|
||||||
RETURNING id
|
RETURNING id
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@@ -437,6 +465,9 @@ export default router => {
|
|||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// ===== REGULAR URL DOWNLOAD (Asynchronous) =====
|
// ===== REGULAR URL DOWNLOAD (Asynchronous) =====
|
||||||
|
const targetVisibility = getTargetVisibility(req, req.post?.visibility);
|
||||||
|
const itemSlug = (cfg.enable_item_slugs !== false) ? lib.generateSlug(11) : null;
|
||||||
|
|
||||||
const session = {
|
const session = {
|
||||||
id: req.session.id,
|
id: req.session.id,
|
||||||
user: req.session.user,
|
user: req.session.user,
|
||||||
@@ -689,8 +720,10 @@ export default router => {
|
|||||||
stamp: ~~(Date.now() / 1000),
|
stamp: ~~(Date.now() / 1000),
|
||||||
active: !isApprovalRequired,
|
active: !isApprovalRequired,
|
||||||
is_oc: !!is_oc,
|
is_oc: !!is_oc,
|
||||||
title: title
|
title: title,
|
||||||
}, 'src', 'dest', 'mime', 'size', 'checksum', 'phash', 'username', 'userchannel', 'usernetwork', 'stamp', 'active', 'is_oc', 'title')}
|
visibility: targetVisibility,
|
||||||
|
slug: itemSlug
|
||||||
|
}, 'src', 'dest', 'mime', 'size', 'checksum', 'phash', 'username', 'userchannel', 'usernetwork', 'stamp', 'active', 'is_oc', 'title', 'visibility', 'slug')}
|
||||||
RETURNING id
|
RETURNING id
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@@ -725,8 +758,8 @@ export default router => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Broadcast new_item event for live grid updates (only if auto-approved)
|
// Broadcast new_item event for live grid updates (only if auto-approved and public)
|
||||||
if (!isApprovalRequired) {
|
if (!isApprovalRequired && targetVisibility === 0) {
|
||||||
try {
|
try {
|
||||||
await db`SELECT pg_notify('new_item', ${JSON.stringify({
|
await db`SELECT pg_notify('new_item', ${JSON.stringify({
|
||||||
id: itemid,
|
id: itemid,
|
||||||
@@ -742,8 +775,8 @@ export default router => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Push to Matrix Channel (only if auto-approved)
|
// Push to Matrix Channel (only if auto-approved and public)
|
||||||
if (!isApprovalRequired) {
|
if (!isApprovalRequired && targetVisibility === 0) {
|
||||||
try {
|
try {
|
||||||
const self = router.self;
|
const self = router.self;
|
||||||
const matrixCfg = cfg.clients?.find(c => c.type === 'matrix');
|
const matrixCfg = cfg.clients?.find(c => c.type === 'matrix');
|
||||||
|
|||||||
@@ -174,7 +174,7 @@ export default (router, tpl) => {
|
|||||||
const modequery = (multiRatingSQL ?? lib.getMode(mode)).replace(/items\.id/g, 'i.id');
|
const modequery = (multiRatingSQL ?? lib.getMode(mode)).replace(/items\.id/g, 'i.id');
|
||||||
|
|
||||||
const comments = await db`
|
const comments = await db`
|
||||||
SELECT c.*, i.mime, i.id as item_id
|
SELECT c.*, i.mime, i.id as item_id, i.slug as item_slug
|
||||||
FROM comments c
|
FROM comments c
|
||||||
LEFT JOIN items i ON c.item_id = i.id
|
LEFT JOIN items i ON c.item_id = i.id
|
||||||
WHERE c.user_id = ${userId} AND c.is_deleted = false
|
WHERE c.user_id = ${userId} AND c.is_deleted = false
|
||||||
@@ -570,6 +570,7 @@ export default (router, tpl) => {
|
|||||||
// Fetch the trigger-updated xd_score and the item rating tag from the DB (trigger runs synchronously before we get here)
|
// Fetch the trigger-updated xd_score and the item rating tag from the DB (trigger runs synchronously before we get here)
|
||||||
const itemQuery = await db`
|
const itemQuery = await db`
|
||||||
SELECT
|
SELECT
|
||||||
|
i.slug,
|
||||||
i.xd_score,
|
i.xd_score,
|
||||||
(SELECT ta.tag_id FROM tags_assign ta
|
(SELECT ta.tag_id FROM tags_assign ta
|
||||||
WHERE ta.item_id = i.id AND ta.tag_id = ANY(${[1, 2, cfg.nsfl_tag_id || 3]}::int[])
|
WHERE ta.item_id = i.id AND ta.tag_id = ANY(${[1, 2, cfg.nsfl_tag_id || 3]}::int[])
|
||||||
@@ -592,6 +593,7 @@ export default (router, tpl) => {
|
|||||||
type: 'comment',
|
type: 'comment',
|
||||||
id: commentId,
|
id: commentId,
|
||||||
item_id: item_id,
|
item_id: item_id,
|
||||||
|
item_slug: itemQuery[0]?.slug || null,
|
||||||
parent_id: parent_id || null,
|
parent_id: parent_id || null,
|
||||||
body: notifyBody,
|
body: notifyBody,
|
||||||
username: req.session.user,
|
username: req.session.user,
|
||||||
@@ -1003,6 +1005,7 @@ export default (router, tpl) => {
|
|||||||
c.*,
|
c.*,
|
||||||
i.mime,
|
i.mime,
|
||||||
i.id as item_id,
|
i.id as item_id,
|
||||||
|
i.slug as item_slug,
|
||||||
i.dest as item_dest,
|
i.dest as item_dest,
|
||||||
(SELECT ta.tag_id FROM tags_assign ta
|
(SELECT ta.tag_id FROM tags_assign ta
|
||||||
WHERE ta.item_id = i.id AND ta.tag_id = ANY(${[1, 2, cfg.nsfl_tag_id || 3]}::int[])
|
WHERE ta.item_id = i.id AND ta.tag_id = ANY(${[1, 2, cfg.nsfl_tag_id || 3]}::int[])
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ export default (router, tpl) => {
|
|||||||
ratings: ratingsArr,
|
ratings: ratingsArr,
|
||||||
mime: mime,
|
mime: mime,
|
||||||
fav: false,
|
fav: false,
|
||||||
session: !!req.session,
|
session: req.session,
|
||||||
user_id: req.session?.id,
|
user_id: req.session?.id,
|
||||||
random: isRandom
|
random: isRandom
|
||||||
});
|
});
|
||||||
@@ -253,7 +253,7 @@ export default (router, tpl) => {
|
|||||||
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`);
|
||||||
|
|
||||||
if (!data.success) {
|
if (!data.success) {
|
||||||
if (data.is_private) {
|
if (data.is_private && (data.message === 'private favorites' || req.params.mode === 'favs')) {
|
||||||
const { t: tErr } = createI18n(req.session?.language || req.lang || 'en');
|
const { t: tErr } = createI18n(req.session?.language || req.lang || 'en');
|
||||||
return res.reply({
|
return res.reply({
|
||||||
code: 403,
|
code: 403,
|
||||||
@@ -414,11 +414,11 @@ export default (router, tpl) => {
|
|||||||
|
|
||||||
// Specific route for direct item links: /user/:user/:itemid
|
// Specific route for direct item links: /user/:user/:itemid
|
||||||
// This avoids ambiguity with the profile route
|
// This avoids ambiguity with the profile route
|
||||||
router.get(/\/user\/(?<user>[^/]+)\/(?<itemid>\d+)$/, handleGenericRoute);
|
router.get(/\/user\/(?<user>[^/]+)\/(?<itemid>[a-zA-Z0-9_-]+)$/, handleGenericRoute);
|
||||||
|
|
||||||
// Generic router for everything else (Index, Tags, standard User Grids)
|
// Generic router for everything else (Index, Tags, standard User Grids)
|
||||||
// We exclude static paths (/s/, /b/, /t/, /ca/, /a/) to prevent the greedy regex from intercepting them.
|
// We exclude static paths (/s/, /b/, /t/, /ca/, /a/, system routes) to prevent the greedy regex from intercepting them.
|
||||||
router.get(/^(?!\/(s|b|t|ca|a)\/)\/?(?:\/tag\/(?<tag>.+?))?(?:\/h\/(?<hall>.+?))?(?:\/user\/(?<user>.+?)\/(?<mode>f0cks|uploads|favs))?(?:\/(?<mime>(?:video|audio|image)(?:,(?:video|audio|image))*))?(?:\/p\/(?<page>\d+))?(?:\/(?<itemid>\d+))?\/?(?:\?.*)?$/, handleGenericRoute);
|
router.get(/^(?!\/(s|b|t|ca|a|login|register|settings|about|terms|rules|api|logout|auth|admin|comments|notifications|feed)\/)\/?(?:\/tag\/(?<tag>.+?))?(?:\/h\/(?<hall>.+?))?(?:\/user\/(?<user>.+?)\/(?<mode>f0cks|uploads|favs))?(?:\/(?<mime>(?:video|audio|image)(?:,(?:video|audio|image))*))?(?:\/p\/(?<page>\d+))?(?:\/(?<itemid>[a-zA-Z0-9_-]{11}|\d+))?\/?(?:\?.*)?$/, handleGenericRoute);
|
||||||
/* </routing-refactor> */
|
/* </routing-refactor> */
|
||||||
|
|
||||||
router.get(/^\/(about)$/, (req, res) => {
|
router.get(/^\/(about)$/, (req, res) => {
|
||||||
|
|||||||
@@ -114,7 +114,7 @@ export default (router, tpl) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const f0ck = await db`
|
const f0ck = await db`
|
||||||
select i.dest, i.mime, i.username, i.id, ta.tag_id
|
select i.dest, i.mime, i.username, i.id, i.visibility, ta.tag_id
|
||||||
from "items" i
|
from "items" i
|
||||||
left join tags_assign ta on ta.item_id = i.id and ta.tag_id in (1, 2)
|
left join tags_assign ta on ta.item_id = i.id and ta.tag_id in (1, 2)
|
||||||
where i.id = ${id} and i.active = false
|
where i.id = ${id} and i.active = false
|
||||||
@@ -156,65 +156,69 @@ export default (router, tpl) => {
|
|||||||
console.error('[MOD APPROVE] Failed to notify user:', err);
|
console.error('[MOD APPROVE] Failed to notify user:', err);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Push to Discord Webhook (Direct)
|
const isPublic = (f0ck[0].visibility || 0) === 0;
|
||||||
try {
|
|
||||||
const discordClient = cfg.clients.find(c => c.type === 'discord');
|
|
||||||
if (discordClient && discordClient.webhook_url) {
|
|
||||||
const message = `${f0ck[0].username} uploaded a new video ${cfg.main.url.full}/${id}`;
|
|
||||||
const payload = JSON.stringify({ content: message });
|
|
||||||
const url = new URL(discordClient.webhook_url);
|
|
||||||
const options = {
|
|
||||||
hostname: url.hostname,
|
|
||||||
path: url.pathname + url.search,
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'Content-Length': Buffer.byteLength(payload)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const reqDiscord = https.request(options, (resDiscord) => {
|
|
||||||
if (resDiscord.statusCode >= 400) {
|
|
||||||
console.error(`[MOD APPROVE] Webhook returned status ${resDiscord.statusCode}`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
reqDiscord.on('error', (err) => {
|
|
||||||
console.error('[MOD APPROVE] Webhook failed:', err);
|
|
||||||
});
|
|
||||||
reqDiscord.write(payload);
|
|
||||||
reqDiscord.end();
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error('[MOD APPROVE] Discord Webhook error:', err);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Push to Matrix Channel
|
if (isPublic) {
|
||||||
try {
|
// Push to Discord Webhook (Direct)
|
||||||
const matrixCfg = cfg.clients.find(c => c.type === 'matrix');
|
try {
|
||||||
if (matrixCfg?.notification_channel_id && router.self?.bot?.clients) {
|
const discordClient = cfg.clients.find(c => c.type === 'discord');
|
||||||
const clients = await Promise.all(router.self.bot.clients);
|
if (discordClient && discordClient.webhook_url) {
|
||||||
const matrixWrapper = clients.find(c => c.type === 'matrix');
|
|
||||||
if (matrixWrapper?.client) {
|
|
||||||
const message = `${f0ck[0].username} uploaded a new video ${cfg.main.url.full}/${id}`;
|
const message = `${f0ck[0].username} uploaded a new video ${cfg.main.url.full}/${id}`;
|
||||||
await matrixWrapper.client.send(matrixCfg.notification_channel_id, message);
|
const payload = JSON.stringify({ content: message });
|
||||||
console.log(`[MOD APPROVE] Matrix notification sent for item ${id}`);
|
const url = new URL(discordClient.webhook_url);
|
||||||
|
const options = {
|
||||||
|
hostname: url.hostname,
|
||||||
|
path: url.pathname + url.search,
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Content-Length': Buffer.byteLength(payload)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const reqDiscord = https.request(options, (resDiscord) => {
|
||||||
|
if (resDiscord.statusCode >= 400) {
|
||||||
|
console.error(`[MOD APPROVE] Webhook returned status ${resDiscord.statusCode}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
reqDiscord.on('error', (err) => {
|
||||||
|
console.error('[MOD APPROVE] Webhook failed:', err);
|
||||||
|
});
|
||||||
|
reqDiscord.write(payload);
|
||||||
|
reqDiscord.end();
|
||||||
}
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[MOD APPROVE] Discord Webhook error:', err);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
|
||||||
console.error('[MOD APPROVE] Matrix notification error:', err);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Broadcast new_item event for live grid updates
|
// Push to Matrix Channel
|
||||||
try {
|
try {
|
||||||
await db`SELECT pg_notify('new_item', ${JSON.stringify({
|
const matrixCfg = cfg.clients.find(c => c.type === 'matrix');
|
||||||
id: id,
|
if (matrixCfg?.notification_channel_id && router.self?.bot?.clients) {
|
||||||
dest: f0ck[0].dest,
|
const clients = await Promise.all(router.self.bot.clients);
|
||||||
mime: f0ck[0].mime,
|
const matrixWrapper = clients.find(c => c.type === 'matrix');
|
||||||
username: f0ck[0].username,
|
if (matrixWrapper?.client) {
|
||||||
tag_id: f0ck[0].tag_id,
|
const message = `${f0ck[0].username} uploaded a new video ${cfg.main.url.full}/${id}`;
|
||||||
is_oc: !!f0ck[0].is_oc
|
await matrixWrapper.client.send(matrixCfg.notification_channel_id, message);
|
||||||
})})`;
|
console.log(`[MOD APPROVE] Matrix notification sent for item ${id}`);
|
||||||
} catch (err) {
|
}
|
||||||
console.error('[MOD APPROVE] new_item notify failed:', err);
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[MOD APPROVE] Matrix notification error:', err);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Broadcast new_item event for live grid updates
|
||||||
|
try {
|
||||||
|
await db`SELECT pg_notify('new_item', ${JSON.stringify({
|
||||||
|
id: id,
|
||||||
|
dest: f0ck[0].dest,
|
||||||
|
mime: f0ck[0].mime,
|
||||||
|
username: f0ck[0].username,
|
||||||
|
tag_id: f0ck[0].tag_id,
|
||||||
|
is_oc: !!f0ck[0].is_oc
|
||||||
|
})})`;
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[MOD APPROVE] new_item notify failed:', err);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -122,7 +122,7 @@ db.listen('activity', async (payload) => {
|
|||||||
// We need the username, avatar, and item mime for the preview
|
// We need the username, avatar, and item mime for the preview
|
||||||
// trigger only gave us user_id and item_id
|
// trigger only gave us user_id and item_id
|
||||||
const [details] = await db`
|
const [details] = await db`
|
||||||
SELECT u.id as user_id, u.user as username, uo.avatar, uo.avatar_file, uo.username_color, uo.display_name, i.mime,
|
SELECT u.id as user_id, u.user as username, uo.avatar, uo.avatar_file, uo.username_color, uo.display_name, i.mime, i.slug as item_slug,
|
||||||
(SELECT tag_id FROM tags_assign WHERE item_id = i.id AND tag_id IN (1, 2) LIMIT 1) as tag_id
|
(SELECT tag_id FROM tags_assign WHERE item_id = i.id AND tag_id IN (1, 2) LIMIT 1) as tag_id
|
||||||
FROM "user" u
|
FROM "user" u
|
||||||
LEFT JOIN user_options uo ON u.id = uo.user_id
|
LEFT JOIN user_options uo ON u.id = uo.user_id
|
||||||
@@ -138,6 +138,7 @@ db.listen('activity', async (payload) => {
|
|||||||
data.username_color = details.username_color;
|
data.username_color = details.username_color;
|
||||||
data.display_name = details.display_name || null;
|
data.display_name = details.display_name || null;
|
||||||
data.tag_id = details.tag_id;
|
data.tag_id = details.tag_id;
|
||||||
|
data.item_slug = details.item_slug;
|
||||||
} else {
|
} else {
|
||||||
data.username = 'System';
|
data.username = 'System';
|
||||||
}
|
}
|
||||||
@@ -210,6 +211,7 @@ db.listen('motd', (payload) => {
|
|||||||
db.listen('new_item', (payload) => {
|
db.listen('new_item', (payload) => {
|
||||||
try {
|
try {
|
||||||
const data = JSON.parse(payload);
|
const data = JSON.parse(payload);
|
||||||
|
if (data.visibility && data.visibility !== 0) return;
|
||||||
console.log(`[SSE] Broadcasting new_item (id: ${data.id}) to ${clients.size} clients`);
|
console.log(`[SSE] Broadcasting new_item (id: ${data.id}) to ${clients.size} clients`);
|
||||||
for (const client of clients) {
|
for (const client of clients) {
|
||||||
client.send({ type: 'new_item', data });
|
client.send({ type: 'new_item', data });
|
||||||
@@ -367,7 +369,7 @@ export default (router, tpl) => {
|
|||||||
const typeFilter = tab === 'system' ? SYSTEM_TYPES : (tab === 'user' ? USER_TYPES : null);
|
const typeFilter = tab === 'system' ? SYSTEM_TYPES : (tab === 'user' ? USER_TYPES : null);
|
||||||
const notifications = typeFilter
|
const notifications = typeFilter
|
||||||
? await db`
|
? await db`
|
||||||
SELECT n.id, n.type, n.item_id, n.reference_id, n.created_at, n.is_read, n.data,
|
SELECT n.id, n.type, n.item_id, i.slug as item_slug, n.reference_id, n.created_at, n.is_read, n.data,
|
||||||
COALESCE(u.user, 'System') as from_user,
|
COALESCE(u.user, 'System') as from_user,
|
||||||
COALESCE(uo.display_name, '') as from_display_name,
|
COALESCE(uo.display_name, '') as from_display_name,
|
||||||
COALESCE(u.id, 0) as from_user_id,
|
COALESCE(u.id, 0) as from_user_id,
|
||||||
@@ -392,7 +394,7 @@ export default (router, tpl) => {
|
|||||||
OFFSET ${offset}
|
OFFSET ${offset}
|
||||||
`
|
`
|
||||||
: await db`
|
: await db`
|
||||||
SELECT n.id, n.type, n.item_id, n.reference_id, n.created_at, n.is_read, n.data,
|
SELECT n.id, n.type, n.item_id, i.slug as item_slug, n.reference_id, n.created_at, n.is_read, n.data,
|
||||||
COALESCE(u.user, 'System') as from_user,
|
COALESCE(u.user, 'System') as from_user,
|
||||||
COALESCE(uo.display_name, '') as from_display_name,
|
COALESCE(uo.display_name, '') as from_display_name,
|
||||||
COALESCE(u.id, 0) as from_user_id,
|
COALESCE(u.id, 0) as from_user_id,
|
||||||
@@ -443,7 +445,7 @@ export default (router, tpl) => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const notifications = await db`
|
const notifications = await db`
|
||||||
SELECT n.id, n.type, n.item_id, n.reference_id, n.created_at, n.is_read, n.data,
|
SELECT n.id, n.type, n.item_id, i.slug as item_slug, n.reference_id, n.created_at, n.is_read, n.data,
|
||||||
COALESCE(u.user, 'System') as from_user,
|
COALESCE(u.user, 'System') as from_user,
|
||||||
COALESCE(uo.display_name, '') as from_display_name,
|
COALESCE(uo.display_name, '') as from_display_name,
|
||||||
COALESCE(u.id, 0) as from_user_id,
|
COALESCE(u.id, 0) as from_user_id,
|
||||||
|
|||||||
@@ -97,11 +97,11 @@ export default (router, tpl) => {
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
const favotop = await db`
|
const favotop = await db`
|
||||||
select favorites.item_id, count(*) favs
|
select favorites.item_id as id, items.slug, count(*) favs
|
||||||
from favorites
|
from favorites
|
||||||
join items on items.id = favorites.item_id
|
join items on items.id = favorites.item_id
|
||||||
where items.active = true
|
where items.active = true
|
||||||
group by favorites.item_id
|
group by favorites.item_id, items.slug
|
||||||
having count(*) > 1
|
having count(*) > 1
|
||||||
order by favs desc
|
order by favs desc
|
||||||
limit 10
|
limit 10
|
||||||
@@ -110,7 +110,7 @@ export default (router, tpl) => {
|
|||||||
let xdtop = [];
|
let xdtop = [];
|
||||||
if (config.websrv.enable_xd_score) {
|
if (config.websrv.enable_xd_score) {
|
||||||
const xdRows = await db`
|
const xdRows = await db`
|
||||||
select id, xd_score
|
select id, slug, xd_score
|
||||||
from items
|
from items
|
||||||
where active = true and is_deleted = false and xd_score > 0
|
where active = true and is_deleted = false and xd_score > 0
|
||||||
order by xd_score desc
|
order by xd_score desc
|
||||||
|
|||||||
@@ -268,6 +268,7 @@ export default (router, tpl) => {
|
|||||||
WHERE
|
WHERE
|
||||||
${db.unsafe(modeQuery)}
|
${db.unsafe(modeQuery)}
|
||||||
AND items.active = true
|
AND items.active = true
|
||||||
|
AND COALESCE(items.visibility, 0) = 0
|
||||||
${excludeSwfSQL}
|
${excludeSwfSQL}
|
||||||
${excludePdfSQL}
|
${excludePdfSQL}
|
||||||
${excludeArchiveSQL}
|
${excludeArchiveSQL}
|
||||||
@@ -292,6 +293,7 @@ export default (router, tpl) => {
|
|||||||
WHERE
|
WHERE
|
||||||
${db.unsafe(modeQuery)}
|
${db.unsafe(modeQuery)}
|
||||||
AND items.active = true
|
AND items.active = true
|
||||||
|
AND COALESCE(items.visibility, 0) = 0
|
||||||
${excludeSwfSQL}
|
${excludeSwfSQL}
|
||||||
${excludePdfSQL}
|
${excludePdfSQL}
|
||||||
${excludeArchiveSQL}
|
${excludeArchiveSQL}
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ export default (router, tpl) => {
|
|||||||
const subs = await db`
|
const subs = await db`
|
||||||
SELECT
|
SELECT
|
||||||
s.created_at as sub_date,
|
s.created_at as sub_date,
|
||||||
i.id, i.dest, i.mime, i.username as uploader_name
|
i.id, i.slug, i.dest, i.mime, i.username as uploader_name
|
||||||
FROM comment_subscriptions s
|
FROM comment_subscriptions s
|
||||||
JOIN items i ON s.item_id = i.id
|
JOIN items i ON s.item_id = i.id
|
||||||
WHERE s.user_id = ${req.session.id} AND s.is_subscribed = true
|
WHERE s.user_id = ${req.session.id} AND s.is_subscribed = true
|
||||||
@@ -45,6 +45,7 @@ export default (router, tpl) => {
|
|||||||
|
|
||||||
const items = subs.map(i => ({
|
const items = subs.map(i => ({
|
||||||
id: i.id,
|
id: i.id,
|
||||||
|
slug: i.slug || null,
|
||||||
user: i.uploader_name || 'System',
|
user: i.uploader_name || 'System',
|
||||||
sub_created: new Date(i.sub_date).toLocaleString(),
|
sub_created: new Date(i.sub_date).toLocaleString(),
|
||||||
thumb: `/t/${i.id}.webp`
|
thumb: `/t/${i.id}.webp`
|
||||||
@@ -108,7 +109,7 @@ export default (router, tpl) => {
|
|||||||
const subs = await db`
|
const subs = await db`
|
||||||
SELECT
|
SELECT
|
||||||
s.created_at as sub_date,
|
s.created_at as sub_date,
|
||||||
i.id, i.dest, i.mime, i.username as uploader_name
|
i.id, i.slug, i.dest, i.mime, i.username as uploader_name
|
||||||
FROM comment_subscriptions s
|
FROM comment_subscriptions s
|
||||||
JOIN items i ON s.item_id = i.id
|
JOIN items i ON s.item_id = i.id
|
||||||
WHERE s.user_id = ${req.session.id} AND s.is_subscribed = true
|
WHERE s.user_id = ${req.session.id} AND s.is_subscribed = true
|
||||||
@@ -118,6 +119,7 @@ export default (router, tpl) => {
|
|||||||
|
|
||||||
const items = subs.map(i => ({
|
const items = subs.map(i => ({
|
||||||
id: i.id,
|
id: i.id,
|
||||||
|
slug: i.slug || null,
|
||||||
user: i.uploader_name || 'System',
|
user: i.uploader_name || 'System',
|
||||||
sub_created: new Date(i.sub_date).toLocaleString(),
|
sub_created: new Date(i.sub_date).toLocaleString(),
|
||||||
thumb: `/t/${i.id}.webp`
|
thumb: `/t/${i.id}.webp`
|
||||||
|
|||||||
@@ -31,7 +31,9 @@ export async function regenerateTagImage(tag, mode) {
|
|||||||
JOIN tags_assign ta ON ta.item_id = i.id
|
JOIN tags_assign ta ON ta.item_id = i.id
|
||||||
JOIN tags t ON t.id = ta.tag_id
|
JOIN tags t ON t.id = ta.tag_id
|
||||||
${modeFilter}
|
${modeFilter}
|
||||||
WHERE (t.tag = ${tag} OR t.normalized = ${tag}) AND i.active = true
|
WHERE (t.tag = ${tag} OR t.normalized = ${tag})
|
||||||
|
AND i.active = true
|
||||||
|
AND COALESCE(i.visibility, 0) = 0
|
||||||
ORDER BY RANDOM()
|
ORDER BY RANDOM()
|
||||||
LIMIT 3
|
LIMIT 3
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ export default (router, tpl) => {
|
|||||||
JOIN tags_assign ta ON t.id = ta.tag_id
|
JOIN tags_assign ta ON t.id = ta.tag_id
|
||||||
JOIN items ON items.id = ta.item_id
|
JOIN items ON items.id = ta.item_id
|
||||||
WHERE items.active = true
|
WHERE items.active = true
|
||||||
|
AND COALESCE(items.visibility, 0) = 0
|
||||||
AND t.id NOT IN (1, 2)
|
AND t.id NOT IN (1, 2)
|
||||||
AND ${db.unsafe(modequery)}
|
AND ${db.unsafe(modequery)}
|
||||||
${restrictedFilter}
|
${restrictedFilter}
|
||||||
@@ -58,6 +59,7 @@ export default (router, tpl) => {
|
|||||||
JOIN items ON items.id = ta.item_id
|
JOIN items ON items.id = ta.item_id
|
||||||
WHERE t.normalized LIKE '%' || ${tag.normalized} || '%'
|
WHERE t.normalized LIKE '%' || ${tag.normalized} || '%'
|
||||||
AND items.active = true
|
AND items.active = true
|
||||||
|
AND COALESCE(items.visibility, 0) = 0
|
||||||
AND ${db.unsafe(modequery)}
|
AND ${db.unsafe(modequery)}
|
||||||
${restrictedFilter}
|
${restrictedFilter}
|
||||||
${userExcludeFilter}
|
${userExcludeFilter}
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ export default (router, tpl) => {
|
|||||||
const data = await f0cklib.getf0cks({
|
const data = await f0cklib.getf0cks({
|
||||||
page: req.params.page,
|
page: req.params.page,
|
||||||
mode: req.mode,
|
mode: req.mode,
|
||||||
session: !!req.session,
|
session: req.session,
|
||||||
exclude: req.session?.excluded_tags || [],
|
exclude: req.session?.excluded_tags || [],
|
||||||
user_id: req.session?.id,
|
user_id: req.session?.id,
|
||||||
userHall: slug,
|
userHall: slug,
|
||||||
@@ -138,7 +138,7 @@ export default (router, tpl) => {
|
|||||||
const data = await f0cklib.getf0ck({
|
const data = await f0cklib.getf0ck({
|
||||||
itemid: req.params.itemid,
|
itemid: req.params.itemid,
|
||||||
mode: req.mode,
|
mode: req.mode,
|
||||||
session: !!req.session,
|
session: req.session,
|
||||||
exclude: req.session?.excluded_tags || [],
|
exclude: req.session?.excluded_tags || [],
|
||||||
user_id: req.session?.id,
|
user_id: req.session?.id,
|
||||||
userHall: slug,
|
userHall: slug,
|
||||||
@@ -260,7 +260,7 @@ export default (router, tpl) => {
|
|||||||
FROM items i
|
FROM items i
|
||||||
JOIN user_halls_assign uha ON uha.item_id = i.id
|
JOIN user_halls_assign uha ON uha.item_id = i.id
|
||||||
${modeFilter}
|
${modeFilter}
|
||||||
WHERE uha.hall_id = ${hall.id} AND i.active = true
|
WHERE uha.hall_id = ${hall.id} AND i.active = true AND COALESCE(i.visibility, 0) = 0
|
||||||
ORDER BY RANDOM()
|
ORDER BY RANDOM()
|
||||||
LIMIT 3
|
LIMIT 3
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ export default (router, tpl) => {
|
|||||||
JOIN tags_assign ta ON t.id = ta.tag_id
|
JOIN tags_assign ta ON t.id = ta.tag_id
|
||||||
JOIN items ON items.id = ta.item_id
|
JOIN items ON items.id = ta.item_id
|
||||||
WHERE items.active = true
|
WHERE items.active = true
|
||||||
|
AND COALESCE(items.visibility, 0) = 0
|
||||||
AND t.id NOT IN (1, 2)
|
AND t.id NOT IN (1, 2)
|
||||||
AND ta.user_id = ${userId}
|
AND ta.user_id = ${userId}
|
||||||
AND ${db.unsafe(modequery)}
|
AND ${db.unsafe(modequery)}
|
||||||
@@ -59,6 +60,7 @@ export default (router, tpl) => {
|
|||||||
JOIN items ON items.id = ta.item_id
|
JOIN items ON items.id = ta.item_id
|
||||||
WHERE t.normalized LIKE '%' || ${tag.normalized} || '%'
|
WHERE t.normalized LIKE '%' || ${tag.normalized} || '%'
|
||||||
AND items.active = true
|
AND items.active = true
|
||||||
|
AND COALESCE(items.visibility, 0) = 0
|
||||||
AND ta.user_id = ${userId}
|
AND ta.user_id = ${userId}
|
||||||
AND ${db.unsafe(modequery)}
|
AND ${db.unsafe(modequery)}
|
||||||
${restrictedFilter}
|
${restrictedFilter}
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ export default async bot => {
|
|||||||
rows = await db`
|
rows = await db`
|
||||||
select id, mime, username, size
|
select id, mime, username, size
|
||||||
from "items"
|
from "items"
|
||||||
where id >= ${randomId} and active = true
|
where id >= ${randomId} and active = true and coalesce(visibility, 0) = 0
|
||||||
order by id asc
|
order by id asc
|
||||||
limit 1
|
limit 1
|
||||||
`;
|
`;
|
||||||
@@ -43,7 +43,7 @@ export default async bot => {
|
|||||||
rows = await db`
|
rows = await db`
|
||||||
select id, mime, username, size
|
select id, mime, username, size
|
||||||
from "items"
|
from "items"
|
||||||
where active = true
|
where active = true and coalesce(visibility, 0) = 0
|
||||||
order by id asc
|
order by id asc
|
||||||
limit 1
|
limit 1
|
||||||
`;
|
`;
|
||||||
@@ -54,7 +54,7 @@ export default async bot => {
|
|||||||
rows = await db`
|
rows = await db`
|
||||||
select id, mime, username, size
|
select id, mime, username, size
|
||||||
from "items"
|
from "items"
|
||||||
where
|
where active = true and coalesce(visibility, 0) = 0 and
|
||||||
${args.map(a => a.charAt(0) === "!"
|
${args.map(a => a.charAt(0) === "!"
|
||||||
? db`username not ilike ${a.slice(1)}`
|
? db`username not ilike ${a.slice(1)}`
|
||||||
: db`username ilike ${a}`
|
: db`username ilike ${a}`
|
||||||
|
|||||||
@@ -672,7 +672,7 @@ process.on('uncaughtException', err => {
|
|||||||
user = [_cachedRow];
|
user = [_cachedRow];
|
||||||
} else {
|
} else {
|
||||||
user = await db`
|
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".favorites_private, "user_options".hide_fav_badge, "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
|
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".favorites_private, "user_options".hide_fav_badge, "user_options".default_upload_visibility, "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"
|
from "user_sessions"
|
||||||
left join "user" on "user".id = "user_sessions".user_id
|
left join "user" on "user".id = "user_sessions".user_id
|
||||||
left join "user_options" on "user_options".user_id = "user_sessions".user_id
|
left join "user_options" on "user_options".user_id = "user_sessions".user_id
|
||||||
@@ -875,13 +875,28 @@ process.on('uncaughtException', err => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// CSRF validation helper — used by route handlers that have already populated req.session
|
// Intercept app.readBody to allow memoized reading of request body.
|
||||||
// NOTE: Cannot be used in flummpress app.use() middlewares for upload/avatar bypass handlers
|
// flummpress runs app.use() in parallel via Promise.all before routing/body-reading,
|
||||||
// because flummpress runs ALL middlewares in parallel (Promise.all), so the session
|
// so validateCsrf needs to be able to read req.post without breaking subsequent router handler body parsing.
|
||||||
// middleware hasn't finished by the time these run. Those handlers validate CSRF inline.
|
const _originalReadBody = app.readBody.bind(app);
|
||||||
const validateCsrf = (req, res) => {
|
app.readBody = async (req) => {
|
||||||
|
if (req.post !== undefined) return req.post;
|
||||||
|
return await _originalReadBody(req);
|
||||||
|
};
|
||||||
|
|
||||||
|
// CSRF validation helper — used by route handlers and global middleware
|
||||||
|
const validateCsrf = async (req, res) => {
|
||||||
if (req.session && req.session.csrf_token) {
|
if (req.session && req.session.csrf_token) {
|
||||||
const token = req.headers['x-csrf-token'] || req.body?.csrf_token || req.post?.csrf_token || req.url.qs?.csrf_token;
|
let token = req.headers['x-csrf-token'] || req.body?.csrf_token || req.post?.csrf_token || req.url.qs?.csrf_token;
|
||||||
|
|
||||||
|
// If header/query token is missing and body is not parsed yet on a non-GET method, parse it now
|
||||||
|
if (!token && req.post === undefined && ['POST', 'PUT', 'DELETE', 'PATCH'].includes(req.method)) {
|
||||||
|
try {
|
||||||
|
req.post = await app.readBody(req);
|
||||||
|
token = req.post?.csrf_token;
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
|
|
||||||
if (!token || token !== req.session.csrf_token) {
|
if (!token || token !== req.session.csrf_token) {
|
||||||
console.error(`[CSRF] Blocked ${req.method} ${req.url.pathname} for user ${req.session.user}. Reason: ${!token ? 'Missing token' : 'Token mismatch'}`);
|
console.error(`[CSRF] Blocked ${req.method} ${req.url.pathname} for user ${req.session.user}. Reason: ${!token ? 'Missing token' : 'Token mismatch'}`);
|
||||||
res.writeHead(403, { 'Content-Type': 'application/json' }).end(JSON.stringify({ success: false, msg: 'Invalid CSRF token' }));
|
res.writeHead(403, { 'Content-Type': 'application/json' }).end(JSON.stringify({ success: false, msg: 'Invalid CSRF token' }));
|
||||||
@@ -903,7 +918,7 @@ process.on('uncaughtException', err => {
|
|||||||
if (cfg.websrv.halls_enabled !== false && req.url.pathname.match(/^\/api\/v2\/admin\/halls(\/|$)/)) return;
|
if (cfg.websrv.halls_enabled !== false && req.url.pathname.match(/^\/api\/v2\/admin\/halls(\/|$)/)) return;
|
||||||
// User hall image upload is handled by bypass middleware below
|
// User hall image upload is handled by bypass middleware below
|
||||||
if (cfg.websrv.userhalls_enabled !== false && cfg.websrv.enable_userhall_image_upload !== false && req.url.pathname.match(/^\/api\/v2\/me\/halls\/[^/]+\/image$/)) return;
|
if (cfg.websrv.userhalls_enabled !== false && cfg.websrv.enable_userhall_image_upload !== false && req.url.pathname.match(/^\/api\/v2\/me\/halls\/[^/]+\/image$/)) return;
|
||||||
if (!validateCsrf(req, res)) return;
|
if (!(await validateCsrf(req, res))) return;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Bypass middleware for direct upload handling
|
// Bypass middleware for direct upload handling
|
||||||
@@ -1362,6 +1377,9 @@ process.on('uncaughtException', err => {
|
|||||||
default_font: cfg.websrv.default_font || "",
|
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,
|
||||||
|
enable_private_uploads: cfg.enable_private_uploads !== false,
|
||||||
|
default_upload_visibility: (typeof cfg.default_upload_visibility === 'number' ? cfg.default_upload_visibility : (typeof cfg.websrv?.default_upload_visibility === 'number' ? cfg.websrv.default_upload_visibility : 0)),
|
||||||
|
allow_user_upload_visibility: cfg.allow_user_upload_visibility !== false && cfg.websrv?.allow_user_upload_visibility !== false,
|
||||||
nsfl_tag_id: cfg.nsfl_tag_id || 3,
|
nsfl_tag_id: cfg.nsfl_tag_id || 3,
|
||||||
scroller_mime_cats: Array.isArray(cfg.allowedMimes) ? cfg.allowedMimes.filter(c => ['video','image','audio'].includes(c)) : ['video','image','audio'],
|
scroller_mime_cats: Array.isArray(cfg.allowedMimes) ? cfg.allowedMimes.filter(c => ['video','image','audio'].includes(c)) : ['video','image','audio'],
|
||||||
themes_json: JSON.stringify(cfg.websrv.themes || []),
|
themes_json: JSON.stringify(cfg.websrv.themes || []),
|
||||||
|
|||||||
@@ -78,6 +78,7 @@ export async function handleKoepfeUpload(req, res) {
|
|||||||
throw new Error('Unsupported format');
|
throw new Error('Unsupported format');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await fs.mkdir(cfg.paths.koepfe, { recursive: true });
|
||||||
const newName = crypto.randomBytes(8).toString('hex') + ext;
|
const newName = crypto.randomBytes(8).toString('hex') + ext;
|
||||||
const targetPath = path.join(cfg.paths.koepfe, newName);
|
const targetPath = path.join(cfg.paths.koepfe, newName);
|
||||||
|
|
||||||
|
|||||||
@@ -43,6 +43,10 @@ db`ALTER TABLE items ALTER COLUMN checksum TYPE character varying(255)`.catch(()
|
|||||||
db`ALTER TABLE items ALTER COLUMN dest TYPE character varying(60)`.catch(() => {});
|
db`ALTER TABLE items ALTER COLUMN dest TYPE character varying(60)`.catch(() => {});
|
||||||
db`ALTER TABLE comment_files ALTER COLUMN dest TYPE character varying(60)`.catch(() => {});
|
db`ALTER TABLE comment_files ALTER COLUMN dest TYPE character varying(60)`.catch(() => {});
|
||||||
|
|
||||||
|
// One-time migration: fix NULL visibility values and ensure NOT NULL default going forward
|
||||||
|
db`UPDATE items SET visibility = 0 WHERE visibility IS NULL`.catch(() => {});
|
||||||
|
db`ALTER TABLE items ALTER COLUMN visibility SET DEFAULT 0`.catch(() => {});
|
||||||
|
|
||||||
export const handleUpload = async (req, res, self) => {
|
export const handleUpload = async (req, res, self) => {
|
||||||
// Manual session lookup is required here because this handler is called from a
|
// Manual session lookup is required here because this handler is called from a
|
||||||
// bypass middleware that runs in parallel with the main session middleware.
|
// bypass middleware that runs in parallel with the main session middleware.
|
||||||
@@ -138,6 +142,39 @@ export const handleUpload = async (req, res, self) => {
|
|||||||
|
|
||||||
const is_shitpost = (parts.is_shitpost === 'true' || parts.is_shitpost === '1') || cfg.websrv.shitpost_mode === true;
|
const is_shitpost = (parts.is_shitpost === 'true' || parts.is_shitpost === '1') || cfg.websrv.shitpost_mode === true;
|
||||||
|
|
||||||
|
// Parse visibility: Header 'X-Upload-Visibility' or body field 'visibility' or user default preference
|
||||||
|
let targetVisibility = 0;
|
||||||
|
if (cfg.enable_private_uploads !== false) {
|
||||||
|
const sysDefault = (typeof cfg.default_upload_visibility === 'number')
|
||||||
|
? cfg.default_upload_visibility
|
||||||
|
: (typeof cfg.websrv?.default_upload_visibility === 'number' ? cfg.websrv.default_upload_visibility : 0);
|
||||||
|
|
||||||
|
const allowUserOverride = cfg.allow_user_upload_visibility !== false && cfg.websrv?.allow_user_upload_visibility !== false;
|
||||||
|
|
||||||
|
if (!allowUserOverride) {
|
||||||
|
targetVisibility = sysDefault;
|
||||||
|
} else {
|
||||||
|
const rawVisHeader = req.headers['x-upload-visibility'];
|
||||||
|
const rawVisBody = parts.visibility;
|
||||||
|
const visVal = (rawVisHeader || rawVisBody || '').toString().trim().toLowerCase();
|
||||||
|
|
||||||
|
if (visVal === 'private' || visVal === '2') {
|
||||||
|
targetVisibility = 2;
|
||||||
|
} else if (visVal === 'unlisted' || visVal === '1') {
|
||||||
|
targetVisibility = 1;
|
||||||
|
} else if (visVal === 'public' || visVal === '0') {
|
||||||
|
targetVisibility = 0;
|
||||||
|
} else {
|
||||||
|
targetVisibility = (req.session?.default_upload_visibility !== undefined && req.session?.default_upload_visibility !== null)
|
||||||
|
? req.session.default_upload_visibility
|
||||||
|
: sysDefault;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate slug if enabled
|
||||||
|
const itemSlug = (cfg.enable_item_slugs !== false) ? lib.generateSlug(11) : null;
|
||||||
|
|
||||||
const maxLen = cfg.main.comment_max_length;
|
const maxLen = cfg.main.comment_max_length;
|
||||||
if (comment && maxLen !== null && maxLen !== undefined && comment.length > maxLen) {
|
if (comment && maxLen !== null && maxLen !== undefined && comment.length > maxLen) {
|
||||||
return sendJson(res, { success: false, msg: `Comment too long (max ${maxLen} characters)` }, 400);
|
return sendJson(res, { success: false, msg: `Comment too long (max ${maxLen} characters)` }, 400);
|
||||||
@@ -456,8 +493,10 @@ export const handleUpload = async (req, res, self) => {
|
|||||||
original_filename: originalFilename,
|
original_filename: originalFilename,
|
||||||
title: title,
|
title: title,
|
||||||
width: itemWidth,
|
width: itemWidth,
|
||||||
height: itemHeight
|
height: itemHeight,
|
||||||
}, 'src', 'dest', 'mime', 'size', 'checksum', 'phash', 'username', 'userchannel', 'usernetwork', 'stamp', 'active', 'is_oc', 'original_filename', 'title', 'width', 'height')}
|
visibility: targetVisibility,
|
||||||
|
slug: itemSlug
|
||||||
|
}, 'src', 'dest', 'mime', 'size', 'checksum', 'phash', 'username', 'userchannel', 'usernetwork', 'stamp', 'active', 'is_oc', 'original_filename', 'title', 'width', 'height', 'visibility', 'slug')}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const itemid = await queue.getItemID(filename);
|
const itemid = await queue.getItemID(filename);
|
||||||
@@ -678,33 +717,35 @@ export const handleUpload = async (req, res, self) => {
|
|||||||
// Auto-tagging from embedded metadata was removed — the user must select suggestions explicitly.
|
// Auto-tagging from embedded metadata was removed — the user must select suggestions explicitly.
|
||||||
|
|
||||||
|
|
||||||
// Discord Webhook
|
// Discord Webhook (only for public uploads)
|
||||||
try {
|
if (targetVisibility === 0) {
|
||||||
const discordClient = cfg.clients.find(c => c.type === 'discord');
|
try {
|
||||||
if (discordClient && discordClient.webhook_url) {
|
const discordClient = cfg.clients.find(c => c.type === 'discord');
|
||||||
const message = `${req.session.user} uploaded a new ${actualMime.split('/')[0]}: ${cfg.main.url.full}/${itemid}`;
|
if (discordClient && discordClient.webhook_url) {
|
||||||
const payload = JSON.stringify({ content: message });
|
const message = `${req.session.user} uploaded a new ${actualMime.split('/')[0]}: ${cfg.main.url.full}/${itemid}`;
|
||||||
const url = new URL(discordClient.webhook_url);
|
const payload = JSON.stringify({ content: message });
|
||||||
const options = {
|
const url = new URL(discordClient.webhook_url);
|
||||||
hostname: url.hostname,
|
const options = {
|
||||||
path: url.pathname + url.search,
|
hostname: url.hostname,
|
||||||
method: 'POST',
|
path: url.pathname + url.search,
|
||||||
headers: {
|
method: 'POST',
|
||||||
'Content-Type': 'application/json',
|
headers: {
|
||||||
'Content-Length': Buffer.byteLength(payload)
|
'Content-Type': 'application/json',
|
||||||
}
|
'Content-Length': Buffer.byteLength(payload)
|
||||||
};
|
}
|
||||||
const reqDiscord = https.request(options, (resDiscord) => { });
|
};
|
||||||
reqDiscord.on('error', (err) => console.error('[UPLOAD] Discord Webhook failed:', err));
|
const reqDiscord = https.request(options, (resDiscord) => { });
|
||||||
reqDiscord.write(payload);
|
reqDiscord.on('error', (err) => console.error('[UPLOAD] Discord Webhook failed:', err));
|
||||||
reqDiscord.end();
|
reqDiscord.write(payload);
|
||||||
|
reqDiscord.end();
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`[BACKGROUND ERROR] Discord notification failed:`, err);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
|
||||||
console.error(`[BACKGROUND ERROR] Discord notification failed:`, err);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Broadcast new_item event for live grid updates (only if auto-approved)
|
// Broadcast new_item event for live grid updates (only if auto-approved and public)
|
||||||
if (!manualApproval) {
|
if (!manualApproval && targetVisibility === 0) {
|
||||||
try {
|
try {
|
||||||
await db`SELECT pg_notify('new_item', ${JSON.stringify({
|
await db`SELECT pg_notify('new_item', ${JSON.stringify({
|
||||||
id: itemid,
|
id: itemid,
|
||||||
@@ -720,20 +761,22 @@ export const handleUpload = async (req, res, self) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Push to Matrix Channel
|
// Push to Matrix Channel (only if auto-approved and public)
|
||||||
try {
|
if (!manualApproval && targetVisibility === 0) {
|
||||||
const matrixCfg = cfg.clients.find(c => c.type === 'matrix');
|
try {
|
||||||
if (matrixCfg?.notification_channel_id && self?.bot?.clients) {
|
const matrixCfg = cfg.clients.find(c => c.type === 'matrix');
|
||||||
const clients = await Promise.all(self.bot.clients);
|
if (matrixCfg?.notification_channel_id && self?.bot?.clients) {
|
||||||
const matrixWrapper = clients.find(c => c.type === 'matrix');
|
const clients = await Promise.all(self.bot.clients);
|
||||||
if (matrixWrapper?.client) {
|
const matrixWrapper = clients.find(c => c.type === 'matrix');
|
||||||
const message = `${req.session.user} uploaded a new item ${cfg.main.url.full}/${itemid}`;
|
if (matrixWrapper?.client) {
|
||||||
await matrixWrapper.client.send(matrixCfg.notification_channel_id, message);
|
const message = `${req.session.user} uploaded a new item ${cfg.main.url.full}/${itemid}`;
|
||||||
console.log(`[UPLOAD] Matrix notification sent for item ${itemid}`);
|
await matrixWrapper.client.send(matrixCfg.notification_channel_id, message);
|
||||||
|
console.log(`[UPLOAD] Matrix notification sent for item ${itemid}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[UPLOAD] Matrix notification error:', err);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
|
||||||
console.error('[UPLOAD] Matrix notification error:', err);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Staff Notifications
|
// Staff Notifications
|
||||||
@@ -768,13 +811,16 @@ export const handleUpload = async (req, res, self) => {
|
|||||||
: 'Upload successful! Your upload is now live.';
|
: 'Upload successful! Your upload is now live.';
|
||||||
|
|
||||||
const imagesPath = cfg.websrv.paths?.images || '/b';
|
const imagesPath = cfg.websrv.paths?.images || '/b';
|
||||||
|
const itemRoute = itemSlug ? `/${itemSlug}` : `/${itemid}`;
|
||||||
return sendJson(res, {
|
return sendJson(res, {
|
||||||
success: true,
|
success: true,
|
||||||
msg: successMsg,
|
msg: successMsg,
|
||||||
itemid: itemid,
|
itemid: itemid,
|
||||||
|
slug: itemSlug,
|
||||||
|
visibility: targetVisibility,
|
||||||
manual_approval: manualApproval,
|
manual_approval: manualApproval,
|
||||||
redirect: !manualApproval ? `/${itemid}` : null,
|
redirect: !manualApproval ? itemRoute : null,
|
||||||
url: !manualApproval ? `${cfg.main.url.full}/${itemid}` : `${cfg.main.url.full}/`,
|
url: !manualApproval ? `${cfg.main.url.full}${itemRoute}` : `${cfg.main.url.full}/`,
|
||||||
file_url: !manualApproval ? `${cfg.main.url.full}${imagesPath}/${filename}` : null,
|
file_url: !manualApproval ? `${cfg.main.url.full}${imagesPath}/${filename}` : null,
|
||||||
// Fields for immediate client-side grid injection (avoids SSE race condition)
|
// Fields for immediate client-side grid injection (avoids SSE race condition)
|
||||||
dest: filename,
|
dest: filename,
|
||||||
|
|||||||
@@ -117,18 +117,20 @@
|
|||||||
status.style.color = 'var(--accent)';
|
status.style.color = 'var(--accent)';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const csrfToken = window.f0ckSession?.csrf_token || '{{ csrf_token }}';
|
||||||
const res = await fetch('/admin/settings', {
|
const res = await fetch('/admin/settings', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'X-Requested-With': 'XMLHttpRequest',
|
'X-Requested-With': 'XMLHttpRequest',
|
||||||
'Content-Type': 'application/x-www-form-urlencoded'
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
'X-CSRF-Token': csrfToken
|
||||||
},
|
},
|
||||||
body: new URLSearchParams({
|
body: new URLSearchParams({
|
||||||
manual_approval: approvalToggle.checked ? 'on' : 'off',
|
manual_approval: approvalToggle.checked ? 'on' : 'off',
|
||||||
...(registrationToggle ? { registration_open: registrationToggle.checked ? 'on' : 'off' } : {}),
|
...(registrationToggle ? { registration_open: registrationToggle.checked ? 'on' : 'off' } : {}),
|
||||||
min_tags: minTagsInput.value,
|
min_tags: minTagsInput.value,
|
||||||
trusted_uploads: trustedUploadsInput.value,
|
trusted_uploads: trustedUploadsInput.value,
|
||||||
csrf_token: '{{ csrf_token }}'
|
csrf_token: csrfToken
|
||||||
}).toString()
|
}).toString()
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
@include(snippets/page-title)
|
@include(snippets/page-title)
|
||||||
<div class="posts" data-current-page="{{ pagination.current }}" data-has-more="{{ pagination.next ? 'true' : 'false' }}">
|
<div class="posts" data-current-page="{{ pagination.current }}" data-has-more="{{ pagination.next ? 'true' : 'false' }}">
|
||||||
@each(items as item)
|
@each(items as item)
|
||||||
<a href="{{ link.main }}{{ item.id }}" class="{{ item.is_pinned ? 'anim-boxshadow ' : '' }}thumb lazy-thumb {{ item.has_notification ? 'has-notif' : '' }} {{ item.is_pinned ? 'is-pinned' : '' }}" data-file="{{ item.dest }}" data-mime="{{ item.mime }}" data-user="{!! item.display_name || item.username !!}" data-ext="{{ item.mime.split('/')[1].replace('youtube', 'yt').replace('x-shockwave-flash', 'flash').replace('vnd.adobe.flash.movie', 'flash').toUpperCase() }}" data-mode="{{ item.tag_id == nsfl_tag_id ? 'nsfl' : (item.tag_id == 2 ? 'nsfw' : (item.tag_id == 1 ? 'sfw' : 'null')) }}" data-bg="/t/{{ item.id }}.webp" data-size="{{ enable_dynamic_thumbs ? (item.thumb_size || 1) : 1 }}">
|
<a href="{{ link.main }}{{ item.slug || item.id }}" class="{{ item.is_pinned ? 'anim-boxshadow ' : '' }}thumb lazy-thumb {{ item.has_notification ? 'has-notif' : '' }} {{ item.is_pinned ? 'is-pinned' : '' }}" data-file="{{ item.dest }}" data-mime="{{ item.mime }}" data-user="{!! item.display_name || item.username !!}" data-ext="{{ item.mime.split('/')[1].replace('youtube', 'yt').replace('x-shockwave-flash', 'flash').replace('vnd.adobe.flash.movie', 'flash').toUpperCase() }}" data-mode="{{ item.tag_id == nsfl_tag_id ? 'nsfl' : (item.tag_id == 2 ? 'nsfw' : (item.tag_id == 1 ? 'sfw' : 'null')) }}" data-bg="/t/{{ item.id }}.webp" data-size="{{ enable_dynamic_thumbs ? (item.thumb_size || 1) : 1 }}">
|
||||||
<div class="thumb-indicators">
|
<div class="thumb-indicators">
|
||||||
@if(item.is_pinned)
|
@if(item.is_pinned)
|
||||||
<i class="fa-solid fa-thumbtack pin-indicator anim"></i>
|
<i class="fa-solid fa-thumbtack pin-indicator anim"></i>
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
<div class="item-main-content">
|
<div class="item-main-content">
|
||||||
|
|
||||||
<div class="_204863">
|
<div class="_204863">
|
||||||
<div class="location">{{ link.mainDisplay || link.main }}{{ item.id }}{{ link.suffix }}</div>
|
<div class="location">{{ link.mainDisplay || link.main }}{{ item.slug || item.id }}{{ link.suffix }}</div>
|
||||||
<div class="gapLeft"></div>
|
<div class="gapLeft"></div>
|
||||||
</div>
|
</div>
|
||||||
@if(enable_item_title)
|
@if(enable_item_title)
|
||||||
@@ -102,7 +102,7 @@
|
|||||||
<div class="user-infobox-username-container">
|
<div class="user-infobox-username-container">
|
||||||
<a id="a_username" data-username="{{ item.username || '' }}" data-author-id="{{ item.author_id || '' }}" href="/user/{{ (item.username || '').toLowerCase() }}" tooltip="ID: {{ item.author_id }}" class="user-infobox-username">{!! item.author_display_name || item.username !!}</a>
|
<a id="a_username" data-username="{{ item.username || '' }}" data-author-id="{{ item.author_id || '' }}" href="/user/{{ (item.username || '').toLowerCase() }}" tooltip="ID: {{ item.author_id }}" class="user-infobox-username">{!! item.author_display_name || item.username !!}</a>
|
||||||
</div>
|
</div>
|
||||||
<span class="user-infobox-timestamp"><a href="/{{ item.id }}" class="timestamp-link"><time class="timeago" tooltip="{{ item.timestamp.timefull }}">{{item.timestamp.timeago }}</time></a></span>
|
<span class="user-infobox-timestamp"><a href="/{{ item.slug || item.id }}" class="timestamp-link"><time class="timeago" tooltip="{{ item.timestamp.timefull }}">{{item.timestamp.timeago }}</time></a></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="user-infobox-body">
|
<div class="user-infobox-body">
|
||||||
<div class="user-infobox-description">
|
<div class="user-infobox-description">
|
||||||
@@ -111,9 +111,9 @@
|
|||||||
<div class="user-infobox-actions">
|
<div class="user-infobox-actions">
|
||||||
@if(session)
|
@if(session)
|
||||||
@if(user_has_favorited)
|
@if(user_has_favorited)
|
||||||
<i class="iconset fa-solid fa-heart" id="a_favo" title="Favorite"></i>
|
<i class="iconset fa-solid fa-heart" id="a_favo" data-item-id="{{ item.id }}" title="Favorite"></i>
|
||||||
@else
|
@else
|
||||||
<i class="iconset fa-regular fa-heart" id="a_favo" title="Favorite"></i>
|
<i class="iconset fa-regular fa-heart" id="a_favo" data-item-id="{{ item.id }}" title="Favorite"></i>
|
||||||
@endif
|
@endif
|
||||||
@endif
|
@endif
|
||||||
<span id="oc-badge-container-infobox">@if(item.is_oc)<span class="oc-badge" tooltip="Original Content">OC</span>@endif</span>
|
<span id="oc-badge-container-infobox">@if(item.is_oc)<span class="oc-badge" tooltip="Original Content">OC</span>@endif</span>
|
||||||
@@ -125,13 +125,13 @@
|
|||||||
|
|
||||||
<span class="badge badge-dark">
|
<span class="badge badge-dark">
|
||||||
|
|
||||||
<a href="/{{ item.id }}" class="id-link" @if(user_alternative_infobox)style="display:none"@endif>{{ item.id }}</a>
|
<a href="/{{ item.slug || item.id }}" class="id-link" data-item-id="{{ item.id }}" @if(user_alternative_infobox)style="display:none"@endif>{{ item.slug || item.id }}</a>
|
||||||
@if(!user_alternative_infobox) — [<a id="a_username" data-username="{{ item.username || '' }}" @if(session) data-author-id="{{ item.author_id || '' }}" @endif href="/user/{{ (item.username || '').toLowerCase() }}" @if(session && item.author_id) tooltip="ID: {{ item.author_id }}" @endif @if(item.author_color) style="color: {{ item.author_color }}" @endif>{!! item.author_display_name || item.username || 'unknown' !!}</a>] @endif
|
@if(!user_alternative_infobox) — [<a id="a_username" data-username="{{ item.username || '' }}" @if(session) data-author-id="{{ item.author_id || '' }}" @endif href="/user/{{ (item.username || '').toLowerCase() }}" @if(session && item.author_id) tooltip="ID: {{ item.author_id }}" @endif @if(item.author_color) style="color: {{ item.author_color }}" @endif>{!! item.author_display_name || item.username || 'unknown' !!}</a>] @endif
|
||||||
@if(!user_alternative_infobox)
|
@if(!user_alternative_infobox)
|
||||||
<span id="oc-badge-container">@if(item.is_oc) — <span class="oc-badge" tooltip="Original Content">OC</span>@endif</span>
|
<span id="oc-badge-container">@if(item.is_oc) — <span class="oc-badge" tooltip="Original Content">OC</span>@endif</span>
|
||||||
@endif
|
@endif
|
||||||
</span>
|
</span>
|
||||||
@if(!user_alternative_infobox) — <span class="badge badge-dark"><a href="/{{ item.id }}" class="timestamp-link"><time class="timeago" tooltip="{{ item.timestamp.timefull }}">{{item.timestamp.timeago }}</time></a></span>@if(halls_enabled && item.primaryHall) — @endif @endif
|
@if(!user_alternative_infobox) — <span class="badge badge-dark"><a href="/{{ item.slug || item.id }}" class="timestamp-link"><time class="timeago" tooltip="{{ item.timestamp.timefull }}">{{item.timestamp.timeago }}</time></a></span>@if(halls_enabled && item.primaryHall) — @endif @endif
|
||||||
@if(halls_enabled && item.primaryHall)
|
@if(halls_enabled && item.primaryHall)
|
||||||
<span class="badge hall-badge-wrap">
|
<span class="badge hall-badge-wrap">
|
||||||
<a href="/h/{{ item.primaryHall.slug }}" class="hall-badge-primary"><i class="fa-solid fa-layer-group"></i> {{ item.primaryHall.name }}</a>@if(item.otherHalls && item.otherHalls.length)<span class="hall-overflow-pill">+{{ item.otherHalls.length }}<span class="hall-overflow-tooltip">@each(item.otherHalls as oh)<a href="/h/{{ oh.slug }}">{{ oh.name }}</a>@endeach</span></span>@endif
|
<a href="/h/{{ item.primaryHall.slug }}" class="hall-badge-primary"><i class="fa-solid fa-layer-group"></i> {{ item.primaryHall.name }}</a>@if(item.otherHalls && item.otherHalls.length)<span class="hall-overflow-pill">+{{ item.otherHalls.length }}<span class="hall-overflow-tooltip">@each(item.otherHalls as oh)<a href="/h/{{ oh.slug }}">{{ oh.name }}</a>@endeach</span></span>@endif
|
||||||
@@ -143,9 +143,9 @@
|
|||||||
@if(session)
|
@if(session)
|
||||||
@if(!user_alternative_infobox)
|
@if(!user_alternative_infobox)
|
||||||
@if(user_has_favorited)
|
@if(user_has_favorited)
|
||||||
<i class="iconset fa-solid fa-heart" id="a_favo" title="Favorite"></i>
|
<i class="iconset fa-solid fa-heart" id="a_favo" data-item-id="{{ item.id }}" title="Favorite"></i>
|
||||||
@else
|
@else
|
||||||
<i class="iconset fa-regular fa-heart" id="a_favo" title="Favorite"></i>
|
<i class="iconset fa-regular fa-heart" id="a_favo" data-item-id="{{ item.id }}" title="Favorite"></i>
|
||||||
@endif
|
@endif
|
||||||
@endif
|
@endif
|
||||||
<i class="iconset fa-solid fa-circle-info" id="a_info" data-item-id="{{ item.id }}" title="{{ t('info_modal.button_title') || 'Post & File Info' }}"></i>
|
<i class="iconset fa-solid fa-circle-info" id="a_info" data-item-id="{{ item.id }}" title="{{ t('info_modal.button_title') || 'Post & File Info' }}"></i>
|
||||||
@@ -155,6 +155,7 @@
|
|||||||
<i class="iconset fa-solid fa-layer-group" id="a_hall" data-item-id="{{ item.id }}" data-halls="{{ halls_slugs }}" data-user-halls="{{ user_halls_slugs }}" data-current-hall="{{ (tmp.hall && typeof tmp.hall === 'object') ? tmp.hall.slug : (tmp.hall || '') }}" data-current-user-hall="{{ (tmp.userHall && typeof tmp.userHall === 'object') ? tmp.userHall.slug : (tmp.userHall || '') }}" data-current-user-hall-owner="{{ tmp.userHallOwner || '' }}" title="Add to Hall"></i>
|
<i class="iconset fa-solid fa-layer-group" id="a_hall" data-item-id="{{ item.id }}" data-halls="{{ halls_slugs }}" data-user-halls="{{ user_halls_slugs }}" data-current-hall="{{ (tmp.hall && typeof tmp.hall === 'object') ? tmp.hall.slug : (tmp.hall || '') }}" data-current-user-hall="{{ (tmp.userHall && typeof tmp.userHall === 'object') ? tmp.userHall.slug : (tmp.userHall || '') }}" data-current-user-hall-owner="{{ tmp.userHallOwner || '' }}" title="Add to Hall"></i>
|
||||||
@endif
|
@endif
|
||||||
@if(can_manage_item)
|
@if(can_manage_item)
|
||||||
|
<i class="iconset fa-solid fa-eye" id="a_visibility" data-item-id="{{ item.id }}" data-visibility="{{ item.visibility || 0 }}" title="Visibility: {{ item.visibility === 2 ? 'Private' : (item.visibility === 1 ? 'Unlisted' : 'Public') }} (Click to change)"></i>
|
||||||
<i class="iconset {{ item.is_oc ? 'fa-solid' : 'fa-regular' }} fa-star" id="a_oc" data-item-id="{{ item.id }}" data-is-oc="{{ item.is_oc }}" title="{{ item.is_oc ? 'Remove OC status' : 'Mark as OC' }}"></i>
|
<i class="iconset {{ item.is_oc ? 'fa-solid' : 'fa-regular' }} fa-star" id="a_oc" data-item-id="{{ item.id }}" data-is-oc="{{ item.is_oc }}" title="{{ item.is_oc ? 'Remove OC status' : 'Mark as OC' }}"></i>
|
||||||
@if(can_extract_meta)
|
@if(can_extract_meta)
|
||||||
<i class="iconset fa-solid fa-magic" id="a_metadata" data-item-id="{{ item.id }}" @if(item.mime === 'video/youtube') data-src="https://www.youtube.com/watch?v={{ item.dest.replace('yt:', '') }}" @endif title="Extract Metadata"></i>
|
<i class="iconset fa-solid fa-magic" id="a_metadata" data-item-id="{{ item.id }}" @if(item.mime === 'video/youtube') data-src="https://www.youtube.com/watch?v={{ item.dest.replace('yt:', '') }}" @endif title="Extract Metadata"></i>
|
||||||
@@ -197,7 +198,7 @@
|
|||||||
</span>
|
</span>
|
||||||
<span class="badge" id="favs" @if(!item.favorites.length) hidden@endif>
|
<span class="badge" id="favs" @if(!item.favorites.length) hidden@endif>
|
||||||
@each(item.favorites as fav)
|
@each(item.favorites as fav)
|
||||||
@if(fav.hide_fav_badge)
|
@if(fav.hide_fav_badge && (!session || session.user !== fav.user))
|
||||||
<a class="ghost-fav" tooltip="?" flow="up" style="cursor: default;"><img src="/s/img/ghost_fav.svg" style="height: 32px; width: 32px;" loading="lazy" /></a>
|
<a class="ghost-fav" tooltip="?" flow="up" style="cursor: default;"><img src="/s/img/ghost_fav.svg" style="height: 32px; width: 32px;" loading="lazy" /></a>
|
||||||
@else
|
@else
|
||||||
<a href="/user/{{ fav.user.toLowerCase() }}" tooltip="{!! fav.display_name || fav.user !!}" flow="up"><img src="@if(fav.avatar_file)/a/{{ fav.avatar_file }}@elseif(fav.avatar)/t/{{ fav.avatar }}.webp@else/a/default.png@endif" style="height: 32px; width: 32px@if(fav.username_color); border-color: {{ fav.username_color }}@endif" loading="lazy" /></a>
|
<a href="/user/{{ fav.user.toLowerCase() }}" tooltip="{!! fav.display_name || fav.user !!}" flow="up"><img src="@if(fav.avatar_file)/a/{{ fav.avatar_file }}@elseif(fav.avatar)/t/{{ fav.avatar }}.webp@else/a/default.png@endif" style="height: 32px; width: 32px@if(fav.username_color); border-color: {{ fav.username_color }}@endif" loading="lazy" /></a>
|
||||||
@@ -255,6 +256,27 @@
|
|||||||
<td>{!! item.title !!}</td>
|
<td>{!! item.title !!}</td>
|
||||||
</tr>
|
</tr>
|
||||||
@endif
|
@endif
|
||||||
|
<tr class="info-id-row">
|
||||||
|
<th>ID</th>
|
||||||
|
<td><code>{{ item.id }}</code></td>
|
||||||
|
</tr>
|
||||||
|
<tr class="info-visibility-row">
|
||||||
|
<th>Visibility</th>
|
||||||
|
<td>
|
||||||
|
<span id="info-visibility-label" style="display: inline-flex; align-items: center; gap: 6px;">
|
||||||
|
@if(item.visibility === 2)
|
||||||
|
<i class="fa-solid fa-lock" style="color: var(--color-danger, #ff4444);"></i> Private
|
||||||
|
@elseif(item.visibility === 1)
|
||||||
|
<i class="fa-solid fa-link" style="color: var(--color-warning, #ffbb33);"></i> Unlisted
|
||||||
|
@else
|
||||||
|
<i class="fa-solid fa-globe" style="color: var(--color-success, #00C851);"></i> Public
|
||||||
|
@endif
|
||||||
|
</span>
|
||||||
|
@if(can_manage_item)
|
||||||
|
<button id="info-visibility-edit-btn" class="btn-secondary btn-sm" style="margin-left: 8px; padding: 2px 8px; font-size: 0.8em;" data-item-id="{{ item.id }}" data-visibility="{{ item.visibility || 0 }}"><i class="fa-solid fa-pen"></i> Edit</button>
|
||||||
|
@endif
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th>{{ t('info_modal.file_size') || 'File Size' }}</th>
|
<th>{{ t('info_modal.file_size') || 'File Size' }}</th>
|
||||||
<td>{{ item.size }}</td>
|
<td>{{ item.size }}</td>
|
||||||
@@ -272,7 +294,7 @@
|
|||||||
@if(item.checksum)
|
@if(item.checksum)
|
||||||
<tr>
|
<tr>
|
||||||
<th>{{ t('info_modal.sha256') || 'SHA-256 Hash' }}</th>
|
<th>{{ t('info_modal.sha256') || 'SHA-256 Hash' }}</th>
|
||||||
<td><code style="word-break: break-all;">{{ item.checksum.split('_bypass_')[0] }}</code></td>
|
<td><pre class="info-hash-codeblock"><code style="word-break: break-all;">{{ item.checksum.split('_bypass_')[0] }}</code></pre></td>
|
||||||
</tr>
|
</tr>
|
||||||
@endif
|
@endif
|
||||||
@if(item.is_repost || (item.reposts && item.reposts.length > 0))
|
@if(item.is_repost || (item.reposts && item.reposts.length > 0))
|
||||||
@@ -281,9 +303,9 @@
|
|||||||
<td>
|
<td>
|
||||||
@each(item.reposts as rp)
|
@each(item.reposts as rp)
|
||||||
@if(rp.match_type === 'phash')
|
@if(rp.match_type === 'phash')
|
||||||
<a href="/{{ rp.id }}" style="margin-right: 4px; opacity: 0.75;" tooltip="Visually similar (perceptual hash)" flow="up">~#{{ rp.id }}</a>
|
<a href="/{{ rp.slug || rp.id }}" style="margin-right: 4px; opacity: 0.75;" tooltip="Visually similar (perceptual hash)" flow="up">~#{{ rp.slug || rp.id }}</a>
|
||||||
@else
|
@else
|
||||||
<a href="/{{ rp.id }}" style="margin-right: 4px;" tooltip="Exact duplicate (checksum)" flow="up">#{{ rp.id }}</a>
|
<a href="/{{ rp.slug || rp.id }}" style="margin-right: 4px;" tooltip="Exact duplicate (checksum)" flow="up">#{{ rp.slug || rp.id }}</a>
|
||||||
@endif
|
@endif
|
||||||
@endeach
|
@endeach
|
||||||
</td>
|
</td>
|
||||||
@@ -310,3 +332,43 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div id="visibility-modal" class="modal-overlay" style="display: none;">
|
||||||
|
<div class="modal-content" style="max-width: 420px;">
|
||||||
|
<div class="modal-header" style="margin-bottom: 15px; border-bottom: 1px solid var(--border-color, #333); padding-bottom: 10px;">
|
||||||
|
<h3 style="margin: 0; font-size: 1.1em; color: var(--text-color, #fff);"><i class="fa-solid fa-eye"></i> Change Visibility</h3>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body" style="padding: 10px 0; text-align: left;">
|
||||||
|
<form id="visibility-form" onsubmit="return false;">
|
||||||
|
<input type="hidden" id="visibility-item-id" value="{{ item.id }}" />
|
||||||
|
<div class="visibility-options" style="display: flex; flex-direction: column; gap: 10px;">
|
||||||
|
<label class="visibility-option" style="display: flex; align-items: flex-start; gap: 12px; padding: 12px; border: 1px solid var(--nav-border-color, #444); border-radius: 6px; cursor: pointer; background: var(--bg-secondary, rgba(255,255,255,0.03));">
|
||||||
|
<input type="radio" name="visibility" value="0" style="margin-top: 3px;" />
|
||||||
|
<div>
|
||||||
|
<strong style="display: block; color: var(--text-color, #fff);"><i class="fa-solid fa-globe"></i> Public</strong>
|
||||||
|
<span style="font-size: 0.85em; color: var(--text-muted, #aaa);">Visible to everyone in main feeds, search, and rankings.</span>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
<label class="visibility-option" style="display: flex; align-items: flex-start; gap: 12px; padding: 12px; border: 1px solid var(--nav-border-color, #444); border-radius: 6px; cursor: pointer; background: var(--bg-secondary, rgba(255,255,255,0.03));">
|
||||||
|
<input type="radio" name="visibility" value="1" style="margin-top: 3px;" />
|
||||||
|
<div>
|
||||||
|
<strong style="display: block; color: var(--text-color, #fff);"><i class="fa-solid fa-link"></i> Unlisted</strong>
|
||||||
|
<span style="font-size: 0.85em; color: var(--text-muted, #aaa);">Hidden from main feeds; accessible only via direct link or slug.</span>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
<label class="visibility-option" style="display: flex; align-items: flex-start; gap: 12px; padding: 12px; border: 1px solid var(--nav-border-color, #444); border-radius: 6px; cursor: pointer; background: var(--bg-secondary, rgba(255,255,255,0.03));">
|
||||||
|
<input type="radio" name="visibility" value="2" style="margin-top: 3px;" />
|
||||||
|
<div>
|
||||||
|
<strong style="display: block; color: var(--text-color, #fff);"><i class="fa-solid fa-lock"></i> Private</strong>
|
||||||
|
<span style="font-size: 0.85em; color: var(--text-muted, #aaa);">Only visible to you and site moderators.</span>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="modal-actions" style="display: flex; justify-content: flex-end; gap: 10px; margin-top: 15px;">
|
||||||
|
<button class="btn-secondary" id="visibility-modal-cancel">Cancel</button>
|
||||||
|
<button class="btn-primary" id="visibility-modal-save">Save Visibility</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|||||||
@@ -58,7 +58,7 @@
|
|||||||
<div class="item-main-content">
|
<div class="item-main-content">
|
||||||
|
|
||||||
<div class="_204863">
|
<div class="_204863">
|
||||||
<div class="location">{{ link.mainDisplay || link.main }}{{ item.id }}{{ link.suffix }}</div>
|
<div class="location">{{ link.mainDisplay || link.main }}{{ item.slug || item.id }}{{ link.suffix }}</div>
|
||||||
<div class="gapLeft"></div>
|
<div class="gapLeft"></div>
|
||||||
</div>
|
</div>
|
||||||
@if(enable_item_title)
|
@if(enable_item_title)
|
||||||
@@ -119,25 +119,26 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="blahlol">
|
<div class="blahlol">
|
||||||
<span class="badge badge-dark">
|
<span class="badge badge-dark">
|
||||||
<a href="/{{ item.id }}" class="id-link">{{ item.id }}</a> — [<a id="a_username" data-username="{{ item.username || '' }}" @if(session) data-author-id="{{ item.author_id || '' }}" @endif href="/user/{{ item_username_lower }}" @if(session && item.author_id) tooltip="ID: {{ item.author_id }}" @endif @if(item.author_color) style="color: {{ item.author_color }}" @endif>{!! item.author_display_name || item.username || 'unknown' !!}</a>] <span id="oc-badge-container">@if(item.is_oc) — <span class="oc-badge" tooltip="Original Content">OC</span>@endif</span>
|
<a href="/{{ item.slug || item.id }}" class="id-link" data-item-id="{{ item.id }}">{{ item.slug || item.id }}</a> — [<a id="a_username" data-username="{{ item.username || '' }}" @if(session) data-author-id="{{ item.author_id || '' }}" @endif href="/user/{{ item_username_lower }}" @if(session && item.author_id) tooltip="ID: {{ item.author_id }}" @endif @if(item.author_color) style="color: {{ item.author_color }}" @endif>{!! item.author_display_name || item.username || 'unknown' !!}</a>] <span id="oc-badge-container">@if(item.is_oc) — <span class="oc-badge" tooltip="Original Content">OC</span>@endif</span>
|
||||||
</span>@if(halls_enabled && item.primaryHall) — @endif
|
</span>@if(halls_enabled && item.primaryHall) — @endif
|
||||||
@if(halls_enabled && item.primaryHall)
|
@if(halls_enabled && item.primaryHall)
|
||||||
<span class="badge hall-badge-wrap">
|
<span class="badge hall-badge-wrap">
|
||||||
<a href="/h/{{ item.primaryHall.slug }}" class="hall-badge-primary"><i class="fa-solid fa-layer-group"></i> {{ item.primaryHall.name }}</a>@if(item.otherHalls && item.otherHalls.length)<span class="hall-overflow-pill">+{{ item.otherHalls.length }}<span class="hall-overflow-tooltip">@each(item.otherHalls as oh)<a href="/h/{{ oh.slug }}">{{ oh.name }}</a>@endeach</span></span>@endif
|
<a href="/h/{{ item.primaryHall.slug }}" class="hall-badge-primary"><i class="fa-solid fa-layer-group"></i> {{ item.primaryHall.name }}</a>@if(item.otherHalls && item.otherHalls.length)<span class="hall-overflow-pill">+{{ item.otherHalls.length }}<span class="hall-overflow-tooltip">@each(item.otherHalls as oh)<a href="/h/{{ oh.slug }}">{{ oh.name }}</a>@endeach</span></span>@endif
|
||||||
</span> —
|
</span> —
|
||||||
@endif
|
@endif
|
||||||
<span class="badge badge-dark"><a href="/{{ item.id }}" class="timestamp-link"><time class="timeago" tooltip="{{ item.timestamp.timefull }}">{{item.timestamp.timeago }}</time></a></span>
|
<span class="badge badge-dark"><a href="/{{ item.slug || item.id }}" class="timestamp-link"><time class="timeago" tooltip="{{ item.timestamp.timefull }}">{{item.timestamp.timeago }}</time></a></span>
|
||||||
<div class="gapRight">
|
<div class="gapRight">
|
||||||
@if(session)
|
@if(session)
|
||||||
@if(user_has_favorited)
|
@if(user_has_favorited)
|
||||||
<i class="iconset fa-solid fa-heart" id="a_favo" title="Favorite"></i>
|
<i class="iconset fa-solid fa-heart" id="a_favo" data-item-id="{{ item.id }}" title="Favorite"></i>
|
||||||
@else
|
@else
|
||||||
<i class="iconset fa-regular fa-heart" id="a_favo" title="Favorite"></i>
|
<i class="iconset fa-regular fa-heart" id="a_favo" data-item-id="{{ item.id }}" title="Favorite"></i>
|
||||||
@endif
|
@endif
|
||||||
<i class="iconset fa-solid fa-circle-info" id="a_info" data-item-id="{{ item.id }}" title="{{ t('info_modal.button_title') || 'Post & File Info' }}"></i>
|
<i class="iconset fa-solid fa-circle-info" id="a_info" data-item-id="{{ item.id }}" title="{{ t('info_modal.button_title') || 'Post & File Info' }}"></i>
|
||||||
<i class="iconset {{ isSubscribed ? 'fa-solid' : 'fa-regular' }} fa-bell" id="subscribe-btn" data-item-id="{{ item.id }}" title="{{ isSubscribed ? 'Subscribed' : 'Subscribe' }}"></i>
|
<i class="iconset {{ isSubscribed ? 'fa-solid' : 'fa-regular' }} fa-bell" id="subscribe-btn" data-item-id="{{ item.id }}" title="{{ isSubscribed ? 'Subscribed' : 'Subscribe' }}"></i>
|
||||||
<i class="iconset fa-solid fa-triangle-exclamation report-item-btn" data-item-id="{{ item.id }}" title="Report this post"></i>
|
<i class="iconset fa-solid fa-triangle-exclamation report-item-btn" data-item-id="{{ item.id }}" title="Report this post"></i>
|
||||||
@if(can_manage_item)
|
@if(can_manage_item)
|
||||||
|
<i class="iconset fa-solid fa-eye" id="a_visibility" data-item-id="{{ item.id }}" data-visibility="{{ item.visibility || 0 }}" title="Visibility: {{ item.visibility === 2 ? 'Private' : (item.visibility === 1 ? 'Unlisted' : 'Public') }} (Click to change)"></i>
|
||||||
<i class="iconset {{ item.is_oc ? 'fa-solid' : 'fa-regular' }} fa-star" id="a_oc" data-item-id="{{ item.id }}" data-is-oc="{{ item.is_oc }}" title="{{ item.is_oc ? 'Remove OC status' : 'Mark as OC' }}"></i>
|
<i class="iconset {{ item.is_oc ? 'fa-solid' : 'fa-regular' }} fa-star" id="a_oc" data-item-id="{{ item.id }}" data-is-oc="{{ item.is_oc }}" title="{{ item.is_oc ? 'Remove OC status' : 'Mark as OC' }}"></i>
|
||||||
<i class="iconset fa-solid fa-magic" id="a_metadata" data-item-id="{{ item.id }}" @if(item.mime === 'video/youtube') data-src="https://www.youtube.com/watch?v={{ item.dest.replace('yt:', '') }}" @endif title="Extract Metadata"></i>
|
<i class="iconset fa-solid fa-magic" id="a_metadata" data-item-id="{{ item.id }}" @if(item.mime === 'video/youtube') data-src="https://www.youtube.com/watch?v={{ item.dest.replace('yt:', '') }}" @endif title="Extract Metadata"></i>
|
||||||
@if(is_flash_item)
|
@if(is_flash_item)
|
||||||
@@ -157,7 +158,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<span id="favs" @if(!item.favorites.length) hidden@endif style="margin-top: 5px;">
|
<span id="favs" @if(!item.favorites.length) hidden@endif style="margin-top: 5px;">
|
||||||
@each(item.favorites as fav)
|
@each(item.favorites as fav)
|
||||||
@if(fav.hide_fav_badge)
|
@if(fav.hide_fav_badge && (!session || session.user !== fav.user))
|
||||||
<a class="ghost-fav" tooltip="?" flow="up" style="cursor: default;"><img src="/s/img/ghost_fav.svg" style="height: 32px; width: 32px;" loading="lazy" /></a>
|
<a class="ghost-fav" tooltip="?" flow="up" style="cursor: default;"><img src="/s/img/ghost_fav.svg" style="height: 32px; width: 32px;" loading="lazy" /></a>
|
||||||
@else
|
@else
|
||||||
<a href="/user/{{ fav.user.toLowerCase() }}" tooltip="{!! fav.display_name || fav.user !!}" flow="up"><img src="@if(fav.avatar_file)/a/{{ fav.avatar_file }}@elseif(fav.avatar)/t/{{ fav.avatar }}.webp@else/a/default.png@endif" style="height: 32px; width: 32px@if(fav.username_color); border-color: {{ fav.username_color }}@endif" loading="lazy" /></a>
|
<a href="/user/{{ fav.user.toLowerCase() }}" tooltip="{!! fav.display_name || fav.user !!}" flow="up"><img src="@if(fav.avatar_file)/a/{{ fav.avatar_file }}@elseif(fav.avatar)/t/{{ fav.avatar }}.webp@else/a/default.png@endif" style="height: 32px; width: 32px@if(fav.username_color); border-color: {{ fav.username_color }}@endif" loading="lazy" /></a>
|
||||||
@@ -197,6 +198,27 @@
|
|||||||
<td>{!! item.title !!}</td>
|
<td>{!! item.title !!}</td>
|
||||||
</tr>
|
</tr>
|
||||||
@endif
|
@endif
|
||||||
|
<tr class="info-id-row">
|
||||||
|
<th>ID</th>
|
||||||
|
<td><code>{{ item.id }}</code></td>
|
||||||
|
</tr>
|
||||||
|
<tr class="info-visibility-row">
|
||||||
|
<th>Visibility</th>
|
||||||
|
<td>
|
||||||
|
<span id="info-visibility-label" style="display: inline-flex; align-items: center; gap: 6px;">
|
||||||
|
@if(item.visibility === 2)
|
||||||
|
<i class="fa-solid fa-lock" style="color: var(--color-danger, #ff4444);"></i> Private
|
||||||
|
@elseif(item.visibility === 1)
|
||||||
|
<i class="fa-solid fa-link" style="color: var(--color-warning, #ffbb33);"></i> Unlisted
|
||||||
|
@else
|
||||||
|
<i class="fa-solid fa-globe" style="color: var(--color-success, #00C851);"></i> Public
|
||||||
|
@endif
|
||||||
|
</span>
|
||||||
|
@if(can_manage_item)
|
||||||
|
<button id="info-visibility-edit-btn" class="btn-secondary btn-sm" style="margin-left: 8px; padding: 2px 8px; font-size: 0.8em;" data-item-id="{{ item.id }}" data-visibility="{{ item.visibility || 0 }}"><i class="fa-solid fa-pen"></i> Edit</button>
|
||||||
|
@endif
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th>{{ t('info_modal.file_size') || 'File Size' }}</th>
|
<th>{{ t('info_modal.file_size') || 'File Size' }}</th>
|
||||||
<td>{{ item.size }}</td>
|
<td>{{ item.size }}</td>
|
||||||
@@ -214,7 +236,7 @@
|
|||||||
@if(item.checksum)
|
@if(item.checksum)
|
||||||
<tr>
|
<tr>
|
||||||
<th>{{ t('info_modal.sha256') || 'SHA-256 Hash' }}</th>
|
<th>{{ t('info_modal.sha256') || 'SHA-256 Hash' }}</th>
|
||||||
<td><code style="word-break: break-all;">{{ item.checksum.split('_bypass_')[0] }}</code></td>
|
<td><pre class="info-hash-codeblock"><code style="word-break: break-all;">{{ item.checksum.split('_bypass_')[0] }}</code></pre></td>
|
||||||
</tr>
|
</tr>
|
||||||
@endif
|
@endif
|
||||||
@if(item.is_repost || (item.reposts && item.reposts.length > 0))
|
@if(item.is_repost || (item.reposts && item.reposts.length > 0))
|
||||||
@@ -223,9 +245,9 @@
|
|||||||
<td>
|
<td>
|
||||||
@each(item.reposts as rp)
|
@each(item.reposts as rp)
|
||||||
@if(rp.match_type === 'phash')
|
@if(rp.match_type === 'phash')
|
||||||
<a href="/{{ rp.id }}" style="margin-right: 4px; opacity: 0.75;" tooltip="Visually similar (perceptual hash)" flow="up">~#{{ rp.id }}</a>
|
<a href="/{{ rp.slug || rp.id }}" style="margin-right: 4px; opacity: 0.75;" tooltip="Visually similar (perceptual hash)" flow="up">~#{{ rp.slug || rp.id }}</a>
|
||||||
@else
|
@else
|
||||||
<a href="/{{ rp.id }}" style="margin-right: 4px;" tooltip="Exact duplicate (checksum)" flow="up">#{{ rp.id }}</a>
|
<a href="/{{ rp.slug || rp.id }}" style="margin-right: 4px;" tooltip="Exact duplicate (checksum)" flow="up">#{{ rp.slug || rp.id }}</a>
|
||||||
@endif
|
@endif
|
||||||
@endeach
|
@endeach
|
||||||
</td>
|
</td>
|
||||||
@@ -251,4 +273,44 @@
|
|||||||
<button class="btn-secondary" id="info-modal-close">{{ t('common.close') || 'Close' }}</button>
|
<button class="btn-secondary" id="info-modal-close">{{ t('common.close') || 'Close' }}</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="visibility-modal" class="modal-overlay" style="display: none;">
|
||||||
|
<div class="modal-content" style="max-width: 420px;">
|
||||||
|
<div class="modal-header" style="margin-bottom: 15px; border-bottom: 1px solid var(--border-color, #333); padding-bottom: 10px;">
|
||||||
|
<h3 style="margin: 0; font-size: 1.1em; color: var(--text-color, #fff);"><i class="fa-solid fa-eye"></i> Change Visibility</h3>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body" style="padding: 10px 0; text-align: left;">
|
||||||
|
<form id="visibility-form" onsubmit="return false;">
|
||||||
|
<input type="hidden" id="visibility-item-id" value="{{ item.id }}" />
|
||||||
|
<div class="visibility-options" style="display: flex; flex-direction: column; gap: 10px;">
|
||||||
|
<label class="visibility-option" style="display: flex; align-items: flex-start; gap: 12px; padding: 12px; border: 1px solid var(--nav-border-color, #444); border-radius: 6px; cursor: pointer; background: var(--bg-secondary, rgba(255,255,255,0.03));">
|
||||||
|
<input type="radio" name="visibility" value="0" style="margin-top: 3px;" />
|
||||||
|
<div>
|
||||||
|
<strong style="display: block; color: var(--text-color, #fff);"><i class="fa-solid fa-globe"></i> Public</strong>
|
||||||
|
<span style="font-size: 0.85em; color: var(--text-muted, #aaa);">Visible to everyone in main feeds, search, and rankings.</span>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
<label class="visibility-option" style="display: flex; align-items: flex-start; gap: 12px; padding: 12px; border: 1px solid var(--nav-border-color, #444); border-radius: 6px; cursor: pointer; background: var(--bg-secondary, rgba(255,255,255,0.03));">
|
||||||
|
<input type="radio" name="visibility" value="1" style="margin-top: 3px;" />
|
||||||
|
<div>
|
||||||
|
<strong style="display: block; color: var(--text-color, #fff);"><i class="fa-solid fa-link"></i> Unlisted</strong>
|
||||||
|
<span style="font-size: 0.85em; color: var(--text-muted, #aaa);">Hidden from main feeds; accessible only via direct link or slug.</span>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
<label class="visibility-option" style="display: flex; align-items: flex-start; gap: 12px; padding: 12px; border: 1px solid var(--nav-border-color, #444); border-radius: 6px; cursor: pointer; background: var(--bg-secondary, rgba(255,255,255,0.03));">
|
||||||
|
<input type="radio" name="visibility" value="2" style="margin-top: 3px;" />
|
||||||
|
<div>
|
||||||
|
<strong style="display: block; color: var(--text-color, #fff);"><i class="fa-solid fa-lock"></i> Private</strong>
|
||||||
|
<span style="font-size: 0.85em; color: var(--text-muted, #aaa);">Only visible to you and site moderators.</span>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="modal-actions" style="display: flex; justify-content: flex-end; gap: 10px; margin-top: 15px;">
|
||||||
|
<button class="btn-secondary" id="visibility-modal-cancel">Cancel</button>
|
||||||
|
<button class="btn-primary" id="visibility-modal-save">Save Visibility</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -94,7 +94,7 @@
|
|||||||
<tbody>
|
<tbody>
|
||||||
@each(favotop as favo)
|
@each(favotop as favo)
|
||||||
<tr>
|
<tr>
|
||||||
<td><a href="/{{ favo.item_id }}">#{{ favo.item_id }}</a></td>
|
<td><a href="/{{ favo.slug || favo.id }}">#{{ favo.slug || favo.id }}</a></td>
|
||||||
<td>{{ favo.favs }} <span style="opacity: 0.5; font-size: 0.8em;">{{ t('ranking.favs') }}</span></td>
|
<td>{{ favo.favs }} <span style="opacity: 0.5; font-size: 0.8em;">{{ t('ranking.favs') }}</span></td>
|
||||||
</tr>
|
</tr>
|
||||||
@endeach
|
@endeach
|
||||||
@@ -109,7 +109,7 @@
|
|||||||
<tbody>
|
<tbody>
|
||||||
@each(xdtop as item)
|
@each(xdtop as item)
|
||||||
<tr>
|
<tr>
|
||||||
<td><a href="/{{ item.id }}">#{{ item.id }}</a></td>
|
<td><a href="/{{ item.slug || item.id }}">#{{ item.slug || item.id }}</a></td>
|
||||||
<td>
|
<td>
|
||||||
<span class="xd-score-badge xd-tier-{{ item.xd_tier }}" tooltip="xD Score: {{ item.xd_score }} pts" flow="up">
|
<span class="xd-score-badge xd-tier-{{ item.xd_tier }}" tooltip="xD Score: {{ item.xd_score }} pts" flow="up">
|
||||||
{{ item.xd_label }} <span class="xd-score-num">{{ item.xd_score }}</span>
|
{{ item.xd_label }} <span class="xd-score-num">{{ item.xd_score }}</span>
|
||||||
|
|||||||
@@ -334,6 +334,19 @@
|
|||||||
<small class="text-muted" style="margin-left: 25px;">{{ t('settings.hide_fav_badge_hint') }}</small>
|
<small class="text-muted" style="margin-left: 25px;">{{ t('settings.hide_fav_badge_hint') }}</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@if(allow_user_upload_visibility !== false)
|
||||||
|
<div class="setting-item" style="margin-top: 15px;">
|
||||||
|
<label for="default_upload_visibility_select" style="display: block; font-weight: bold; margin-bottom: 5px;">
|
||||||
|
<span>{{ t('settings.default_upload_visibility') }}</span>
|
||||||
|
</label>
|
||||||
|
<select id="default_upload_visibility_select" class="form-control" style="width: 200px; padding: 5px; background: var(--bg-color); color: var(--text-color); border: 1px solid var(--nav-border-color); border-radius: 4px;">
|
||||||
|
<option value="0" @if(session.default_upload_visibility === 0 || (!session.default_upload_visibility && session.default_upload_visibility !== 0)) selected @endif>{{ t('visibility.public') }}</option>
|
||||||
|
<option value="1" @if(session.default_upload_visibility === 1) selected @endif>{{ t('visibility.unlisted') }}</option>
|
||||||
|
<option value="2" @if(session.default_upload_visibility === 2) selected @endif>{{ t('visibility.private') }}</option>
|
||||||
|
</select>
|
||||||
|
<small class="text-muted" style="display: block; margin-top: 5px;">{{ t('settings.default_upload_visibility_hint') }}</small>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
<div class="setting-item" style="margin-top: 15px;">
|
<div class="setting-item" style="margin-top: 15px;">
|
||||||
<label for="show_background_toggle" style="cursor: pointer; display: flex; align-items: center; gap: 10px;">
|
<label for="show_background_toggle" style="cursor: pointer; display: flex; align-items: center; gap: 10px;">
|
||||||
<input type="checkbox" id="show_background_toggle" @if(session.show_background !==false) checked @endif>
|
<input type="checkbox" id="show_background_toggle" @if(session.show_background !==false) checked @endif>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<html lang="{{ lang || 'en' }}" theme="@if(typeof theme !== 'undefined'){{ theme }}@endif" res="@if(typeof fullscreen !== 'undefined'){{ fullscreen == 1 ? 'fullscreen' : '' }}@endif">
|
<html lang="{{ lang || 'en' }}" theme="@if(typeof theme !== 'undefined'){{ theme }}@endif" res="@if(typeof fullscreen !== 'undefined'){{ fullscreen == 1 ? 'fullscreen' : '' }}@endif">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
@if(typeof page_meta !== 'undefined' && page_meta.title)<title>{{ domain }} - {{ page_meta.title }}</title>@elseif(typeof item !== 'undefined')<title>{{ domain }} - {{ item.id }}</title>@else<title>{{ domain }}</title>@endif
|
@if(typeof page_meta !== 'undefined' && page_meta.title)<title>{{ domain }} - {{ page_meta.title }}</title>@elseif(typeof item !== 'undefined')<title>{{ domain }} - {{ item.slug || item.id }}</title>@else<title>{{ domain }}</title>@endif
|
||||||
<link rel="manifest" href="/manifest.json">
|
<link rel="manifest" href="/manifest.json">
|
||||||
<meta name="theme-color" content="#0096ff">
|
<meta name="theme-color" content="#0096ff">
|
||||||
<meta name="mobile-web-app-capable" content="yes">
|
<meta name="mobile-web-app-capable" content="yes">
|
||||||
@@ -72,18 +72,18 @@
|
|||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0" />
|
||||||
@if(typeof item !== 'undefined')
|
@if(typeof item !== 'undefined')
|
||||||
<link rel="canonical" href="https://{{ domain }}/{{ item.id }}" />
|
<link rel="canonical" href="https://{{ domain }}/{{ item.slug || item.id }}" />
|
||||||
<meta property="og:site_name" content="{{ domain }}" />
|
<meta property="og:site_name" content="{{ domain }}" />
|
||||||
<meta property="og:title" content="{{ item.id }}" />
|
<meta property="og:title" content="{{ item.slug || item.id }}" />
|
||||||
<meta property="og:url" content="https://{{ domain }}/{{ item.id }}" />
|
<meta property="og:url" content="https://{{ domain }}/{{ item.slug || item.id }}" />
|
||||||
<meta property="og:image" content="https://{{ domain }}{{ item.og_thumbnail }}" />
|
<meta property="og:image" content="https://{{ domain }}{{ item.og_thumbnail }}" />
|
||||||
<meta name="description" content="{{ site_description }}" />
|
<meta name="description" content="{{ site_description }}" />
|
||||||
<meta property="og:description" content="{{ site_description }}" />
|
<meta property="og:description" content="{{ site_description }}" />
|
||||||
<meta property="og:type" content="website" />
|
<meta property="og:type" content="website" />
|
||||||
<meta property="twitter:card" content="summary" />
|
<meta property="twitter:card" content="summary" />
|
||||||
<meta property="twitter:title" content="{{ item.id }}" />
|
<meta property="twitter:title" content="{{ item.slug || item.id }}" />
|
||||||
<meta property="twitter:image" content="https://{{ domain }}{{ item.og_thumbnail }}" />
|
<meta property="twitter:image" content="https://{{ domain }}{{ item.og_thumbnail }}" />
|
||||||
<meta property="twitter:url" content="https://{{ domain }}/{{ item.id }}" />
|
<meta property="twitter:url" content="https://{{ domain }}/{{ item.slug || item.id }}" />
|
||||||
@else
|
@else
|
||||||
<meta property="og:site_name" content="{{ domain }}" />
|
<meta property="og:site_name" content="{{ domain }}" />
|
||||||
<meta property="og:title" content="@if(typeof page_meta !== 'undefined' && page_meta.title){{ page_meta.title }} - {{ domain }}@else{{ domain }}@endif" />
|
<meta property="og:title" content="@if(typeof page_meta !== 'undefined' && page_meta.title){{ page_meta.title }} - {{ domain }}@else{{ domain }}@endif" />
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
@each(items as item)
|
@each(items as item)
|
||||||
<a href="{{ link.main }}{{ item.id }}" class="{{ item.is_pinned ? 'anim-boxshadow ' : '' }}thumb lazy-thumb {{ item.has_notification ? 'has-notif' : '' }} {{ item.is_pinned ? 'is-pinned' : '' }}" data-file="{{ item.dest }}" data-mime="{{ item.mime }}" data-user="{!! item.display_name || item.username !!}" data-ext="{{ item.mime.split('/')[1].replace('youtube', 'yt').replace('x-shockwave-flash', 'flash').replace('vnd.adobe.flash.movie', 'flash').replace('x-zip-compressed', 'zip').replace('x-rar-compressed', 'rar').replace('vnd.rar', 'rar').replace('x-7z-compressed', '7z').replace('x-tar', 'tar').replace('x-bzip2', 'bz2').replace('x-xz', 'xz').toUpperCase() }}" data-mode="{{ item.tag_id == nsfl_tag_id ? 'nsfl' : (item.tag_id == 2 ? 'nsfw' : (item.tag_id == 1 ? 'sfw' : 'null')) }}" data-bg="/t/{{ item.id }}.webp" data-size="{{ enable_dynamic_thumbs ? (item.thumb_size || 1) : 1 }}">
|
<a href="{{ link.main }}{{ item.slug || item.id }}" class="{{ item.is_pinned ? 'anim-boxshadow ' : '' }}thumb lazy-thumb {{ item.has_notification ? 'has-notif' : '' }} {{ item.is_pinned ? 'is-pinned' : '' }}" data-file="{{ item.dest }}" data-mime="{{ item.mime }}" data-user="{!! item.display_name || item.username !!}" data-ext="{{ item.mime.split('/')[1].replace('youtube', 'yt').replace('x-shockwave-flash', 'flash').replace('vnd.adobe.flash.movie', 'flash').replace('x-zip-compressed', 'zip').replace('x-rar-compressed', 'rar').replace('vnd.rar', 'rar').replace('x-7z-compressed', '7z').replace('x-tar', 'tar').replace('x-bzip2', 'bz2').replace('x-xz', 'xz').toUpperCase() }}" data-mode="{{ item.tag_id == nsfl_tag_id ? 'nsfl' : (item.tag_id == 2 ? 'nsfw' : (item.tag_id == 1 ? 'sfw' : 'null')) }}" data-bg="/t/{{ item.id }}.webp" data-size="{{ enable_dynamic_thumbs ? (item.thumb_size || 1) : 1 }}">
|
||||||
<div class="thumb-indicators">
|
<div class="thumb-indicators">
|
||||||
@if(item.is_pinned)
|
@if(item.is_pinned)
|
||||||
<i class="fa-solid fa-thumbtack pin-indicator anim"></i>
|
<i class="fa-solid fa-thumbtack pin-indicator anim"></i>
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
@each(notifications as n)
|
@each(notifications as n)
|
||||||
@if(n.type === 'approve')
|
@if(n.type === 'approve')
|
||||||
<a href="/{{ n.item_id }}" class="notif-item {{ n.is_read ? '' : 'unread' }} notif-with-thumb" data-id="{{ n.id }}">
|
<a href="/{{ n.item_slug || n.item_id }}" class="notif-item {{ n.is_read ? '' : 'unread' }} notif-with-thumb" data-id="{{ n.id }}">
|
||||||
<div class="notif-thumb" data-mode="{{ n.item_mode || '' }}">
|
<div class="notif-thumb" data-mode="{{ n.item_mode || '' }}">
|
||||||
<img src="/t/{{ n.item_id }}{{ ((active_mode===0&&(n.item_mode==='nsfw'||n.item_mode==='nsfl'))||(active_mode===1&&n.item_mode==='nsfl')||(active_mode===4&&(n.item_mode==='sfw'||n.item_mode==='nsfw'))) ? '_blur' : '' }}.webp" data-orig-src="/t/{{ n.item_id }}.webp" alt="thumb" onerror="this.onerror=null;this.src='/mod/pending/t/{{ n.item_id }}.webp';this.onerror=function(){this.onerror=null;this.src='/mod/deleted/t/{{ n.item_id }}.webp';this.onerror=function(){this.style.display='none';};};"/>
|
<img src="/t/{{ n.item_id }}{{ ((active_mode===0&&(n.item_mode==='nsfw'||n.item_mode==='nsfl'))||(active_mode===1&&n.item_mode==='nsfl')||(active_mode===4&&(n.item_mode==='sfw'||n.item_mode==='nsfw'))) ? '_blur' : '' }}.webp" data-orig-src="/t/{{ n.item_id }}.webp" alt="thumb" onerror="this.onerror=null;this.src='/mod/pending/t/{{ n.item_id }}.webp';this.onerror=function(){this.onerror=null;this.src='/mod/deleted/t/{{ n.item_id }}.webp';this.onerror=function(){this.style.display='none';};};"/>
|
||||||
</div>
|
</div>
|
||||||
<div class="notif-content">
|
<div class="notif-content">
|
||||||
<div class="notif-user"><strong>{{ t('notifications.system') }}</strong></div>
|
<div class="notif-user"><strong>{{ t('notifications.system') }}</strong></div>
|
||||||
<div class="notif-msg">{{ t('notifications.upload_approved').replace('{id}', n.item_id) }}</div>
|
<div class="notif-msg">{{ t('notifications.upload_approved').replace('{id}', n.item_slug || n.item_id) }}</div>
|
||||||
<div class="notif-time">{{ new Date(n.created_at).toLocaleString() }}</div>
|
<div class="notif-time">{{ new Date(n.created_at).toLocaleString() }}</div>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="notif-content">
|
<div class="notif-content">
|
||||||
<div class="notif-user"><strong>{{ t('notifications.admin') }}</strong></div>
|
<div class="notif-user"><strong>{{ t('notifications.admin') }}</strong></div>
|
||||||
<div class="notif-msg">{{ t('notifications.upload_pending').replace('{id}', n.item_id) }}</div>
|
<div class="notif-msg">{{ t('notifications.upload_pending').replace('{id}', n.item_slug || n.item_id) }}</div>
|
||||||
<div class="notif-time">{{ new Date(n.created_at).toLocaleString() }}</div>
|
<div class="notif-time">{{ new Date(n.created_at).toLocaleString() }}</div>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
@@ -33,46 +33,46 @@
|
|||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
@elseif(n.type === 'deny')
|
@elseif(n.type === 'deny')
|
||||||
<a href="/{{ n.item_id }}" class="notif-item {{ n.is_read ? '' : 'unread' }} notif-with-thumb" data-id="{{ n.id }}">
|
<a href="/{{ n.item_slug || n.item_id }}" class="notif-item {{ n.is_read ? '' : 'unread' }} notif-with-thumb" data-id="{{ n.id }}">
|
||||||
<div class="notif-thumb" data-mode="{{ n.item_mode || '' }}">
|
<div class="notif-thumb" data-mode="{{ n.item_mode || '' }}">
|
||||||
<img src="/t/{{ n.item_id }}{{ ((active_mode===0&&(n.item_mode==='nsfw'||n.item_mode==='nsfl'))||(active_mode===1&&n.item_mode==='nsfl')||(active_mode===4&&(n.item_mode==='sfw'||n.item_mode==='nsfw'))) ? '_blur' : '' }}.webp" data-orig-src="/t/{{ n.item_id }}.webp" alt="thumb" onerror="this.onerror=null;this.src='/t/{{ n.item_id }}.webp';this.onerror=function(){this.style.display='none'};" />
|
<img src="/t/{{ n.item_id }}{{ ((active_mode===0&&(n.item_mode==='nsfw'||n.item_mode==='nsfl'))||(active_mode===1&&n.item_mode==='nsfl')||(active_mode===4&&(n.item_mode==='sfw'||n.item_mode==='nsfw'))) ? '_blur' : '' }}.webp" data-orig-src="/t/{{ n.item_id }}.webp" alt="thumb" onerror="this.onerror=null;this.src='/t/{{ n.item_id }}.webp';this.onerror=function(){this.style.display='none'};" />
|
||||||
</div>
|
</div>
|
||||||
<div class="notif-content">
|
<div class="notif-content">
|
||||||
<div class="notif-user"><strong>{{ t('notifications.system') }}</strong></div>
|
<div class="notif-user"><strong>{{ t('notifications.system') }}</strong></div>
|
||||||
<div class="notif-msg">
|
<div class="notif-msg">
|
||||||
<strong>{{ t('notifications.upload_denied').replace('{id}', n.item_id) }}</strong>
|
<strong>{{ t('notifications.upload_denied').replace('{id}', n.item_slug || n.item_id) }}</strong>
|
||||||
<div style="font-size: 0.85em; color: #ffb8b8; margin-top: 4px;">Reason: {{ n.reason }}</div>
|
<div style="font-size: 0.85em; color: #ffb8b8; margin-top: 4px;">Reason: {{ n.reason }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="notif-time">{{ new Date(n.created_at).toLocaleString() }}</div>
|
<div class="notif-time">{{ new Date(n.created_at).toLocaleString() }}</div>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
@elseif(n.type === 'item_deleted')
|
@elseif(n.type === 'item_deleted')
|
||||||
<a href="/{{ n.item_id }}" class="notif-item {{ n.is_read ? '' : 'unread' }} notif-with-thumb" data-id="{{ n.id }}">
|
<a href="/{{ n.item_slug || n.item_id }}" class="notif-item {{ n.is_read ? '' : 'unread' }} notif-with-thumb" data-id="{{ n.id }}">
|
||||||
<div class="notif-thumb" data-mode="{{ n.item_mode || '' }}">
|
<div class="notif-thumb" data-mode="{{ n.item_mode || '' }}">
|
||||||
<img src="/t/{{ n.item_id }}{{ ((active_mode===0&&(n.item_mode==='nsfw'||n.item_mode==='nsfl'))||(active_mode===1&&n.item_mode==='nsfl')||(active_mode===4&&(n.item_mode==='sfw'||n.item_mode==='nsfw'))) ? '_blur' : '' }}.webp" data-orig-src="/t/{{ n.item_id }}.webp" alt="thumb" onerror="this.onerror=null;this.src='/t/{{ n.item_id }}.webp';this.onerror=function(){this.style.display='none'};" />
|
<img src="/t/{{ n.item_id }}{{ ((active_mode===0&&(n.item_mode==='nsfw'||n.item_mode==='nsfl'))||(active_mode===1&&n.item_mode==='nsfl')||(active_mode===4&&(n.item_mode==='sfw'||n.item_mode==='nsfw'))) ? '_blur' : '' }}.webp" data-orig-src="/t/{{ n.item_id }}.webp" alt="thumb" onerror="this.onerror=null;this.src='/t/{{ n.item_id }}.webp';this.onerror=function(){this.style.display='none'};" />
|
||||||
</div>
|
</div>
|
||||||
<div class="notif-content">
|
<div class="notif-content">
|
||||||
<div class="notif-user"><strong>{{ t('notifications.moderation') }}</strong></div>
|
<div class="notif-user"><strong>{{ t('notifications.moderation') }}</strong></div>
|
||||||
<div class="notif-msg">
|
<div class="notif-msg">
|
||||||
<strong>{{ t('notifications.upload_deleted').replace('{id}', n.item_id) }}</strong>
|
<strong>{{ t('notifications.upload_deleted').replace('{id}', n.item_slug || n.item_id) }}</strong>
|
||||||
<div style="font-size: 0.85em; color: #ffb8b8; margin-top: 4px;">Reason: {{ n.reason }}</div>
|
<div style="font-size: 0.85em; color: #ffb8b8; margin-top: 4px;">Reason: {{ n.reason }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="notif-time">{{ new Date(n.created_at).toLocaleString() }}</div>
|
<div class="notif-time">{{ new Date(n.created_at).toLocaleString() }}</div>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
@elseif(n.type === 'upload_comment')
|
@elseif(n.type === 'upload_comment')
|
||||||
<a href="/{{ n.item_id }}#c{{ n.reference_id }}" class="notif-item {{ n.is_read ? '' : 'unread' }} notif-with-thumb" data-id="{{ n.id }}">
|
<a href="/{{ n.item_slug || n.item_id }}#c{{ n.reference_id }}" class="notif-item {{ n.is_read ? '' : 'unread' }} notif-with-thumb" data-id="{{ n.id }}">
|
||||||
<div class="notif-thumb" data-mode="{{ n.item_mode || '' }}">
|
<div class="notif-thumb" data-mode="{{ n.item_mode || '' }}">
|
||||||
<img src="/t/{{ n.item_id }}{{ ((active_mode===0&&(n.item_mode==='nsfw'||n.item_mode==='nsfl'))||(active_mode===1&&n.item_mode==='nsfl')||(active_mode===4&&(n.item_mode==='sfw'||n.item_mode==='nsfw'))) ? '_blur' : '' }}.webp" data-orig-src="/t/{{ n.item_id }}.webp" alt="thumbnail" onerror="this.style.display='none'">
|
<img src="/t/{{ n.item_id }}{{ ((active_mode===0&&(n.item_mode==='nsfw'||n.item_mode==='nsfl'))||(active_mode===1&&n.item_mode==='nsfl')||(active_mode===4&&(n.item_mode==='sfw'||n.item_mode==='nsfw'))) ? '_blur' : '' }}.webp" data-orig-src="/t/{{ n.item_id }}.webp" alt="thumbnail" onerror="this.style.display='none'">
|
||||||
</div>
|
</div>
|
||||||
<div class="notif-content">
|
<div class="notif-content">
|
||||||
<div class="notif-info"><strong>{{ t('notifications.new_comments') }}</strong></div>
|
<div class="notif-info"><strong>{{ t('notifications.new_comments') }}</strong></div>
|
||||||
<div class="notif-msg">{{ t('notifications.on_your_upload').replace('{id}', n.item_id) }}</div>
|
<div class="notif-msg">{{ t('notifications.on_your_upload').replace('{id}', n.item_slug || n.item_id) }}</div>
|
||||||
<div class="notif-time">{{ new Date(n.created_at).toLocaleString() }}</div>
|
<div class="notif-time">{{ new Date(n.created_at).toLocaleString() }}</div>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
@elseif(n.type === 'upload_success')
|
@elseif(n.type === 'upload_success')
|
||||||
<a href="/{{ n.item_id }}" class="notif-item {{ n.is_read ? '' : 'unread' }} notif-with-thumb" data-id="{{ n.id }}">
|
<a href="/{{ n.item_slug || n.item_id }}" class="notif-item {{ n.is_read ? '' : 'unread' }} notif-with-thumb" data-id="{{ n.id }}">
|
||||||
<div class="notif-thumb" data-mode="{{ n.item_mode || '' }}">
|
<div class="notif-thumb" data-mode="{{ n.item_mode || '' }}">
|
||||||
<img src="/t/{{ n.item_id }}{{ ((active_mode===0&&(n.item_mode==='nsfw'||n.item_mode==='nsfl'))||(active_mode===1&&n.item_mode==='nsfl')||(active_mode===4&&(n.item_mode==='sfw'||n.item_mode==='nsfw'))) ? '_blur' : '' }}.webp" data-orig-src="/t/{{ n.item_id }}.webp" alt="thumb" onerror="this.style.display='none'" />
|
<img src="/t/{{ n.item_id }}{{ ((active_mode===0&&(n.item_mode==='nsfw'||n.item_mode==='nsfl'))||(active_mode===1&&n.item_mode==='nsfl')||(active_mode===4&&(n.item_mode==='sfw'||n.item_mode==='nsfw'))) ? '_blur' : '' }}.webp" data-orig-src="/t/{{ n.item_id }}.webp" alt="thumb" onerror="this.style.display='none'" />
|
||||||
</div>
|
</div>
|
||||||
@@ -83,7 +83,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
@elseif(n.type === 'upload_error')
|
@elseif(n.type === 'upload_error')
|
||||||
<a href="{{ n.item_id ? '/' + n.item_id : '#' }}" class="notif-item {{ n.is_read ? '' : 'unread' }} {{ n.item_id ? 'notif-with-thumb' : '' }}" data-id="{{ n.id }}">
|
<a href="{{ n.item_id ? '/' + (n.item_slug || n.item_id) : '#' }}" class="notif-item {{ n.is_read ? '' : 'unread' }} {{ n.item_id ? 'notif-with-thumb' : '' }}" data-id="{{ n.id }}">
|
||||||
@if(n.item_id)
|
@if(n.item_id)
|
||||||
<div class="notif-thumb" data-mode="{{ n.item_mode || '' }}">
|
<div class="notif-thumb" data-mode="{{ n.item_mode || '' }}">
|
||||||
<img src="/t/{{ n.item_id }}{{ ((active_mode===0&&(n.item_mode==='nsfw'||n.item_mode==='nsfl'))||(active_mode===1&&n.item_mode==='nsfl')||(active_mode===4&&(n.item_mode==='sfw'||n.item_mode==='nsfw'))) ? '_blur' : '' }}.webp" data-orig-src="/t/{{ n.item_id }}.webp" alt="thumb" onerror="this.onerror=null;this.src='/mod/pending/t/{{ n.item_id }}.webp';this.onerror=function(){this.style.display='none'};" />
|
<img src="/t/{{ n.item_id }}{{ ((active_mode===0&&(n.item_mode==='nsfw'||n.item_mode==='nsfl'))||(active_mode===1&&n.item_mode==='nsfl')||(active_mode===4&&(n.item_mode==='sfw'||n.item_mode==='nsfw'))) ? '_blur' : '' }}.webp" data-orig-src="/t/{{ n.item_id }}.webp" alt="thumb" onerror="this.onerror=null;this.src='/mod/pending/t/{{ n.item_id }}.webp';this.onerror=function(){this.style.display='none'};" />
|
||||||
@@ -116,7 +116,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@else
|
@else
|
||||||
<a href="/{{ n.item_id }}#c{{ n.comment_id || n.reference_id }}" class="notif-item {{ n.is_read ? '' : 'unread' }} notif-with-thumb" data-id="{{ n.id }}">
|
<a href="/{{ n.item_slug || n.item_id }}#c{{ n.comment_id || n.reference_id }}" class="notif-item {{ n.is_read ? '' : 'unread' }} notif-with-thumb" data-id="{{ n.id }}">
|
||||||
@if(n.item_id)
|
@if(n.item_id)
|
||||||
<div class="notif-thumb" data-mode="{{ n.item_mode || '' }}">
|
<div class="notif-thumb" data-mode="{{ n.item_mode || '' }}">
|
||||||
<img src="/t/{{ n.item_id }}{{ ((active_mode===0&&(n.item_mode==='nsfw'||n.item_mode==='nsfl'))||(active_mode===1&&n.item_mode==='nsfl')||(active_mode===4&&(n.item_mode==='sfw'||n.item_mode==='nsfw'))) ? '_blur' : '' }}.webp" data-orig-src="/t/{{ n.item_id }}.webp" alt="thumb" onerror="this.style.display='none'" />
|
<img src="/t/{{ n.item_id }}{{ ((active_mode===0&&(n.item_mode==='nsfw'||n.item_mode==='nsfl'))||(active_mode===1&&n.item_mode==='nsfl')||(active_mode===4&&(n.item_mode==='sfw'||n.item_mode==='nsfw'))) ? '_blur' : '' }}.webp" data-orig-src="/t/{{ n.item_id }}.webp" alt="thumb" onerror="this.style.display='none'" />
|
||||||
@@ -125,9 +125,9 @@
|
|||||||
<div class="notif-content">
|
<div class="notif-content">
|
||||||
<div class="notif-user"><strong @if(n.username_color) style="color: {{ n.username_color }}" @endif>{!! n.from_user !!}</strong></div>
|
<div class="notif-user"><strong @if(n.username_color) style="color: {{ n.username_color }}" @endif>{!! n.from_user !!}</strong></div>
|
||||||
<div class="notif-msg">
|
<div class="notif-msg">
|
||||||
@if(n.type === 'comment_reply') {{ t('notifications.replied').replace('{id}', n.item_id) }}
|
@if(n.type === 'comment_reply') {{ t('notifications.replied').replace('{id}', n.item_slug || n.item_id) }}
|
||||||
@elseif(n.type === 'subscription') {{ t('notifications.subscribed').replace('{id}', n.item_id) }}
|
@elseif(n.type === 'subscription') {{ t('notifications.subscribed').replace('{id}', n.item_slug || n.item_id) }}
|
||||||
@elseif(n.type === 'mention') {{ t('notifications.mentioned').replace('{id}', n.item_id) }}
|
@elseif(n.type === 'mention') {{ t('notifications.mentioned').replace('{id}', n.item_slug || n.item_id) }}
|
||||||
@endif
|
@endif
|
||||||
</div>
|
</div>
|
||||||
<div class="notif-time">{{ new Date(n.created_at).toLocaleString() }}</div>
|
<div class="notif-time">{{ new Date(n.created_at).toLocaleString() }}</div>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
@each(items as item)
|
@each(items as item)
|
||||||
<div class="sub-card {{ item.is_pinned ? 'anim-boxshadow is-pinned' : '' }}" id="sub-{{ item.id }}">
|
<div class="sub-card {{ item.is_pinned ? 'anim-boxshadow is-pinned' : '' }}" id="sub-{{ item.id }}">
|
||||||
<a href="/{{ item.id }}" class="sub-link">
|
<a href="/{{ item.slug || item.id }}" class="sub-link">
|
||||||
<div class="thumb-indicators">
|
<div class="thumb-indicators">
|
||||||
@if(item.is_pinned)
|
@if(item.is_pinned)
|
||||||
<i class="fa-solid fa-thumbtack pin-indicator anim"></i>
|
<i class="fa-solid fa-thumbtack pin-indicator anim"></i>
|
||||||
@@ -8,7 +8,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<img src="{{ item.thumb }}" loading="lazy" />
|
<img src="{{ item.thumb }}" loading="lazy" />
|
||||||
<div class="sub-info">
|
<div class="sub-info">
|
||||||
<span class="sub-id">#{{ item.id }}</span>
|
<span class="sub-id">#{{ item.slug || item.id }}</span>
|
||||||
<span class="sub-user">{{ t('subscriptions.by_user').replace('{user}', item.user) }}</span>
|
<span class="sub-user">{{ t('subscriptions.by_user').replace('{user}', item.user) }}</span>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -79,6 +79,26 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@if(enable_private_uploads !== false && allow_user_upload_visibility !== false)
|
||||||
|
<div class="form-section global-visibility-section">
|
||||||
|
<label>{{ t('upload.visibility') || 'Visibility' }}</label>
|
||||||
|
<div class="visibility-options rating-options">
|
||||||
|
<label class="rating-option">
|
||||||
|
<input type="radio" name="visibility" value="0" {{ ((session.default_upload_visibility !== undefined && session.default_upload_visibility !== null ? session.default_upload_visibility : default_upload_visibility) === 0) ? 'checked' : '' }}>
|
||||||
|
<span class="rating-label sfw" style="display: flex; align-items: center; justify-content: center; gap: 6px;"><i class="fa-solid fa-globe"></i> {{ t('common.public') || 'Public' }}</span>
|
||||||
|
</label>
|
||||||
|
<label class="rating-option">
|
||||||
|
<input type="radio" name="visibility" value="1" {{ ((session.default_upload_visibility !== undefined && session.default_upload_visibility !== null ? session.default_upload_visibility : default_upload_visibility) === 1) ? 'checked' : '' }}>
|
||||||
|
<span class="rating-label nsfw" style="display: flex; align-items: center; justify-content: center; gap: 6px;"><i class="fa-solid fa-link"></i> {{ t('common.unlisted') || 'Unlisted' }}</span>
|
||||||
|
</label>
|
||||||
|
<label class="rating-option">
|
||||||
|
<input type="radio" name="visibility" value="2" {{ ((session.default_upload_visibility !== undefined && session.default_upload_visibility !== null ? session.default_upload_visibility : default_upload_visibility) === 2) ? 'checked' : '' }}>
|
||||||
|
<span class="rating-label nsfl" style="display: flex; align-items: center; justify-content: center; gap: 6px;"><i class="fa-solid fa-lock"></i> {{ t('common.private') || 'Private' }}</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
|
||||||
<div class="form-section global-tag-section">
|
<div class="form-section global-tag-section">
|
||||||
<label>
|
<label>
|
||||||
|
|||||||
@@ -123,7 +123,7 @@
|
|||||||
@if(count.f0cks)
|
@if(count.f0cks)
|
||||||
<div class="posts no-infinite-scroll">
|
<div class="posts no-infinite-scroll">
|
||||||
@each(f0cks.items as item)
|
@each(f0cks.items as item)
|
||||||
<a href="{{ f0cks.link.main }}{{ item.id }}" class="{{ item.is_pinned ? 'anim-boxshadow ' : '' }}thumb lazy-thumb {{ item.is_pinned ? 'is-pinned' : '' }}" data-file="{{ item.dest }}" data-mime="{{ item.mime }}" data-user="{!! item.display_name || item.username !!}" data-ext="{{ item.mime.split('/')[1].replace('youtube', 'yt').replace('x-shockwave-flash', 'flash').replace('vnd.adobe.flash.movie', 'flash').toUpperCase() }}" data-mode="{{ item.tag_id == nsfl_tag_id ? 'nsfl' : (item.tag_id == 2 ? 'nsfw' : (item.tag_id == 1 ? 'sfw' : 'null')) }}" data-bg="/t/{{ item.id }}.webp">
|
<a href="{{ f0cks.link.main }}{{ item.slug || item.id }}" class="{{ item.is_pinned ? 'anim-boxshadow ' : '' }}thumb lazy-thumb {{ item.is_pinned ? 'is-pinned' : '' }}" data-file="{{ item.dest }}" data-mime="{{ item.mime }}" data-user="{!! item.display_name || item.username !!}" data-ext="{{ item.mime.split('/')[1].replace('youtube', 'yt').replace('x-shockwave-flash', 'flash').replace('vnd.adobe.flash.movie', 'flash').toUpperCase() }}" data-mode="{{ item.tag_id == nsfl_tag_id ? 'nsfl' : (item.tag_id == 2 ? 'nsfw' : (item.tag_id == 1 ? 'sfw' : 'null')) }}" data-bg="/t/{{ item.id }}.webp">
|
||||||
<div class="thumb-indicators">
|
<div class="thumb-indicators">
|
||||||
@if(item.is_pinned)
|
@if(item.is_pinned)
|
||||||
<i class="fa-solid fa-thumbtack pin-indicator anim"></i>
|
<i class="fa-solid fa-thumbtack pin-indicator anim"></i>
|
||||||
@@ -156,7 +156,7 @@
|
|||||||
@if(count.favs)
|
@if(count.favs)
|
||||||
<div class="posts no-infinite-scroll">
|
<div class="posts no-infinite-scroll">
|
||||||
@each(favs.items as item)
|
@each(favs.items as item)
|
||||||
<a href="{{ favs.link.main }}{{ item.id }}" class="{{ item.is_pinned ? 'anim-boxshadow ' : '' }}thumb lazy-thumb {{ item.is_pinned ? 'is-pinned' : '' }}" data-file="{{ item.dest }}" data-mime="{{ item.mime }}" data-user="{!! item.display_name || item.username !!}" data-ext="{{ item.mime.split('/')[1].replace('youtube', 'yt').replace('x-shockwave-flash', 'flash').replace('vnd.adobe.flash.movie', 'flash').toUpperCase() }}" data-mode="{{ item.tag_id == nsfl_tag_id ? 'nsfl' : (item.tag_id == 2 ? 'nsfw' : (item.tag_id == 1 ? 'sfw' : 'null')) }}" data-bg="/t/{{ item.id }}.webp">
|
<a href="{{ favs.link.main }}{{ item.slug || item.id }}" class="{{ item.is_pinned ? 'anim-boxshadow ' : '' }}thumb lazy-thumb {{ item.is_pinned ? 'is-pinned' : '' }}" data-file="{{ item.dest }}" data-mime="{{ item.mime }}" data-user="{!! item.display_name || item.username !!}" data-ext="{{ item.mime.split('/')[1].replace('youtube', 'yt').replace('x-shockwave-flash', 'flash').replace('vnd.adobe.flash.movie', 'flash').toUpperCase() }}" data-mode="{{ item.tag_id == nsfl_tag_id ? 'nsfl' : (item.tag_id == 2 ? 'nsfw' : (item.tag_id == 1 ? 'sfw' : 'null')) }}" data-bg="/t/{{ item.id }}.webp">
|
||||||
<div class="thumb-indicators">
|
<div class="thumb-indicators">
|
||||||
@if(item.is_pinned)
|
@if(item.is_pinned)
|
||||||
<i class="fa-solid fa-thumbtack pin-indicator anim"></i>
|
<i class="fa-solid fa-thumbtack pin-indicator anim"></i>
|
||||||
|
|||||||
Reference in New Issue
Block a user