Compare commits
56 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1262b15a5b | |||
| 2cd8bd91ff | |||
| 7b186dcdb2 | |||
| 973753d231 | |||
| c30b3535ae | |||
| eed8b993e1 | |||
| 9c239769e6 | |||
| 6e07194b27 | |||
| 866a8003fe | |||
| 5ab1a9614a | |||
| e45df6a769 | |||
| 99eb9735a1 | |||
| b651621cee | |||
| d3ce2e38f0 | |||
| 096786516e | |||
| ec895a285b | |||
| 20aaf183d8 | |||
| 25ab4ba949 | |||
| 6a52445987 | |||
| 63c39ebe90 | |||
| 0a21b6c8f8 | |||
| a7aabac956 | |||
| 9f5510bc04 | |||
| 54e2e8871b | |||
| c99f3e11d8 | |||
| d14feaf3eb | |||
| 5e35fd48d4 | |||
| a07c21f22b | |||
| 65fb96f675 | |||
| 20a6ffcb04 | |||
| 8518e87c8d | |||
| a3148848f2 | |||
| abedf7a789 | |||
| e61e24dfe8 | |||
| 44af6ae061 | |||
| 0739d72334 | |||
| 848055446a | |||
| 88e6b6efb9 | |||
| eea70ed62e | |||
| b6c906636c | |||
| 55b5a0ba9f | |||
| 5d60d80fc9 | |||
| 4dfb80b149 | |||
| b319f21cd4 | |||
| 28ed8d985c | |||
| 0f5e795793 | |||
| 78d14a1b56 | |||
| 3f149531da | |||
| 7a4432277d | |||
| 84eee7e13d | |||
| 2206b3eb86 | |||
| 5529724982 | |||
| 9c6070afff | |||
| 5bfa336773 | |||
| 517444f9ce | |||
| edb92ffa2a |
@@ -1,9 +1,9 @@
|
|||||||
POSTGRES_USER=f0ckm
|
POSTGRES_USER=f0ckm
|
||||||
POSTGRES_DB=f0ckm
|
POSTGRES_DB=f0ckm
|
||||||
POSTGRES_PASSWORD=f0ckm
|
POSTGRES_PASSWORD=f0ckm
|
||||||
# --- Nginx & Let's Encrypt Configuration (Optional) ---
|
# --- Nginx & Tor Profiles (Optional) ---
|
||||||
# Set to 'f0ckm-nginx' to enable the Nginx & Let's Encrypt proxy stack
|
# Set to 'f0ckm-nginx' for Nginx, 'tor' for Tor hidden service, or 'f0ckm-nginx,tor' for both
|
||||||
# COMPOSE_PROFILES=f0ckm-nginx
|
# COMPOSE_PROFILES=tor
|
||||||
#
|
#
|
||||||
# VIRTUAL_HOST=yourdomain.com
|
# VIRTUAL_HOST=yourdomain.com
|
||||||
# LETSENCRYPT_HOST=yourdomain.com
|
# LETSENCRYPT_HOST=yourdomain.com
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -18,3 +18,4 @@ public/tag_cache
|
|||||||
config.json
|
config.json
|
||||||
bundle.css
|
bundle.css
|
||||||
public/s/fonts/impact.woff
|
public/s/fonts/impact.woff
|
||||||
|
f0ck-svelte/*
|
||||||
20
README.md
20
README.md
@@ -78,3 +78,23 @@ now visit http://localhost:1337 in your browser, you can develop without needing
|
|||||||
## NGINX
|
## NGINX
|
||||||
|
|
||||||
uncomment in .env # COMPOSE_PROFILES=f0ckm-nginx to enable nginx proxy with automatic lets encrypt.
|
uncomment in .env # COMPOSE_PROFILES=f0ckm-nginx to enable nginx proxy with automatic lets encrypt.
|
||||||
|
|
||||||
|
## Tor Hidden Service (.onion)
|
||||||
|
|
||||||
|
Tor is bundled in `docker-compose.yml` and preconfigured as long as COMPOSE_PROFILES=f0ckm-tor is set with a hidden service pointing to the application (`f0ckm:1337`).
|
||||||
|
|
||||||
|
When running `docker compose up -d`, Tor automatically generates a `.onion` address for your node. You can find your onion address by running:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cat ./f0ckm-data/tor/f0ckm_hs/hostname
|
||||||
|
```
|
||||||
|
|
||||||
|
### Automatic Onion-Location Header
|
||||||
|
|
||||||
|
To advertise your `.onion` address to Tor Browser users visiting your clearnet site, add your `.onion` address to `config.json`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"main": {
|
||||||
|
"onion": "http://yourgeneratedaddress.onion"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|||||||
9
config/tor/torrc
Normal file
9
config/tor/torrc
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
# Tor configuration for f0ckm
|
||||||
|
|
||||||
|
# Enable SOCKS proxy for internal container & external connections
|
||||||
|
SocksPort 0.0.0.0:9050
|
||||||
|
|
||||||
|
# Tor Hidden Service (v3 Onion Service) configuration
|
||||||
|
# Tor will automatically generate private keys and hostnames in /var/lib/tor/f0ckm_hs/
|
||||||
|
HiddenServiceDir /var/lib/tor/f0ckm_hs/
|
||||||
|
HiddenServicePort 80 f0ckm:1337
|
||||||
@@ -5,6 +5,7 @@
|
|||||||
"domain": "example.com",
|
"domain": "example.com",
|
||||||
"regex": "example\\.com"
|
"regex": "example\\.com"
|
||||||
},
|
},
|
||||||
|
"onion": "http://your-onion-address.onion",
|
||||||
"socks": "socks5://127.0.0.1:9050",
|
"socks": "socks5://127.0.0.1:9050",
|
||||||
"mail": "admin@example.com",
|
"mail": "admin@example.com",
|
||||||
"maxfilesize": 104857600,
|
"maxfilesize": 104857600,
|
||||||
@@ -61,6 +62,7 @@
|
|||||||
"show_koepfe": false,
|
"show_koepfe": false,
|
||||||
"hide_sidebar_default": true,
|
"hide_sidebar_default": true,
|
||||||
"koepfe": [],
|
"koepfe": [],
|
||||||
|
"enable_tor_hs": true,
|
||||||
"enable_global_chat": true,
|
"enable_global_chat": true,
|
||||||
"enable_danmaku": true,
|
"enable_danmaku": true,
|
||||||
"private_messages": true,
|
"private_messages": true,
|
||||||
@@ -132,6 +134,8 @@
|
|||||||
"open_registration_require_mail_andor_token": false,
|
"open_registration_require_mail_andor_token": false,
|
||||||
"private_society": false,
|
"private_society": false,
|
||||||
"private_society_gate": "cloudflare",
|
"private_society_gate": "cloudflare",
|
||||||
|
"private_society_gate_location": "Frankfurt",
|
||||||
|
"private_society_gate_template": "_gate_template",
|
||||||
"public_nsfw": false,
|
"public_nsfw": false,
|
||||||
"paths": {
|
"paths": {
|
||||||
"images": "/b",
|
"images": "/b",
|
||||||
|
|||||||
@@ -49,9 +49,14 @@ services:
|
|||||||
tor:
|
tor:
|
||||||
image: dockurr/tor
|
image: dockurr/tor
|
||||||
container_name: tor
|
container_name: tor
|
||||||
|
profiles:
|
||||||
|
- tor
|
||||||
ports:
|
ports:
|
||||||
- "9050:9050"
|
- "127.0.0.1:9050:9050"
|
||||||
- "8118:8118"
|
- "127.0.0.1:8118:8118"
|
||||||
|
volumes:
|
||||||
|
- ./config/tor:/etc/tor:ro
|
||||||
|
- ./f0ckm-data/tor:/var/lib/tor:Z
|
||||||
networks:
|
networks:
|
||||||
- f0ckm-net
|
- f0ckm-net
|
||||||
restart: always
|
restart: always
|
||||||
@@ -99,6 +104,8 @@ services:
|
|||||||
container_name: f0ckm-nginx
|
container_name: f0ckm-nginx
|
||||||
profiles:
|
profiles:
|
||||||
- f0ckm-nginx
|
- f0ckm-nginx
|
||||||
|
environment:
|
||||||
|
- ENABLE_IPV6=true
|
||||||
ports:
|
ports:
|
||||||
- "80:80"
|
- "80:80"
|
||||||
- "443:443"
|
- "443:443"
|
||||||
|
|||||||
5
migrations/add_favorites_private.sql
Normal file
5
migrations/add_favorites_private.sql
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
-- Add favorites_private column to user_options table
|
||||||
|
ALTER TABLE user_options
|
||||||
|
ADD COLUMN IF NOT EXISTS favorites_private boolean DEFAULT false;
|
||||||
|
|
||||||
|
COMMENT ON COLUMN public.user_options.favorites_private IS 'When true, only the owner and administrators can view the user''s favorites list';
|
||||||
5
migrations/add_hide_fav_badge.sql
Normal file
5
migrations/add_hide_fav_badge.sql
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
-- Add hide_fav_badge column to user_options table
|
||||||
|
ALTER TABLE user_options
|
||||||
|
ADD COLUMN IF NOT EXISTS hide_fav_badge boolean DEFAULT false;
|
||||||
|
|
||||||
|
COMMENT ON COLUMN public.user_options.favorites_private IS 'When true, the user''s favorite badge on item pages displays as a ghost icon without linking to their profile';
|
||||||
@@ -1498,7 +1498,9 @@ CREATE TABLE public.user_options (
|
|||||||
receive_user_notifications boolean DEFAULT true,
|
receive_user_notifications boolean DEFAULT true,
|
||||||
do_not_disturb boolean DEFAULT false,
|
do_not_disturb boolean DEFAULT false,
|
||||||
comment_display_mode integer DEFAULT 1,
|
comment_display_mode integer DEFAULT 1,
|
||||||
force_comment_display_mode integer DEFAULT 0
|
force_comment_display_mode integer DEFAULT 0,
|
||||||
|
favorites_private boolean DEFAULT false,
|
||||||
|
hide_fav_badge boolean DEFAULT false
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1237,7 +1237,7 @@ html[theme='95'] .item-main-content .metadata {
|
|||||||
|
|
||||||
.notif-count {
|
.notif-count {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0px;
|
top: 0;
|
||||||
right: -5px;
|
right: -5px;
|
||||||
background: var(--badge-nsfw);
|
background: var(--badge-nsfw);
|
||||||
color: white;
|
color: white;
|
||||||
@@ -1650,6 +1650,10 @@ body.layout-modern .global-sidebar-right {
|
|||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
body.sidebar-right-hidden #sidebar-drag-zone {
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
/* Always show the handle on touch devices (no hover state available) */
|
/* Always show the handle on touch devices (no hover state available) */
|
||||||
@media (pointer: coarse) {
|
@media (pointer: coarse) {
|
||||||
#sidebar-drag-zone {
|
#sidebar-drag-zone {
|
||||||
@@ -1984,7 +1988,7 @@ body.layout-modern .global-sidebar-right {
|
|||||||
right: 0;
|
right: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 20px;
|
height: 20px;
|
||||||
background: linear-gradient(to bottom, transparent, rgba(28, 27, 27, 0.8));
|
background: linear-gradient(to bottom, transparent, var(--bg));
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2119,43 +2123,29 @@ body.layout-modern .global-sidebar-right {
|
|||||||
font-size: 1.5em;
|
font-size: 1.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Mobile Stacking for Legacy Mode (max-width: 999px) */
|
/* Mobile sidebar (max-width: 999px): sidebar fixed, capped to viewport width */
|
||||||
@media (max-width: 999px) {
|
@media (max-width: 999px) {
|
||||||
.item-layout-container {
|
/* Sidebar itself: cap width to viewport, always right-anchored */
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
width: 100%;
|
|
||||||
padding: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.item-layout-container .item-main-content {
|
|
||||||
order: 1;
|
|
||||||
/* Main content (video) on top */
|
|
||||||
/* padding: 10px; */
|
|
||||||
}
|
|
||||||
|
|
||||||
.item-sidebar-left {
|
|
||||||
order: 2;
|
|
||||||
/* Comments/Tags on bottom */
|
|
||||||
/* padding: 10px; */
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Mobile Overrides if necessary — most styles now universal */
|
|
||||||
.item-sidebar-right,
|
.item-sidebar-right,
|
||||||
.index-sidebar-right,
|
.index-sidebar-right,
|
||||||
.global-sidebar-right {
|
.global-sidebar-right {
|
||||||
width: 280px !important;
|
width: min(280px, 80vw) !important;
|
||||||
|
right: 0 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
body.sidebar-right-hidden .item-sidebar-right,
|
body.sidebar-right-hidden .item-sidebar-right,
|
||||||
body.sidebar-right-hidden .index-sidebar-right,
|
body.sidebar-right-hidden .index-sidebar-right,
|
||||||
body.sidebar-right-hidden .global-sidebar-right {
|
body.sidebar-right-hidden .global-sidebar-right {
|
||||||
right: -280px !important;
|
right: calc(-1 * min(280px, 80vw)) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Mobile: sidebar is an overlay, so pagination always spans full viewport */
|
/* Mobile: pagination centered in available space (left of sidebar) */
|
||||||
.pagination-container-fluid,
|
.pagination-container-fluid {
|
||||||
|
right: min(280px, 80vw) !important;
|
||||||
|
transition: right 0.3s ease-in-out !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sidebar hidden: pagination expands to full viewport width */
|
||||||
body.sidebar-right-hidden .pagination-container-fluid {
|
body.sidebar-right-hidden .pagination-container-fluid {
|
||||||
right: 0 !important;
|
right: 0 !important;
|
||||||
}
|
}
|
||||||
@@ -2166,6 +2156,99 @@ body.layout-modern .global-sidebar-right {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 600–999px: sidebar is attached to the grid, same as desktop but with mobile width.
|
||||||
|
Grid reserves space for the sidebar so content is never hidden behind it. */
|
||||||
|
@media (min-width: 600px) and (max-width: 999px) {
|
||||||
|
.index-layout-wrapper {
|
||||||
|
padding-right: min(280px, 80vw);
|
||||||
|
}
|
||||||
|
|
||||||
|
body.sidebar-right-hidden .index-layout-wrapper {
|
||||||
|
padding-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Generic pagewrapper (non-index, non-item pages) also reserves space */
|
||||||
|
.pagewrapper:not(:has(.index-layout-wrapper)):not(:has(.item-layout-container)) {
|
||||||
|
padding-right: min(280px, 80vw);
|
||||||
|
}
|
||||||
|
|
||||||
|
body.sidebar-right-hidden
|
||||||
|
.pagewrapper:not(:has(.index-layout-wrapper)):not(:has(.item-layout-container)) {
|
||||||
|
padding-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Restore layout-modern item stacking at 600–999px.
|
||||||
|
Its CSS grid only activates at ≥1000px; without flex-column here
|
||||||
|
the layout falls back to broken block flow. */
|
||||||
|
body.layout-modern .item-layout-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 100%;
|
||||||
|
padding: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.layout-modern .item-layout-container .item-main-content {
|
||||||
|
order: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.layout-modern .item-sidebar-left {
|
||||||
|
order: 2;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Legacy layout: 2-column grid with mobile sidebar width. */
|
||||||
|
body.layout-legacy .item-layout-container {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1fr) min(280px, 80vw);
|
||||||
|
height: auto !important;
|
||||||
|
overflow: visible !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.layout-legacy.sidebar-right-hidden .item-layout-container {
|
||||||
|
grid-template-columns: minmax(0, 1fr) 0px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Very small screens (≤599px): sidebar is always a pure overlay — never reserve space for it.
|
||||||
|
Wrappers stay at full width regardless of sidebar state; no transitions needed here.
|
||||||
|
Item view also switches to stacked layout here (not at 999px). */
|
||||||
|
@media (max-width: 599px) {
|
||||||
|
/* Item view: stack content above comments/sidebar-left */
|
||||||
|
.item-layout-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 100%;
|
||||||
|
padding: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-layout-container .item-main-content {
|
||||||
|
order: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-sidebar-left {
|
||||||
|
order: 2;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.index-layout-wrapper,
|
||||||
|
.item-layout-container,
|
||||||
|
.pagewrapper,
|
||||||
|
body.sidebar-right-hidden .index-layout-wrapper,
|
||||||
|
body.sidebar-right-hidden .item-layout-container,
|
||||||
|
body.sidebar-right-hidden .pagewrapper {
|
||||||
|
padding-right: 5px !important;
|
||||||
|
transition: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-container-fluid,
|
||||||
|
body.sidebar-right-hidden .pagination-container-fluid {
|
||||||
|
right: 0 !important;
|
||||||
|
transition: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
.item-layout-container .item-main-content>._204863,
|
.item-layout-container .item-main-content>._204863,
|
||||||
.item-layout-container .item-main-content>.item_title,
|
.item-layout-container .item-main-content>.item_title,
|
||||||
@@ -2958,7 +3041,8 @@ body.layout-legacy .scroll-nav-wrapper {
|
|||||||
right: -45px;
|
right: -45px;
|
||||||
width: 40px;
|
width: 40px;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
z-index: 1000;
|
z-index: 0;
|
||||||
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
body.layout-legacy .scroll-to-bottom {
|
body.layout-legacy .scroll-to-bottom {
|
||||||
@@ -3064,7 +3148,7 @@ body.layout-legacy .scroll-to-bottom svg {
|
|||||||
color: #888;
|
color: #888;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: flex-end;
|
align-items: flex-start;
|
||||||
line-height: normal;
|
line-height: normal;
|
||||||
flex-wrap: nowrap;
|
flex-wrap: nowrap;
|
||||||
gap: 2px 10px;
|
gap: 2px 10px;
|
||||||
@@ -3072,8 +3156,9 @@ body.layout-legacy .scroll-to-bottom svg {
|
|||||||
|
|
||||||
.comment-header-left {
|
.comment-header-left {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 5px;
|
gap: 2px 6px;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
flex-shrink: 1;
|
flex-shrink: 1;
|
||||||
}
|
}
|
||||||
@@ -3196,9 +3281,13 @@ body.layout-legacy .scroll-to-bottom svg {
|
|||||||
.comment-author {
|
.comment-author {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
margin-right: 8px;
|
margin-right: 2px;
|
||||||
padding-right: 0 !important;
|
padding-right: 0 !important;
|
||||||
word-break: break-word;
|
white-space: nowrap;
|
||||||
|
flex-shrink: 0;
|
||||||
|
max-width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
.comment-time {
|
.comment-time {
|
||||||
@@ -3256,6 +3345,8 @@ body.layout-legacy .scroll-to-bottom svg {
|
|||||||
font-size: 0.9em;
|
font-size: 0.9em;
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
vertical-align: top;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.comment-context-link:hover,
|
.comment-context-link:hover,
|
||||||
@@ -3319,10 +3410,7 @@ body.layout-legacy .scroll-to-bottom svg {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.comment-backlinks {
|
.comment-backlinks {
|
||||||
display: inline-block;
|
display: contents;
|
||||||
font-size: 0.85em;
|
|
||||||
opacity: 0.8;
|
|
||||||
margin-left: 10px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.comments-header-actions {
|
.comments-header-actions {
|
||||||
@@ -3619,6 +3707,7 @@ html[theme="f0ck95"] img.avatar {
|
|||||||
html[theme="f0ck95"] ._204863 {
|
html[theme="f0ck95"] ._204863 {
|
||||||
content: " ";
|
content: " ";
|
||||||
background: -webkit-linear-gradient(left, #08216b, #a5cef7);
|
background: -webkit-linear-gradient(left, #08216b, #a5cef7);
|
||||||
|
background: linear-gradient(90deg, #08216b, #a5cef7);
|
||||||
text-align: left;
|
text-align: left;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
color: white;
|
color: white;
|
||||||
@@ -3628,6 +3717,31 @@ html[theme="f0ck95"] ._204863 {
|
|||||||
border-right: outset 2px silver;
|
border-right: outset 2px silver;
|
||||||
border-bottom: outset 2px silver;
|
border-bottom: outset 2px silver;
|
||||||
line-height: 1.7;
|
line-height: 1.7;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 2px 4px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
html[theme="f0ck95"] ._204863 .location {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
html[theme="f0ck95"] ._204863 .location::before {
|
||||||
|
content: "";
|
||||||
|
display: inline-block;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
background-image: var(--favicon-url, url('/s/img/favicon.gif'));
|
||||||
|
background-size: contain;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: center;
|
||||||
|
image-rendering: pixelated;
|
||||||
|
image-rendering: crisp-edges;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
html[theme="f0ck95"] .iconset#a_delete {
|
html[theme="f0ck95"] .iconset#a_delete {
|
||||||
@@ -3739,8 +3853,8 @@ html[theme='f0ck95d'] {
|
|||||||
|
|
||||||
html[theme="f0ck95d"] .item_title {
|
html[theme="f0ck95d"] .item_title {
|
||||||
background: black;
|
background: black;
|
||||||
border-left: inset 3px silver;
|
border-left: 2px solid gray;
|
||||||
border-right: outset 3px silver;
|
border-right: 2px solid gray;
|
||||||
}
|
}
|
||||||
|
|
||||||
html[theme="f0ck95d"] .err {
|
html[theme="f0ck95d"] .err {
|
||||||
@@ -3755,15 +3869,35 @@ html[theme="f0ck95d"] ._error_topbar {
|
|||||||
html[theme="f0ck95d"] ._204863 {
|
html[theme="f0ck95d"] ._204863 {
|
||||||
content: " ";
|
content: " ";
|
||||||
background: -webkit-linear-gradient(left, #0e111a, #073c71);
|
background: -webkit-linear-gradient(left, #0e111a, #073c71);
|
||||||
|
background: linear-gradient(90deg, #0e111a, #073c71);
|
||||||
text-align: left;
|
text-align: left;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
color: white;
|
color: white;
|
||||||
height: auto;
|
height: auto;
|
||||||
border-left: inset 2px silver;
|
border: 2px solid gray;
|
||||||
border-top: inset 2px silver;
|
|
||||||
border-right: outset 2px silver;
|
|
||||||
border-bottom: outset 2px silver;
|
|
||||||
line-height: 1.7;
|
line-height: 1.7;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0px 5px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
html[theme="f0ck95d"] ._204863 .location {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
html[theme="f0ck95d"] ._204863 .location::before {
|
||||||
|
content: "";
|
||||||
|
display: inline-block;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
background-image: var(--favicon-url, url('/s/img/favicon.gif'));
|
||||||
|
background-size: contain;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: center;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
html[theme="f0ck95d"] .embed-responsive-image {
|
html[theme="f0ck95d"] .embed-responsive-image {
|
||||||
@@ -3772,15 +3906,13 @@ html[theme="f0ck95d"] .embed-responsive-image {
|
|||||||
}
|
}
|
||||||
|
|
||||||
html[theme="f0ck95d"] .media-object {
|
html[theme="f0ck95d"] .media-object {
|
||||||
border-left: inset 3px silver;
|
border-left: 2px solid gray;
|
||||||
border-right: outset 3px silver;
|
border-right: 2px solid gray;
|
||||||
|
background: black;
|
||||||
|
border-bottom: 2px solid gray;
|
||||||
}
|
}
|
||||||
|
|
||||||
html[theme="f0ck95d"] .embed-responsive.embed-responsive-16by9 {
|
|
||||||
background: black;
|
|
||||||
border-left: inset 1px silver;
|
|
||||||
border-right: outset 1px silver;
|
|
||||||
}
|
|
||||||
|
|
||||||
html[theme="f0ck95d"] .embed-responsive-image {
|
html[theme="f0ck95d"] .embed-responsive-image {
|
||||||
border: none;
|
border: none;
|
||||||
@@ -3896,6 +4028,11 @@ html[theme="f0ck95d"] .admin-search button {
|
|||||||
color: black;
|
color: black;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
html[theme="f0ck95d"] .embed-responsive.embed-responsive-16by9 {
|
||||||
|
background: black;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
/* fullscreen */
|
/* fullscreen */
|
||||||
html[res="fullscreen"] ._204863 {
|
html[res="fullscreen"] ._204863 {
|
||||||
border-top: none;
|
border-top: none;
|
||||||
@@ -4488,16 +4625,17 @@ span.placeholder {
|
|||||||
transition: right 0.3s ease-in-out;
|
transition: right 0.3s ease-in-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Sidebar hidden — reclaim the full width */
|
|
||||||
body.sidebar-right-hidden .pagination-container-fluid {
|
|
||||||
right: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Modern layout: wider sidebar needs wider offset */
|
/* Modern layout: wider sidebar needs wider offset */
|
||||||
body.layout-modern .pagination-container-fluid {
|
body.layout-modern .pagination-container-fluid {
|
||||||
right: 400px;
|
right: 400px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Sidebar hidden — reclaim the full width */
|
||||||
|
body.sidebar-right-hidden .pagination-container-fluid,
|
||||||
|
body.layout-modern.sidebar-right-hidden .pagination-container-fluid {
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.bottom-pagination.fixed-pagination {
|
.bottom-pagination.fixed-pagination {
|
||||||
margin: 10px auto;
|
margin: 10px auto;
|
||||||
pointer-events: auto;
|
pointer-events: auto;
|
||||||
@@ -4506,7 +4644,6 @@ body.layout-modern .pagination-container-fluid {
|
|||||||
-webkit-backdrop-filter: blur(12px);
|
-webkit-backdrop-filter: blur(12px);
|
||||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.4);
|
|
||||||
padding: 2px;
|
padding: 2px;
|
||||||
max-width: fit-content;
|
max-width: fit-content;
|
||||||
}
|
}
|
||||||
@@ -4719,7 +4856,7 @@ body.layout-modern .pagination-container-fluid {
|
|||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 1200px) {
|
@media (min-width: 1000px) {
|
||||||
|
|
||||||
/* Reserve space for the fixed sidebar so content doesn't flow behind it */
|
/* Reserve space for the fixed sidebar so content doesn't flow behind it */
|
||||||
.index-layout-wrapper {
|
.index-layout-wrapper {
|
||||||
@@ -7196,6 +7333,11 @@ span#favs {
|
|||||||
border-top: 1px solid var(--nav-border-color);
|
border-top: 1px solid var(--nav-border-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
body.layout-modern span#favs {
|
||||||
|
border-top: none;
|
||||||
|
}
|
||||||
|
|
||||||
span#favs[hidden] {
|
span#favs[hidden] {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
@@ -7702,6 +7844,7 @@ div.posts>a.thumb > p {
|
|||||||
bottom: 5px;
|
bottom: 5px;
|
||||||
left: 5px;
|
left: 5px;
|
||||||
gap: 2px;
|
gap: 2px;
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Make individual item entry subtler and faster for infinite scroll */
|
/* Make individual item entry subtler and faster for infinite scroll */
|
||||||
@@ -9495,77 +9638,16 @@ body.layout-legacy .comment-content img.emoji {
|
|||||||
max-width: 200px;
|
max-width: 200px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes flashGreen {
|
|
||||||
0% {
|
|
||||||
background-color: rgba(var(--accent-rgb), 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
100% {
|
|
||||||
background-color: var(--item-bg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes newItemFlash {
|
|
||||||
0% {
|
|
||||||
border-left-color: var(--accent);
|
|
||||||
background: rgba(var(--accent-rgb), 0.15);
|
|
||||||
}
|
|
||||||
|
|
||||||
60% {
|
|
||||||
border-left-color: var(--accent);
|
|
||||||
background: rgba(var(--accent-rgb), 0.05);
|
|
||||||
}
|
|
||||||
|
|
||||||
100% {
|
|
||||||
border-left-color: transparent;
|
|
||||||
background: transparent;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.new-item-fade {
|
.new-item-fade {
|
||||||
animation: newItemFlash 2.5s ease forwards;
|
box-shadow: inset 3px 0px 0px var(--accent);
|
||||||
border-left: 3px solid transparent;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-activity .new-item-fade {
|
.sidebar-activity .new-item-fade {
|
||||||
border-left: none;
|
box-shadow: none;
|
||||||
animation: none;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Comment posted by the current user — fades in + accent border that auto-disappears */
|
|
||||||
@keyframes commentEnter {
|
|
||||||
0% {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(6px);
|
|
||||||
border-left-color: var(--accent);
|
|
||||||
background: rgba(var(--accent-rgb), 0.15);
|
|
||||||
}
|
|
||||||
|
|
||||||
20% {
|
|
||||||
opacity: 1;
|
|
||||||
transform: translateY(0);
|
|
||||||
border-left-color: var(--accent);
|
|
||||||
background: rgba(var(--accent-rgb), 0.15);
|
|
||||||
}
|
|
||||||
|
|
||||||
60% {
|
|
||||||
background: rgba(var(--accent-rgb), 0.05);
|
|
||||||
}
|
|
||||||
|
|
||||||
70% {
|
|
||||||
border-left-color: var(--accent);
|
|
||||||
background: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
100% {
|
|
||||||
border-left-color: transparent;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.comment-entering {
|
|
||||||
animation: commentEnter 7.5s ease forwards;
|
|
||||||
border-left: 3px solid transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
.comment-highlighted {
|
.comment-highlighted {
|
||||||
box-shadow: inset 3px 0px 0px var(--accent) !important;
|
box-shadow: inset 3px 0px 0px var(--accent) !important;
|
||||||
@@ -9747,6 +9829,20 @@ html[theme="f0ck95d"] .badge-dark {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* At 600–999px: center the button in the free space left of the sidebar.
|
||||||
|
Shift left by half the sidebar width so transform:translateX(-50%) lands
|
||||||
|
at the center of the available area rather than the full viewport. */
|
||||||
|
@media (min-width: 600px) and (max-width: 999px) {
|
||||||
|
.mobile-scroll-to-top {
|
||||||
|
left: calc(50% - min(140px, 40vw));
|
||||||
|
transition: left 0.3s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.sidebar-right-hidden .mobile-scroll-to-top {
|
||||||
|
left: 50%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.steuerung.steuerung-icon .fa-solid {
|
.steuerung.steuerung-icon .fa-solid {
|
||||||
font-size: 0.85em;
|
font-size: 0.85em;
|
||||||
}
|
}
|
||||||
@@ -9888,14 +9984,6 @@ html[theme="orange"] .image-brand {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
html[theme="f0ck95d"] .embed-responsive.embed-responsive-16by9 {
|
|
||||||
border-bottom: outset 2px silver;
|
|
||||||
}
|
|
||||||
|
|
||||||
html[theme="f0ck95"] .embed-responsive.embed-responsive-16by9 {
|
|
||||||
border-bottom: outset 2px silver;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Fade in video on page load */
|
/* Fade in video on page load */
|
||||||
|
|
||||||
@keyframes fadeIn {
|
@keyframes fadeIn {
|
||||||
@@ -9923,7 +10011,7 @@ html[theme="f0ck95"] .embed-responsive.embed-responsive-16by9 {
|
|||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 1200px) {
|
@media (min-width: 1000px) {
|
||||||
.pagewrapper {
|
.pagewrapper {
|
||||||
padding-right: 300px;
|
padding-right: 300px;
|
||||||
}
|
}
|
||||||
@@ -9941,7 +10029,7 @@ html[theme="f0ck95"] .embed-responsive.embed-responsive-16by9 {
|
|||||||
Since Index and Item pages have their own layout wrappers that handle space reservation,
|
Since Index and Item pages have their own layout wrappers that handle space reservation,
|
||||||
we disable the generic pagewrapper padding on those pages to avoid "double padding" or broken expansion.
|
we disable the generic pagewrapper padding on those pages to avoid "double padding" or broken expansion.
|
||||||
*/
|
*/
|
||||||
@media (min-width: 1200px) {
|
@media (min-width: 1000px) {
|
||||||
|
|
||||||
.pagewrapper:has(.index-layout-wrapper),
|
.pagewrapper:has(.index-layout-wrapper),
|
||||||
.pagewrapper:has(.item-layout-container),
|
.pagewrapper:has(.item-layout-container),
|
||||||
@@ -10090,7 +10178,7 @@ ol {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 20px;
|
gap: 20px;
|
||||||
padding: 0 15px;
|
padding: 0 5px;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
}
|
}
|
||||||
@@ -10207,7 +10295,7 @@ ol {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* ── Mobile: ≤768px ── */
|
/* ── Mobile: ≤768px ── */
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 800px) {
|
||||||
|
|
||||||
/* Show hamburger */
|
/* Show hamburger */
|
||||||
.navbar-toggler {
|
.navbar-toggler {
|
||||||
@@ -11070,8 +11158,7 @@ div.posts>a::after {
|
|||||||
|
|
||||||
|
|
||||||
.pin-indicator {
|
.pin-indicator {
|
||||||
position: absolute;
|
position: relative;
|
||||||
top: 5px;
|
|
||||||
left: 5px;
|
left: 5px;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
fill: var(--accent);
|
fill: var(--accent);
|
||||||
@@ -11393,11 +11480,12 @@ body.layout-legacy .navbar-header {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* layout-modern: restore sidebar right-padding AND block-flow for generic pages
|
/* layout-modern: restore sidebar right-padding AND block-flow for generic pages
|
||||||
(profile, settings, subscriptions, etc.) — identical behaviour to layout-legacy.
|
(profile, settings, subscriptions, tags, etc.) — identical behaviour to layout-legacy.
|
||||||
Specialized pages (index, item, meme) manage their own layout, so exclude them. */
|
Specialized pages (index, item, meme) manage their own layout, so exclude them.
|
||||||
@media (min-width: 1200px) {
|
Must be ≥1000px so the gap at 1000–1199px is covered. */
|
||||||
|
@media (min-width: 1000px) {
|
||||||
body.layout-modern>.pagewrapper:not(:has(.index-layout-wrapper)):not(:has(.item-layout-container)):not(:has(.meme-layout-wrapper)) {
|
body.layout-modern>.pagewrapper:not(:has(.index-layout-wrapper)):not(:has(.item-layout-container)):not(:has(.meme-layout-wrapper)) {
|
||||||
padding-right: 400px;
|
padding-right: 300px;
|
||||||
}
|
}
|
||||||
|
|
||||||
body.layout-modern.sidebar-right-hidden>.pagewrapper:not(:has(.index-layout-wrapper)):not(:has(.item-layout-container)):not(:has(.meme-layout-wrapper)) {
|
body.layout-modern.sidebar-right-hidden>.pagewrapper:not(:has(.index-layout-wrapper)):not(:has(.item-layout-container)):not(:has(.meme-layout-wrapper)) {
|
||||||
@@ -12966,10 +13054,8 @@ body.layout-modern .tag-ac-input {
|
|||||||
Full-height chat layout — conversation page
|
Full-height chat layout — conversation page
|
||||||
══════════════════════════════════════════════════════════ */
|
══════════════════════════════════════════════════════════ */
|
||||||
|
|
||||||
/* ── DESKTOP (> 768px): same pattern as the sidebar ───────
|
/* ── UNIVERSAL: position:fixed takes it out of flow entirely ───────
|
||||||
position:fixed takes it out of flow entirely — no pagewrapper
|
No pagewrapper padding, no body scroll involvement, just viewport-anchored. */
|
||||||
padding, no body scroll involvement, just viewport-anchored. */
|
|
||||||
@media (min-width: 769px) {
|
|
||||||
.messages-convo-page {
|
.messages-convo-page {
|
||||||
position: fixed !important;
|
position: fixed !important;
|
||||||
top: var(--navbar-h, 50px) !important;
|
top: var(--navbar-h, 50px) !important;
|
||||||
@@ -12982,26 +13068,21 @@ body.layout-modern .tag-ac-input {
|
|||||||
transition: right 0.3s ease-in-out;
|
transition: right 0.3s ease-in-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 1200px) {
|
/* Desktop: 300px reserved space (both layouts) */
|
||||||
|
@media (min-width: 1000px) {
|
||||||
body:not(.sidebar-right-hidden) .messages-convo-page {
|
body:not(.sidebar-right-hidden) .messages-convo-page {
|
||||||
right: 300px !important;
|
right: 300px !important;
|
||||||
}
|
}
|
||||||
body.layout-modern:not(.sidebar-right-hidden) .messages-convo-page {
|
}
|
||||||
right: 400px !important;
|
|
||||||
|
/* Attached Mobile: mobile sidebar width reserved space */
|
||||||
|
@media (min-width: 600px) and (max-width: 999px) {
|
||||||
|
body:not(.sidebar-right-hidden) .messages-convo-page {
|
||||||
|
right: min(280px, 80vw) !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Kill the layout-legacy pagewrapper min-height:100vh that causes body overflow */
|
/* Kill the pagewrapper min-height and overflow so body doesn't scroll behind it */
|
||||||
body:has(.messages-convo-page) .pagewrapper {
|
|
||||||
min-height: 0 !important;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ── MOBILE (≤ 768px): position:fixed pagewrapper ─────────
|
|
||||||
Works on mobile; locks the pagewrapper to the viewport. */
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
|
|
||||||
html:has(.messages-convo-page),
|
html:has(.messages-convo-page),
|
||||||
body:has(.messages-convo-page) {
|
body:has(.messages-convo-page) {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
@@ -13009,16 +13090,9 @@ body.layout-modern .tag-ac-input {
|
|||||||
}
|
}
|
||||||
|
|
||||||
body:has(.messages-convo-page) .pagewrapper {
|
body:has(.messages-convo-page) .pagewrapper {
|
||||||
position: fixed;
|
min-height: 0 !important;
|
||||||
top: var(--navbar-h, 50px);
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
padding: 0;
|
padding: 0 !important;
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Shared: convo pane is a flex column ──────────────────
|
/* ── Shared: convo pane is a flex column ──────────────────
|
||||||
@@ -13028,7 +13102,8 @@ body.layout-modern .tag-ac-input {
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
max-width: 720px;
|
max-width: 720px;
|
||||||
width: 100%;
|
/* Removed width: 100% here because it overconstrains the fixed element
|
||||||
|
against left:0 and right:300px, causing negative margins and overlap. */
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -16060,12 +16135,21 @@ body.scroller-active #gchat-reopen-bubble {
|
|||||||
color: #ccc;
|
color: #ccc;
|
||||||
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
|
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
|
||||||
font-size: 0.9em;
|
font-size: 0.9em;
|
||||||
|
word-break: break-word;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
}
|
}
|
||||||
|
|
||||||
.info-table td a {
|
.info-table td a {
|
||||||
color: var(--accent, #9f0);
|
color: var(--accent, #9f0);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
word-break: break-all;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
|
||||||
|
#info-modal .modal-content .info-table td a {
|
||||||
|
word-break: break-all;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
}
|
}
|
||||||
|
|
||||||
.info-table td a:hover {
|
.info-table td a:hover {
|
||||||
|
|||||||
@@ -13,8 +13,9 @@
|
|||||||
/* Meme Creator Styles */
|
/* Meme Creator Styles */
|
||||||
|
|
||||||
.meme-creator-container {
|
.meme-creator-container {
|
||||||
|
box-sizing: border-box !important;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
max-width: 1200px;
|
max-width: 1100px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
position: relative;
|
position: relative;
|
||||||
@@ -111,6 +112,7 @@
|
|||||||
|
|
||||||
.canvas-wrapper {
|
.canvas-wrapper {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
max-width: 650px;
|
||||||
min-width: 0; /* Allow shrinking */
|
min-width: 0; /* Allow shrinking */
|
||||||
background: #000;
|
background: #000;
|
||||||
border: 2px solid var(--nav-bg, #2b2b2b);
|
border: 2px solid var(--nav-bg, #2b2b2b);
|
||||||
@@ -238,22 +240,49 @@ canvas#memeCanvas {
|
|||||||
color: var(--white, #fff);
|
color: var(--white, #fff);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Mobile Stacking - Simple 2-Row Layout */
|
/* Mobile Stacking - Clean Flexbox Column Layout */
|
||||||
@media (max-width: 950px) {
|
@media (max-width: 950px) {
|
||||||
.meme-editor-layout {
|
.meme-editor-layout {
|
||||||
display: grid !important;
|
display: flex !important;
|
||||||
grid-template-columns: 1fr !important;
|
flex-direction: column !important;
|
||||||
grid-template-rows: 0.6fr auto !important;
|
align-items: center !important;
|
||||||
gap: 20px;
|
gap: 20px;
|
||||||
}
|
}
|
||||||
.meme-controls {
|
.meme-controls {
|
||||||
|
box-sizing: border-box !important;
|
||||||
width: 100% !important;
|
width: 100% !important;
|
||||||
grid-row: 2;
|
max-width: 500px !important;
|
||||||
overflow: visible !important;
|
overflow: visible !important;
|
||||||
}
|
}
|
||||||
.canvas-wrapper {
|
.canvas-wrapper {
|
||||||
|
box-sizing: border-box !important;
|
||||||
width: 100% !important;
|
width: 100% !important;
|
||||||
grid-row: 1;
|
max-width: 650px !important;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
overflow: visible !important;
|
||||||
|
height: auto !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Stack early when sidebar is open (sidebar is 300px, so 950 + 300 = 1250px)
|
||||||
|
This prevents the canvas from shrinking too much when the sidebar takes space. */
|
||||||
|
@media (max-width: 1250px) {
|
||||||
|
body:not(.sidebar-right-hidden) .meme-editor-layout {
|
||||||
|
display: flex !important;
|
||||||
|
flex-direction: column !important;
|
||||||
|
align-items: center !important;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
body:not(.sidebar-right-hidden) .meme-controls {
|
||||||
|
box-sizing: border-box !important;
|
||||||
|
width: 100% !important;
|
||||||
|
max-width: 500px !important;
|
||||||
|
overflow: visible !important;
|
||||||
|
}
|
||||||
|
body:not(.sidebar-right-hidden) .canvas-wrapper {
|
||||||
|
box-sizing: border-box !important;
|
||||||
|
width: 100% !important;
|
||||||
|
max-width: 650px !important;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
overflow: visible !important;
|
overflow: visible !important;
|
||||||
height: auto !important;
|
height: auto !important;
|
||||||
@@ -294,20 +323,19 @@ canvas#memeCanvas {
|
|||||||
|
|
||||||
/* Sidebar space reservation for meme pages */
|
/* Sidebar space reservation for meme pages */
|
||||||
.meme-layout-wrapper {
|
.meme-layout-wrapper {
|
||||||
|
box-sizing: border-box !important;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
transition: padding-right 0.3s ease-in-out;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 1200px) {
|
@media (min-width: 1000px) {
|
||||||
/* Reserve space for the fixed sidebar so content doesn't flow behind it */
|
/* Reserve space for the fixed sidebar so content doesn't flow behind it */
|
||||||
.meme-layout-wrapper {
|
body:not(.sidebar-right-hidden) .meme-layout-wrapper {
|
||||||
padding-right: 300px;
|
padding-right: 300px;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* Collapse reserved space when sidebar is hidden */
|
/* Collapse reserved space when sidebar is hidden */
|
||||||
body.sidebar-right-hidden .meme-layout-wrapper {
|
body.sidebar-right-hidden .meme-layout-wrapper {
|
||||||
padding-right: 0;
|
padding-right: 0 !important;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
5
public/s/img/ghost_fav.svg
Normal file
5
public/s/img/ghost_fav.svg
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" width="100" height="100">
|
||||||
|
<rect width="100" height="100" fill="#242526"/>
|
||||||
|
<path d="M50 22 C37 22 28 32 28 45 L28 72 L35 66 L42 72 L50 66 L58 72 L65 66 L72 72 L72 45 C72 32 63 22 50 22 Z" fill="#666666" opacity="0.6"/>
|
||||||
|
<text x="50" y="58" font-family="system-ui, -apple-system, sans-serif" font-weight="bold" font-size="36" fill="#ffffff" text-anchor="middle">?</text>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 443 B |
@@ -54,7 +54,7 @@ class CommentSystem {
|
|||||||
document.body.classList.remove('sidebar-left-hidden');
|
document.body.classList.remove('sidebar-left-hidden');
|
||||||
const layout = this.container.closest('.item-layout-container');
|
const layout = this.container.closest('.item-layout-container');
|
||||||
if (layout) layout.classList.remove('sidebar-hidden');
|
if (layout) layout.classList.remove('sidebar-hidden');
|
||||||
} else if (isHidden) {
|
} else if (isHidden && (window.innerWidth >= 1000 || !document.body.classList.contains('layout-modern'))) {
|
||||||
this.container.classList.add('faded-out');
|
this.container.classList.add('faded-out');
|
||||||
this.container.style.display = 'none';
|
this.container.style.display = 'none';
|
||||||
document.body.classList.add('sidebar-left-hidden');
|
document.body.classList.add('sidebar-left-hidden');
|
||||||
@@ -596,7 +596,7 @@ class CommentSystem {
|
|||||||
if (cached) cached.content = fullContent;
|
if (cached) cached.content = fullContent;
|
||||||
}
|
}
|
||||||
|
|
||||||
contentEl.dataset.raw = this.escapeHtml(fullContent);
|
contentEl.dataset.raw = fullContent;
|
||||||
contentEl.innerHTML = this.renderCommentContent(fullContent, commentId);
|
contentEl.innerHTML = this.renderCommentContent(fullContent, commentId);
|
||||||
CommentSystem.autoplayConvertedGifs(contentEl);
|
CommentSystem.autoplayConvertedGifs(contentEl);
|
||||||
CommentSystem.playEmojiVideos(contentEl);
|
CommentSystem.playEmojiVideos(contentEl);
|
||||||
@@ -622,6 +622,7 @@ class CommentSystem {
|
|||||||
|
|
||||||
const contentEl = el.querySelector('.comment-content');
|
const contentEl = el.querySelector('.comment-content');
|
||||||
if (contentEl) {
|
if (contentEl) {
|
||||||
|
contentEl.dataset.raw = data.content;
|
||||||
contentEl.innerHTML = this.renderCommentContent(data.content, data.comment_id);
|
contentEl.innerHTML = this.renderCommentContent(data.content, data.comment_id);
|
||||||
CommentSystem.autoplayConvertedGifs(contentEl);
|
CommentSystem.autoplayConvertedGifs(contentEl);
|
||||||
CommentSystem.playEmojiVideos(contentEl);
|
CommentSystem.playEmojiVideos(contentEl);
|
||||||
@@ -857,26 +858,14 @@ class CommentSystem {
|
|||||||
* border-right) is fully restored after the effect completes.
|
* border-right) is fully restored after the effect completes.
|
||||||
*
|
*
|
||||||
* @param {Element} el - The .comment element to animate.
|
* @param {Element} el - The .comment element to animate.
|
||||||
* @param {boolean} entering - True to also add 'comment-entering' (own post);
|
* @param {boolean} entering - Unused (kept for compatibility).
|
||||||
* false for 'new-item-fade' only (live others).
|
|
||||||
*/
|
*/
|
||||||
static _animateNewComment(el, entering = false) {
|
static _animateNewComment(el, entering = false) {
|
||||||
if (!el) return;
|
if (!el) return;
|
||||||
|
el.classList.add('new-item-fade');
|
||||||
const classes = entering
|
setTimeout(() => {
|
||||||
? ['comment-entering', 'new-item-fade']
|
el.classList.remove('new-item-fade');
|
||||||
: ['new-item-fade'];
|
}, 2500);
|
||||||
|
|
||||||
// Reset in case the element already has a stale animation
|
|
||||||
classes.forEach(c => el.classList.remove(c));
|
|
||||||
void el.offsetWidth; // force reflow so re-adding the class restarts the animation
|
|
||||||
|
|
||||||
classes.forEach(c => el.classList.add(c));
|
|
||||||
|
|
||||||
const cleanup = () => {
|
|
||||||
classes.forEach(c => el.classList.remove(c));
|
|
||||||
};
|
|
||||||
el.addEventListener('animationend', cleanup, { once: true });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -892,7 +881,6 @@ class CommentSystem {
|
|||||||
if (el) {
|
if (el) {
|
||||||
// Remove highlight from any previously highlighted or newly-posted comment
|
// Remove highlight from any previously highlighted or newly-posted comment
|
||||||
document.querySelectorAll('.comment-highlighted').forEach(c => c.classList.remove('comment-highlighted'));
|
document.querySelectorAll('.comment-highlighted').forEach(c => c.classList.remove('comment-highlighted'));
|
||||||
document.querySelectorAll('.comment-entering').forEach(c => c.classList.remove('comment-entering'));
|
|
||||||
document.querySelectorAll('.new-item-fade').forEach(c => c.classList.remove('new-item-fade'));
|
document.querySelectorAll('.new-item-fade').forEach(c => c.classList.remove('new-item-fade'));
|
||||||
|
|
||||||
// Always smooth-scroll so there's no jarring jump
|
// Always smooth-scroll so there's no jarring jump
|
||||||
@@ -1038,7 +1026,7 @@ class CommentSystem {
|
|||||||
preview.dataset.id = targetId;
|
preview.dataset.id = targetId;
|
||||||
|
|
||||||
// Remove temporary animation/highlight classes
|
// Remove temporary animation/highlight classes
|
||||||
preview.classList.remove('new-item-fade', 'comment-highlighted', 'comment-entering');
|
preview.classList.remove('new-item-fade', 'comment-highlighted');
|
||||||
|
|
||||||
// Remove input forms or action buttons
|
// Remove input forms or action buttons
|
||||||
const actions = preview.querySelector('.comment-actions');
|
const actions = preview.querySelector('.comment-actions');
|
||||||
@@ -1172,12 +1160,13 @@ class CommentSystem {
|
|||||||
const author = openerEl.dataset.display || openerEl.dataset.username || 'System';
|
const author = openerEl.dataset.display || openerEl.dataset.username || 'System';
|
||||||
const contentEl = body.querySelector('.comment-content');
|
const contentEl = body.querySelector('.comment-content');
|
||||||
if (contentEl) {
|
if (contentEl) {
|
||||||
const LINE_MAX = 200;
|
let rawText = (contentEl.dataset.raw || '').trim();
|
||||||
const rawText = (contentEl.dataset.raw || '').trim();
|
if (rawText.includes('>') || rawText.includes('<') || rawText.includes('&')) {
|
||||||
// Preserve all lines but cap any single line exceeding LINE_MAX chars
|
const txt = document.createElement('textarea');
|
||||||
const lines = rawText.split('\n').map(line =>
|
txt.innerHTML = rawText;
|
||||||
line.length > LINE_MAX ? line.substring(0, LINE_MAX) + '\u2026' : line
|
rawText = txt.value;
|
||||||
);
|
}
|
||||||
|
const lines = rawText.split('\n');
|
||||||
const quote = `>>${id} \n>${author}\n${lines.map(line => `>${line}`).join('\n')}\n`;
|
const quote = `>>${id} \n>${author}\n${lines.map(line => `>${line}`).join('\n')}\n`;
|
||||||
|
|
||||||
if (isNew) {
|
if (isNew) {
|
||||||
@@ -1632,18 +1621,26 @@ class CommentSystem {
|
|||||||
syncLevel(roots, list, false);
|
syncLevel(roots, list, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Maximum characters to fully render in the item view per comment
|
// Maximum characters & lines to fully render in the item view per comment
|
||||||
static get ITEM_VIEW_MAX_CHARS() { return 2000; }
|
static get ITEM_VIEW_MAX_CHARS() { return 500; }
|
||||||
|
static get ITEM_VIEW_MAX_LINES() { return 6; }
|
||||||
|
|
||||||
renderCommentContent(content, commentId = null, bypassTruncation = false) {
|
renderCommentContent(content, commentId = null, bypassTruncation = false) {
|
||||||
if (!content) return '';
|
if (!content) return '';
|
||||||
|
|
||||||
// Truncate extremely long comments before any processing
|
// Truncate long comments before any processing
|
||||||
let truncated = false;
|
let truncated = false;
|
||||||
if (!bypassTruncation && content.length > CommentSystem.ITEM_VIEW_MAX_CHARS) {
|
if (!bypassTruncation) {
|
||||||
content = content.substring(0, CommentSystem.ITEM_VIEW_MAX_CHARS) + '\u2026';
|
const lines = content.split('\n');
|
||||||
|
if (lines.length > CommentSystem.ITEM_VIEW_MAX_LINES) {
|
||||||
|
content = lines.slice(0, CommentSystem.ITEM_VIEW_MAX_LINES).join('\n');
|
||||||
truncated = true;
|
truncated = true;
|
||||||
}
|
}
|
||||||
|
if (content.length > CommentSystem.ITEM_VIEW_MAX_CHARS) {
|
||||||
|
content = content.substring(0, CommentSystem.ITEM_VIEW_MAX_CHARS);
|
||||||
|
truncated = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (typeof marked === 'undefined') {
|
if (typeof marked === 'undefined') {
|
||||||
console.warn('Marked.js not loaded, falling back to plain text');
|
console.warn('Marked.js not loaded, falling back to plain text');
|
||||||
@@ -1791,9 +1788,10 @@ class CommentSystem {
|
|||||||
if (trimmed.startsWith('>') && !trimmed.match(/^>>\d+/)) {
|
if (trimmed.startsWith('>') && !trimmed.match(/^>>\d+/)) {
|
||||||
const quoteContent = line.substring(line.indexOf('>') + 1);
|
const quoteContent = line.substring(line.indexOf('>') + 1);
|
||||||
const quoteEmojis = window.f0ckSession?.quote_emojis === true;
|
const quoteEmojis = window.f0ckSession?.quote_emojis === true;
|
||||||
|
const escapedQuote = quoteContent.replace(/>/g, '>');
|
||||||
const renderedContent = quoteEmojis
|
const renderedContent = quoteEmojis
|
||||||
? quoteContent.replace(/:([a-z0-9_]+):/g, (m, n) => this.renderEmoji(m, n))
|
? escapedQuote.replace(/:([a-z0-9_]+):/g, (m, n) => this.renderEmoji(m, n))
|
||||||
: quoteContent;
|
: escapedQuote;
|
||||||
return `<span class="greentext">>${renderedContent}</span>`;
|
return `<span class="greentext">>${renderedContent}</span>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2199,7 +2197,7 @@ class CommentSystem {
|
|||||||
if (!unsafe) return '';
|
if (!unsafe) return '';
|
||||||
const div = document.createElement('div');
|
const div = document.createElement('div');
|
||||||
div.textContent = unsafe;
|
div.textContent = unsafe;
|
||||||
return div.innerHTML;
|
return div.innerHTML.replace(/"/g, '"').replace(/'/g, ''');
|
||||||
}
|
}
|
||||||
|
|
||||||
renderCommentAttachments(files, content = '') {
|
renderCommentAttachments(files, content = '') {
|
||||||
@@ -3336,9 +3334,15 @@ class CommentSystem {
|
|||||||
params.append('has_poll', '1');
|
params.append('has_poll', '1');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const csrfToken = window.f0ckSession?.csrf_token || '';
|
||||||
|
if (csrfToken) params.append('csrf_token', csrfToken);
|
||||||
|
|
||||||
const res = await fetch('/api/comments', {
|
const res = await fetch('/api/comments', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
...(csrfToken ? { 'X-CSRF-Token': csrfToken } : {})
|
||||||
|
},
|
||||||
body: params
|
body: params
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -3604,9 +3608,9 @@ class CommentSystem {
|
|||||||
// element whenever the raw content exceeds ITEM_VIEW_MAX_CHARS. Called after every
|
// element whenever the raw content exceeds ITEM_VIEW_MAX_CHARS. Called after every
|
||||||
// optimistic DOM insert to guarantee the button regardless of rendering path.
|
// optimistic DOM insert to guarantee the button regardless of rendering path.
|
||||||
_ensureTruncationButton(commentEl, rawContent) {
|
_ensureTruncationButton(commentEl, rawContent) {
|
||||||
console.log('[ensureBtn] called, rawContent.length:', rawContent?.length, 'threshold:', CommentSystem.ITEM_VIEW_MAX_CHARS);
|
if (!rawContent) return;
|
||||||
if (!rawContent || rawContent.length <= CommentSystem.ITEM_VIEW_MAX_CHARS) {
|
const lineCount = rawContent.split('\n').length;
|
||||||
console.log('[ensureBtn] below threshold, skipping');
|
if (rawContent.length <= CommentSystem.ITEM_VIEW_MAX_CHARS && lineCount <= CommentSystem.ITEM_VIEW_MAX_LINES) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const contentEl = commentEl.querySelector('.comment-content');
|
const contentEl = commentEl.querySelector('.comment-content');
|
||||||
@@ -3808,10 +3812,13 @@ class CommentSystem {
|
|||||||
toggleComments() {
|
toggleComments() {
|
||||||
if (!this.container) return;
|
if (!this.container) return;
|
||||||
|
|
||||||
if (document.body.classList.contains('layout-modern') && typeof window.toggleSidebarLeft === 'function') {
|
if (document.body.classList.contains('layout-modern')) {
|
||||||
|
if (window.innerWidth < 1000) return;
|
||||||
|
if (typeof window.toggleSidebarLeft === 'function') {
|
||||||
window.toggleSidebarLeft();
|
window.toggleSidebarLeft();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const layout = this.container.closest('.item-layout-container');
|
const layout = this.container.closest('.item-layout-container');
|
||||||
const sidebar = this.container.closest('.item-sidebar-left');
|
const sidebar = this.container.closest('.item-sidebar-left');
|
||||||
@@ -4457,20 +4464,71 @@ class CommentSystem {
|
|||||||
}, { passive: true });
|
}, { passive: true });
|
||||||
};
|
};
|
||||||
|
|
||||||
// Helper: reliable touch handling for tab buttons on mobile
|
// Helper: reliable touch handling for tab buttons on mobile (supports hold-to-action)
|
||||||
const addTabTouch = (btn, cb) => {
|
let lastHoldTime = 0;
|
||||||
|
const addTabTouch = (btn, cb, onHold) => {
|
||||||
let ts = null;
|
let ts = null;
|
||||||
|
let holdTimer = null;
|
||||||
|
let holdFired = false;
|
||||||
btn.addEventListener('touchstart', (e) => {
|
btn.addEventListener('touchstart', (e) => {
|
||||||
|
holdFired = false;
|
||||||
ts = { x: e.touches[0].clientX, y: e.touches[0].clientY };
|
ts = { x: e.touches[0].clientX, y: e.touches[0].clientY };
|
||||||
|
if (onHold) {
|
||||||
|
holdTimer = setTimeout(() => {
|
||||||
|
holdFired = true;
|
||||||
|
lastHoldTime = Date.now();
|
||||||
|
try { navigator.vibrate?.(40); } catch(err) {}
|
||||||
|
onHold();
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
}, { passive: true });
|
}, { passive: true });
|
||||||
|
|
||||||
|
btn.addEventListener('touchmove', (e) => {
|
||||||
|
if (!ts) return;
|
||||||
|
const dx = Math.abs(e.touches[0].clientX - ts.x);
|
||||||
|
const dy = Math.abs(e.touches[0].clientY - ts.y);
|
||||||
|
if (dx > 10 || dy > 10) {
|
||||||
|
clearTimeout(holdTimer);
|
||||||
|
ts = null;
|
||||||
|
}
|
||||||
|
}, { passive: true });
|
||||||
|
|
||||||
btn.addEventListener('touchend', (e) => {
|
btn.addEventListener('touchend', (e) => {
|
||||||
|
clearTimeout(holdTimer);
|
||||||
|
if (holdFired) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
holdFired = false;
|
||||||
|
ts = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!ts) return;
|
if (!ts) return;
|
||||||
const dx = Math.abs(e.changedTouches[0].clientX - ts.x);
|
const dx = Math.abs(e.changedTouches[0].clientX - ts.x);
|
||||||
const dy = Math.abs(e.changedTouches[0].clientY - ts.y);
|
const dy = Math.abs(e.changedTouches[0].clientY - ts.y);
|
||||||
ts = null;
|
ts = null;
|
||||||
if (dx < 10 && dy < 10) { e.preventDefault(); cb(); }
|
if (dx < 10 && dy < 10) { e.preventDefault(); cb(); }
|
||||||
});
|
});
|
||||||
|
|
||||||
|
btn.addEventListener('touchcancel', () => {
|
||||||
|
clearTimeout(holdTimer);
|
||||||
|
holdFired = false;
|
||||||
|
ts = null;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleFavDefault = (tabEl) => {
|
||||||
|
const KEY = 'f0ck_emoji_default_tab';
|
||||||
|
const isDefault = localStorage.getItem(KEY) === '__favs__';
|
||||||
|
if (isDefault) {
|
||||||
|
localStorage.removeItem(KEY);
|
||||||
|
tabEl.classList.remove('ep-tab-default');
|
||||||
|
window._showEmojiToast?.('Favorites unset as default tab');
|
||||||
|
} else {
|
||||||
|
localStorage.setItem(KEY, '__favs__');
|
||||||
|
tabEl.classList.add('ep-tab-default');
|
||||||
|
window._showEmojiToast?.('Favorites set as default tab');
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Build tab buttons — Favorites first
|
// Build tab buttons — Favorites first
|
||||||
@@ -4482,23 +4540,13 @@ class CommentSystem {
|
|||||||
favTab.innerHTML = '<i class="fa-solid fa-star" style="color:var(--emoji-fav-color,#f5c518);font-size:1.1em;"></i>';
|
favTab.innerHTML = '<i class="fa-solid fa-star" style="color:var(--emoji-fav-color,#f5c518);font-size:1.1em;"></i>';
|
||||||
favTab.addEventListener('mousedown', e => e.preventDefault());
|
favTab.addEventListener('mousedown', e => e.preventDefault());
|
||||||
favTab.addEventListener('click', () => showTab('__favs__'));
|
favTab.addEventListener('click', () => showTab('__favs__'));
|
||||||
addTabTouch(favTab, () => showTab('__favs__'));
|
addTabTouch(favTab, () => showTab('__favs__'), () => toggleFavDefault(favTab));
|
||||||
// Right-click on fav tab → set/unset as default
|
// Right-click on fav tab → set/unset as default
|
||||||
favTab.addEventListener('contextmenu', (e) => {
|
favTab.addEventListener('contextmenu', (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
if (navigator.maxTouchPoints > 0) return;
|
if (Date.now() - lastHoldTime < 1000) return;
|
||||||
const KEY = 'f0ck_emoji_default_tab';
|
toggleFavDefault(favTab);
|
||||||
const isDefault = localStorage.getItem(KEY) === '__favs__';
|
|
||||||
if (isDefault) {
|
|
||||||
localStorage.removeItem(KEY);
|
|
||||||
favTab.classList.remove('ep-tab-default');
|
|
||||||
window._showEmojiToast?.('Favorites unset as default tab');
|
|
||||||
} else {
|
|
||||||
localStorage.setItem(KEY, '__favs__');
|
|
||||||
favTab.classList.add('ep-tab-default');
|
|
||||||
window._showEmojiToast?.('Favorites set as default tab');
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
// Mark as default if already set
|
// Mark as default if already set
|
||||||
if (localStorage.getItem('f0ck_emoji_default_tab') === '__favs__') {
|
if (localStorage.getItem('f0ck_emoji_default_tab') === '__favs__') {
|
||||||
|
|||||||
@@ -2307,9 +2307,49 @@ window.cancelAnimFrame = (function () {
|
|||||||
hidden = hiddenValue === 'true';
|
hidden = hiddenValue === 'true';
|
||||||
}
|
}
|
||||||
document.body.classList.toggle('sidebar-right-hidden', hidden);
|
document.body.classList.toggle('sidebar-right-hidden', hidden);
|
||||||
|
|
||||||
|
// Always force-hide on very small screens regardless of saved preference
|
||||||
|
if (window.innerWidth <= 599) {
|
||||||
|
document.body.classList.add('sidebar-right-hidden');
|
||||||
|
}
|
||||||
};
|
};
|
||||||
window.initSidebarRightToggle();
|
window.initSidebarRightToggle();
|
||||||
|
|
||||||
|
// Auto-hide sidebar on very small screens (≤599px), restore on wider
|
||||||
|
(() => {
|
||||||
|
const mq = window.matchMedia('(max-width: 599px)');
|
||||||
|
const applyBreakpoint = (e) => {
|
||||||
|
// Freeze transitions so the class change is instant (no sliding during resize)
|
||||||
|
const sidebar = document.querySelector('.global-sidebar-right, .item-sidebar-right, .index-sidebar-right');
|
||||||
|
const wrappers = document.querySelectorAll('.index-layout-wrapper, .item-layout-container, .pagination-container-fluid, .pagewrapper');
|
||||||
|
if (sidebar) sidebar.style.setProperty('transition', 'none', 'important');
|
||||||
|
wrappers.forEach(w => w.style.setProperty('transition', 'none', 'important'));
|
||||||
|
|
||||||
|
if (e.matches) {
|
||||||
|
// Entering narrow: force-hide without touching localStorage
|
||||||
|
document.body.classList.add('sidebar-right-hidden');
|
||||||
|
} else {
|
||||||
|
// Leaving narrow: restore from saved preference
|
||||||
|
const saved = localStorage.getItem('sidebarRightHidden');
|
||||||
|
const hidden = saved === 'true';
|
||||||
|
document.body.classList.toggle('sidebar-right-hidden', hidden);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Force reflow then restore transitions
|
||||||
|
if (sidebar) void sidebar.offsetWidth;
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
if (sidebar) sidebar.style.removeProperty('transition');
|
||||||
|
wrappers.forEach(w => {
|
||||||
|
w.style.removeProperty('transition');
|
||||||
|
w.style.removeProperty('padding-right');
|
||||||
|
w.style.removeProperty('right');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
mq.addEventListener('change', applyBreakpoint);
|
||||||
|
applyBreakpoint(mq); // run once on init
|
||||||
|
})();
|
||||||
|
|
||||||
const loadPageAjax = async (url, replace = true, options = {}) => {
|
const loadPageAjax = async (url, replace = true, options = {}) => {
|
||||||
if (isNavigating) return;
|
if (isNavigating) return;
|
||||||
|
|
||||||
@@ -4427,6 +4467,10 @@ window.cancelAnimFrame = (function () {
|
|||||||
if (window.renderTags) {
|
if (window.renderTags) {
|
||||||
window.renderTags(res.tags);
|
window.renderTags(res.tags);
|
||||||
}
|
}
|
||||||
|
// Evict the cached item HTML so navigating back fetches fresh content
|
||||||
|
if (window.invalidateItemCache) {
|
||||||
|
window.invalidateItemCache(postid);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
revert();
|
revert();
|
||||||
window.flashMessage('Error: ' + (res.msg || 'Failed to update rating'), 3000, 'error');
|
window.flashMessage('Error: ' + (res.msg || 'Failed to update rating'), 3000, 'error');
|
||||||
@@ -6277,8 +6321,10 @@ window.cancelAnimFrame = (function () {
|
|||||||
if (settled) return;
|
if (settled) return;
|
||||||
settled = true;
|
settled = true;
|
||||||
|
|
||||||
// Freeze all transitions (override CSS !important) so the class toggle
|
// Freeze sidebar AND wrapper transitions so the class toggle
|
||||||
// doesn't trigger an additional right-property animation.
|
// doesn't trigger a second padding-right animation.
|
||||||
|
const wrappers = getSidebarWrappers();
|
||||||
|
wrappers.forEach(w => w.style.setProperty('transition', 'none', 'important'));
|
||||||
sidebar.style.setProperty('transition', 'none', 'important');
|
sidebar.style.setProperty('transition', 'none', 'important');
|
||||||
|
|
||||||
if (stateChanging) {
|
if (stateChanging) {
|
||||||
@@ -6296,6 +6342,11 @@ window.cancelAnimFrame = (function () {
|
|||||||
// Restore CSS-controlled transitions on the next frame
|
// Restore CSS-controlled transitions on the next frame
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
sidebar.style.removeProperty('transition');
|
sidebar.style.removeProperty('transition');
|
||||||
|
wrappers.forEach(w => {
|
||||||
|
w.style.removeProperty('transition');
|
||||||
|
w.style.removeProperty('padding-right');
|
||||||
|
w.style.removeProperty('right');
|
||||||
|
});
|
||||||
swipeRT.isDraggingSidebar = false;
|
swipeRT.isDraggingSidebar = false;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -6533,6 +6584,10 @@ window.cancelAnimFrame = (function () {
|
|||||||
const settle = () => {
|
const settle = () => {
|
||||||
if (settled) return;
|
if (settled) return;
|
||||||
settled = true;
|
settled = true;
|
||||||
|
// Freeze sidebar AND wrapper transitions so the class toggle
|
||||||
|
// doesn't trigger a second padding-right animation.
|
||||||
|
const wrappers = getSidebarWrappers();
|
||||||
|
wrappers.forEach(w => w.style.setProperty('transition', 'none', 'important'));
|
||||||
sidebar.style.setProperty('transition', 'none', 'important');
|
sidebar.style.setProperty('transition', 'none', 'important');
|
||||||
|
|
||||||
if (stateChanging) {
|
if (stateChanging) {
|
||||||
@@ -6545,6 +6600,11 @@ window.cancelAnimFrame = (function () {
|
|||||||
sidebar.style.transform = '';
|
sidebar.style.transform = '';
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
sidebar.style.removeProperty('transition');
|
sidebar.style.removeProperty('transition');
|
||||||
|
wrappers.forEach(w => {
|
||||||
|
w.style.removeProperty('transition');
|
||||||
|
w.style.removeProperty('padding-right');
|
||||||
|
w.style.removeProperty('right');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -6612,15 +6672,55 @@ window.cancelAnimFrame = (function () {
|
|||||||
});
|
});
|
||||||
window.addEventListener('resize', syncEdgeZone, { passive: true });
|
window.addEventListener('resize', syncEdgeZone, { passive: true });
|
||||||
|
|
||||||
// Touchend on edgeZone: instant tap-to-toggle (no 300ms synthetic click delay)
|
// Returns true if (clientX, clientY) is within the pill's hit area.
|
||||||
|
// The pill is #sidebar-drag-zone::after — 4×40px, centered on pointer:fine,
|
||||||
|
// right-aligned with padding-right:6px on pointer:coarse.
|
||||||
|
const isWithinPillArea = (clientX, clientY) => {
|
||||||
|
const rect = edgeZone.getBoundingClientRect();
|
||||||
|
const pillH = 40;
|
||||||
|
const hitPad = 12; // generous padding so it's easy to hit
|
||||||
|
// Vertical: pill is always centered in the drag zone
|
||||||
|
const pillCenterY = rect.top + rect.height / 2;
|
||||||
|
if (clientY < pillCenterY - pillH / 2 - hitPad || clientY > pillCenterY + pillH / 2 + hitPad) return false;
|
||||||
|
// Horizontal: right-aligned on touch, centered on mouse
|
||||||
|
const isCoarse = window.matchMedia('(pointer: coarse)').matches;
|
||||||
|
const pillCenterX = isCoarse ? rect.right - 6 - 2 : rect.left + rect.width / 2;
|
||||||
|
return clientX >= pillCenterX - 2 - hitPad && clientX <= pillCenterX + 2 + hitPad;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Touchend: tap on the pill toggles sidebar; rest of drag zone only responds to swipe
|
||||||
edgeZone.addEventListener('touchend', e => {
|
edgeZone.addEventListener('touchend', e => {
|
||||||
// Only if it was a clean tap (no significant swipe movement)
|
|
||||||
if (Math.abs(swipeRT.xDiff || 0) < 15 && Math.abs(swipeRT.yDiff || 0) < 15) {
|
if (Math.abs(swipeRT.xDiff || 0) < 15 && Math.abs(swipeRT.yDiff || 0) < 15) {
|
||||||
e.preventDefault();
|
e.preventDefault(); // always prevent 300ms synthetic click
|
||||||
edgeZone.dispatchEvent(new Event('click'));
|
const touch = e.changedTouches[0];
|
||||||
|
if (touch && isWithinPillArea(touch.clientX, touch.clientY)) {
|
||||||
|
window.toggleSidebarRight();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, { passive: false });
|
}, { passive: false });
|
||||||
|
|
||||||
|
// Helper: all layout wrappers that have padding-right animated by sidebar state
|
||||||
|
const getSidebarWrappers = () => {
|
||||||
|
// At ≤999px: sidebar is a pure overlay — only pagination tracks it.
|
||||||
|
if (window.innerWidth <= 599) {
|
||||||
|
return document.querySelectorAll('.pagination-container-fluid');
|
||||||
|
}
|
||||||
|
// At ≥600px: exclude pagewrapper when a dedicated layout wrapper is present.
|
||||||
|
// CSS already zeroes pagewrapper.padding-right via :has on those pages,
|
||||||
|
// so including it here causes double-padding (pagewrapper + index-layout-wrapper both
|
||||||
|
// get 300px snapped, making the grid 600px too narrow, then jumping back on settle).
|
||||||
|
const hasDedicatedWrapper = !!(
|
||||||
|
document.querySelector('.index-layout-wrapper') ||
|
||||||
|
document.querySelector('.item-layout-container') ||
|
||||||
|
document.querySelector('.meme-layout-wrapper') ||
|
||||||
|
document.querySelector('.messages-convo-page')
|
||||||
|
);
|
||||||
|
const sel = hasDedicatedWrapper
|
||||||
|
? '.index-layout-wrapper, .meme-layout-wrapper, .messages-convo-page, .pagination-container-fluid'
|
||||||
|
: '.pagination-container-fluid, .pagewrapper';
|
||||||
|
return document.querySelectorAll(sel);
|
||||||
|
};
|
||||||
|
|
||||||
// Smoothly toggle sidebar open/closed
|
// Smoothly toggle sidebar open/closed
|
||||||
window.toggleSidebarRight = () => {
|
window.toggleSidebarRight = () => {
|
||||||
const sidebar = document.querySelector('.global-sidebar-right, .item-sidebar-right, .index-sidebar-right');
|
const sidebar = document.querySelector('.global-sidebar-right, .item-sidebar-right, .index-sidebar-right');
|
||||||
@@ -6630,6 +6730,29 @@ window.cancelAnimFrame = (function () {
|
|||||||
const sidebarWidth = sidebar.offsetWidth || 300;
|
const sidebarWidth = sidebar.offsetWidth || 300;
|
||||||
const targetTranslate = isHidden ? -sidebarWidth : sidebarWidth;
|
const targetTranslate = isHidden ? -sidebarWidth : sidebarWidth;
|
||||||
|
|
||||||
|
// At ≤599px the sidebar is a pure overlay — never animate wrapper padding
|
||||||
|
const animateWrappers = window.innerWidth > 599;
|
||||||
|
|
||||||
|
// Grid/pagewrapper: snap instantly in both directions (no animated reflow).
|
||||||
|
// Pagination always animates — it tracks sidebar position with no reflow cost.
|
||||||
|
const wrappers = getSidebarWrappers();
|
||||||
|
if (animateWrappers) {
|
||||||
|
wrappers.forEach(w => {
|
||||||
|
const isPagination = w.classList.contains('pagination-container-fluid');
|
||||||
|
const isMessages = w.classList.contains('messages-convo-page');
|
||||||
|
|
||||||
|
const prop = (isPagination || isMessages) ? 'right' : 'padding-right';
|
||||||
|
const targetVal = isHidden ? sidebarWidth + 'px' : '0px';
|
||||||
|
|
||||||
|
// Lightweight layouts (pagination, messages) animate smoothly.
|
||||||
|
// Heavy/reflow-intensive layouts snap instantly to avoid reflow lag.
|
||||||
|
const shouldAnimate = (isPagination || isMessages);
|
||||||
|
w.style.setProperty('transition', shouldAnimate ? `${prop} 0.3s ease-in-out` : 'none', 'important');
|
||||||
|
w.style.setProperty(prop, targetVal, 'important');
|
||||||
|
});
|
||||||
|
void sidebar.offsetWidth; // settle layout before sidebar transform begins
|
||||||
|
}
|
||||||
|
|
||||||
sidebar.style.setProperty('transition', 'transform 0.3s ease-in-out', 'important');
|
sidebar.style.setProperty('transition', 'transform 0.3s ease-in-out', 'important');
|
||||||
sidebar.style.transform = `translateX(${targetTranslate}px)`;
|
sidebar.style.transform = `translateX(${targetTranslate}px)`;
|
||||||
|
|
||||||
@@ -6637,13 +6760,22 @@ window.cancelAnimFrame = (function () {
|
|||||||
const settle = () => {
|
const settle = () => {
|
||||||
if (settled) return;
|
if (settled) return;
|
||||||
settled = true;
|
settled = true;
|
||||||
|
// Freeze wrapper transitions so the class commit doesn't re-trigger them
|
||||||
|
wrappers.forEach(w => w.style.setProperty('transition', 'none', 'important'));
|
||||||
sidebar.style.setProperty('transition', 'none', 'important');
|
sidebar.style.setProperty('transition', 'none', 'important');
|
||||||
const toHidden = !isHidden;
|
const toHidden = !isHidden;
|
||||||
document.body.classList.toggle('sidebar-right-hidden', toHidden);
|
document.body.classList.toggle('sidebar-right-hidden', toHidden);
|
||||||
localStorage.setItem('sidebarRightHidden', toHidden);
|
localStorage.setItem('sidebarRightHidden', toHidden);
|
||||||
void sidebar.offsetWidth;
|
void sidebar.offsetWidth;
|
||||||
sidebar.style.transform = '';
|
sidebar.style.transform = '';
|
||||||
requestAnimationFrame(() => sidebar.style.removeProperty('transition'));
|
requestAnimationFrame(() => {
|
||||||
|
sidebar.style.removeProperty('transition');
|
||||||
|
wrappers.forEach(w => {
|
||||||
|
w.style.removeProperty('transition');
|
||||||
|
w.style.removeProperty('padding-right');
|
||||||
|
w.style.removeProperty('right');
|
||||||
|
});
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const onEnd = ev => {
|
const onEnd = ev => {
|
||||||
@@ -6655,9 +6787,11 @@ window.cancelAnimFrame = (function () {
|
|||||||
setTimeout(settle, 350);
|
setTimeout(settle, 350);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Click on edgeZone: smoothly toggle sidebar open/closed
|
// Click on edgeZone: only toggle when the click lands on the pill
|
||||||
edgeZone.addEventListener('click', () => {
|
edgeZone.addEventListener('click', (e) => {
|
||||||
|
if (isWithinPillArea(e.clientX, e.clientY)) {
|
||||||
window.toggleSidebarRight();
|
window.toggleSidebarRight();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
// </mouse-sidebar-drag>
|
// </mouse-sidebar-drag>
|
||||||
|
|
||||||
@@ -6677,7 +6811,9 @@ window.cancelAnimFrame = (function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const isLSActive = () =>
|
const isLSActive = () =>
|
||||||
document.body.classList.contains('layout-modern') && !!document.querySelector('.item-sidebar-left');
|
document.body.classList.contains('layout-modern') &&
|
||||||
|
!!document.querySelector('.item-sidebar-left') &&
|
||||||
|
window.innerWidth >= 1000;
|
||||||
|
|
||||||
// Prepare sidebar for drag — keeps it in the CSS grid so overflow:hidden clips translateX.
|
// Prepare sidebar for drag — keeps it in the CSS grid so overflow:hidden clips translateX.
|
||||||
// OPEN: removes sidebar-hidden/display:none, places sidebar at translateX(-sw), reflows twice.
|
// OPEN: removes sidebar-hidden/display:none, places sidebar at translateX(-sw), reflows twice.
|
||||||
@@ -6936,6 +7072,7 @@ window.cancelAnimFrame = (function () {
|
|||||||
};
|
};
|
||||||
|
|
||||||
window.toggleSidebarLeft = () => {
|
window.toggleSidebarLeft = () => {
|
||||||
|
if (window.innerWidth < 1000) return;
|
||||||
const { sb, layout } = getLS();
|
const { sb, layout } = getLS();
|
||||||
if (!sb || !layout) return;
|
if (!sb || !layout) return;
|
||||||
const isHidden = layout.classList.contains('sidebar-hidden');
|
const isHidden = layout.classList.contains('sidebar-hidden');
|
||||||
@@ -7430,30 +7567,49 @@ class NotificationSystem {
|
|||||||
|
|
||||||
// Helper: apply a display name change to all relevant DOM nodes
|
// Helper: apply a display name change to all relevant DOM nodes
|
||||||
this.applyDisplayNameUpdate = (display_name, user) => {
|
this.applyDisplayNameUpdate = (display_name, user) => {
|
||||||
|
const isSelf = !user || (window.f0ckSession?.user && user.toLowerCase() === window.f0ckSession.user.toLowerCase());
|
||||||
|
|
||||||
// Capture old value before overwriting — needed for thumb matching below
|
// Capture old value before overwriting — needed for thumb matching below
|
||||||
const oldDisplayName = window.f0ckSession?.display_name || null;
|
const oldDisplayName = isSelf ? (window.f0ckSession?.display_name || null) : null;
|
||||||
if (window.f0ckSession) window.f0ckSession.display_name = display_name || null;
|
if (isSelf && window.f0ckSession) {
|
||||||
|
window.f0ckSession.display_name = display_name || null;
|
||||||
|
}
|
||||||
|
|
||||||
const userName = user || window.f0ckSession?.user;
|
const userName = user || window.f0ckSession?.user;
|
||||||
|
|
||||||
// Update ALL nav-display-name instances (handles the profile page having a duplicate ID)
|
// Update navbar display name only if this update is for the logged-in user
|
||||||
|
if (isSelf) {
|
||||||
document.querySelectorAll('#nav-display-name').forEach(nameSpan => {
|
document.querySelectorAll('#nav-display-name').forEach(nameSpan => {
|
||||||
// preserve the badges (Admin, Mod)
|
|
||||||
for (let i = nameSpan.childNodes.length - 1; i >= 0; i--) {
|
for (let i = nameSpan.childNodes.length - 1; i >= 0; i--) {
|
||||||
let node = nameSpan.childNodes[i];
|
let node = nameSpan.childNodes[i];
|
||||||
if (node.nodeType === Node.TEXT_NODE && node.nodeValue.trim().length > 0) {
|
if (node.nodeType === Node.TEXT_NODE && node.nodeValue.trim().length > 0) {
|
||||||
// Match and preserve any non-breaking spaces at the start (from admin/mod badges)
|
|
||||||
const match = node.nodeValue.match(new RegExp('^(\\s*)(.*)'));
|
const match = node.nodeValue.match(new RegExp('^(\\s*)(.*)'));
|
||||||
node.nodeValue = (match ? match[1] : '') + (display_name || userName || '');
|
node.nodeValue = (match ? match[1] : '') + (display_name || userName || '');
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update profile header display name if currently viewing this user's profile
|
||||||
|
const profileBox = document.getElementById('live-profile-preview-box');
|
||||||
|
if (profileBox && profileBox.dataset.bannerUser && userName && profileBox.dataset.bannerUser.toLowerCase() === userName.toLowerCase()) {
|
||||||
|
const profileNameSpan = document.getElementById('profile-display-name');
|
||||||
|
if (profileNameSpan) {
|
||||||
|
for (let i = profileNameSpan.childNodes.length - 1; i >= 0; i--) {
|
||||||
|
let node = profileNameSpan.childNodes[i];
|
||||||
|
if (node.nodeType === Node.TEXT_NODE && node.nodeValue.trim().length > 0) {
|
||||||
|
const match = node.nodeValue.match(new RegExp('^(\\s*)(.*)'));
|
||||||
|
node.nodeValue = (match ? match[1] : '') + (display_name || userName || '');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
const bracket = document.getElementById('username-bracket');
|
const bracket = document.getElementById('username-bracket');
|
||||||
if (bracket) {
|
if (bracket) {
|
||||||
bracket.style.display = display_name ? 'inline' : 'none';
|
bracket.style.display = display_name ? 'inline' : 'none';
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (userName) {
|
if (userName) {
|
||||||
const lowerHref = `/user/${userName.toLowerCase()}`;
|
const lowerHref = `/user/${userName.toLowerCase()}`;
|
||||||
@@ -7479,6 +7635,7 @@ class NotificationSystem {
|
|||||||
|
|
||||||
// Helper: apply a username color change to relevant DOM nodes
|
// Helper: apply a username color change to relevant DOM nodes
|
||||||
this.applyUsernameColorUpdate = (username_color, user) => {
|
this.applyUsernameColorUpdate = (username_color, user) => {
|
||||||
|
const isSelf = !user || (window.f0ckSession?.user && user.toLowerCase() === window.f0ckSession.user.toLowerCase());
|
||||||
const userName = user || window.f0ckSession?.user;
|
const userName = user || window.f0ckSession?.user;
|
||||||
if (!userName) return;
|
if (!userName) return;
|
||||||
|
|
||||||
@@ -7525,22 +7682,21 @@ class NotificationSystem {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Special case for settings page preview block
|
// Profile header display name color (if currently viewing this user's profile)
|
||||||
if (window.f0ckSession && userName === window.f0ckSession.user) {
|
const profileBox = document.getElementById('live-profile-preview-box');
|
||||||
const previewBlock = document.getElementById('live-profile-preview-box');
|
if (profileBox && profileBox.dataset.bannerUser && profileBox.dataset.bannerUser.toLowerCase() === userName.toLowerCase()) {
|
||||||
if (previewBlock) {
|
const profileNameSpan = document.getElementById('profile-display-name');
|
||||||
|
if (profileNameSpan) {
|
||||||
if (username_color) {
|
if (username_color) {
|
||||||
previewBlock.style.setProperty('--author-accent', username_color);
|
profileNameSpan.style.color = username_color;
|
||||||
previewBlock.style.setProperty('--author-border', username_color);
|
|
||||||
} else {
|
} else {
|
||||||
previewBlock.style.setProperty('--author-accent', 'var(--accent)');
|
profileNameSpan.style.removeProperty('color');
|
||||||
previewBlock.style.setProperty('--author-border', 'var(--accent)');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle the nav-display-name span elements (profile header and top navbar)
|
// Navbar display name color (only for current user)
|
||||||
if (window.f0ckSession && userName === window.f0ckSession.user) {
|
if (isSelf) {
|
||||||
document.querySelectorAll('#nav-display-name').forEach(el => {
|
document.querySelectorAll('#nav-display-name').forEach(el => {
|
||||||
if (username_color) {
|
if (username_color) {
|
||||||
el.style.color = username_color;
|
el.style.color = username_color;
|
||||||
@@ -7553,14 +7709,21 @@ class NotificationSystem {
|
|||||||
|
|
||||||
// Helper: apply an avatar update to relevant DOM nodes
|
// Helper: apply an avatar update to relevant DOM nodes
|
||||||
this.applyAvatarUpdate = (avatar_file, user) => {
|
this.applyAvatarUpdate = (avatar_file, user) => {
|
||||||
|
const isSelf = !user || (window.f0ckSession?.user && user.toLowerCase() === window.f0ckSession.user.toLowerCase());
|
||||||
const userName = user || window.f0ckSession?.user;
|
const userName = user || window.f0ckSession?.user;
|
||||||
if (!userName) return;
|
if (!userName) return;
|
||||||
|
|
||||||
const newSrc = avatar_file ? `/a/${avatar_file}?t=${Date.now()}` : '/a/default.png';
|
const newSrc = avatar_file ? `/a/${avatar_file}?t=${Date.now()}` : '/a/default.png';
|
||||||
|
|
||||||
// Update Nav Avatar & profile header Avatar (only for current user)
|
// Update Nav Avatar only for current user
|
||||||
if (window.f0ckSession && userName === window.f0ckSession.user) {
|
if (isSelf) {
|
||||||
document.querySelectorAll('.nav-avatar-img, .profile_head_avatar img').forEach(img => img.src = newSrc);
|
document.querySelectorAll('.nav-avatar-img').forEach(img => img.src = newSrc);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update profile header avatar if currently viewing this user's profile
|
||||||
|
const profileBox = document.getElementById('live-profile-preview-box');
|
||||||
|
if (profileBox && profileBox.dataset.bannerUser && profileBox.dataset.bannerUser.toLowerCase() === userName.toLowerCase()) {
|
||||||
|
document.querySelectorAll('.profile_head_avatar img').forEach(img => img.src = newSrc);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update globally (comments, sidebar, polls, rank, infoboxes, favs)
|
// Update globally (comments, sidebar, polls, rank, infoboxes, favs)
|
||||||
@@ -8605,6 +8768,21 @@ 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 a = document.createElement('a');
|
||||||
|
a.className = 'ghost-fav';
|
||||||
|
a.setAttribute('tooltip', 'Ghost Fav');
|
||||||
|
a.setAttribute('flow', 'up');
|
||||||
|
a.style.cursor = 'default';
|
||||||
|
|
||||||
|
const img = document.createElement('img');
|
||||||
|
img.src = '/s/img/ghost_fav.svg';
|
||||||
|
img.style.height = '32px';
|
||||||
|
img.style.width = '32px';
|
||||||
|
|
||||||
|
a.appendChild(img);
|
||||||
|
fragment.appendChild(a);
|
||||||
|
} else {
|
||||||
const avatarUrl = fav.avatar_file
|
const avatarUrl = fav.avatar_file
|
||||||
? `/a/${fav.avatar_file}`
|
? `/a/${fav.avatar_file}`
|
||||||
: (fav.avatar ? `/t/${fav.avatar}.webp` : '/a/default.png');
|
: (fav.avatar ? `/t/${fav.avatar}.webp` : '/a/default.png');
|
||||||
@@ -8622,6 +8800,7 @@ class NotificationSystem {
|
|||||||
|
|
||||||
a.appendChild(img);
|
a.appendChild(img);
|
||||||
fragment.appendChild(a);
|
fragment.appendChild(a);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
favsContainer.innerHTML = '';
|
favsContainer.innerHTML = '';
|
||||||
|
|||||||
@@ -1976,6 +1976,73 @@ if (window.__dmLoaded) {
|
|||||||
dmGridArea = document.createElement('div');
|
dmGridArea = document.createElement('div');
|
||||||
dmGridArea.className = 'emoji-picker-grid';
|
dmGridArea.className = 'emoji-picker-grid';
|
||||||
|
|
||||||
|
// Helper: reliable touch handling for tab buttons on mobile (supports hold-to-action)
|
||||||
|
let lastDmHoldTime = 0;
|
||||||
|
const addDmTabTouch = (btn, cb, onHold) => {
|
||||||
|
let ts = null;
|
||||||
|
let holdTimer = null;
|
||||||
|
let holdFired = false;
|
||||||
|
btn.addEventListener('touchstart', (e) => {
|
||||||
|
holdFired = false;
|
||||||
|
ts = { x: e.touches[0].clientX, y: e.touches[0].clientY };
|
||||||
|
if (onHold) {
|
||||||
|
holdTimer = setTimeout(() => {
|
||||||
|
holdFired = true;
|
||||||
|
lastDmHoldTime = Date.now();
|
||||||
|
try { navigator.vibrate?.(40); } catch(err) {}
|
||||||
|
onHold();
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
e.stopPropagation();
|
||||||
|
}, { passive: true });
|
||||||
|
|
||||||
|
btn.addEventListener('touchmove', (e) => {
|
||||||
|
if (!ts) return;
|
||||||
|
const dx = Math.abs(e.touches[0].clientX - ts.x);
|
||||||
|
const dy = Math.abs(e.touches[0].clientY - ts.y);
|
||||||
|
if (dx > 10 || dy > 10) {
|
||||||
|
clearTimeout(holdTimer);
|
||||||
|
ts = null;
|
||||||
|
}
|
||||||
|
}, { passive: true });
|
||||||
|
|
||||||
|
btn.addEventListener('touchend', (e) => {
|
||||||
|
clearTimeout(holdTimer);
|
||||||
|
if (holdFired) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
holdFired = false;
|
||||||
|
ts = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!ts) return;
|
||||||
|
const dx = Math.abs(e.changedTouches[0].clientX - ts.x);
|
||||||
|
const dy = Math.abs(e.changedTouches[0].clientY - ts.y);
|
||||||
|
ts = null;
|
||||||
|
if (dx < 10 && dy < 10) { e.preventDefault(); cb(); }
|
||||||
|
});
|
||||||
|
|
||||||
|
btn.addEventListener('touchcancel', () => {
|
||||||
|
clearTimeout(holdTimer);
|
||||||
|
holdFired = false;
|
||||||
|
ts = null;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleDmFavDefault = (tabEl) => {
|
||||||
|
const KEY = 'f0ck_emoji_default_tab';
|
||||||
|
const isDefault = localStorage.getItem(KEY) === '__favs__';
|
||||||
|
if (isDefault) {
|
||||||
|
localStorage.removeItem(KEY);
|
||||||
|
tabEl.classList.remove('ep-tab-default');
|
||||||
|
window._showEmojiToast?.('Favorites unset as default tab');
|
||||||
|
} else {
|
||||||
|
localStorage.setItem(KEY, '__favs__');
|
||||||
|
tabEl.classList.add('ep-tab-default');
|
||||||
|
window._showEmojiToast?.('Favorites set as default tab');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// ── Favorites tab ──
|
// ── Favorites tab ──
|
||||||
const favsTab = document.createElement('button');
|
const favsTab = document.createElement('button');
|
||||||
favsTab.className = 'ep-tab ep-tab-favs';
|
favsTab.className = 'ep-tab ep-tab-favs';
|
||||||
@@ -1985,22 +2052,13 @@ if (window.__dmLoaded) {
|
|||||||
favsTab.innerHTML = '<i class="fa-solid fa-star" style="font-size:14px;color:var(--emoji-fav-color,#f5c518);"></i>';
|
favsTab.innerHTML = '<i class="fa-solid fa-star" style="font-size:14px;color:var(--emoji-fav-color,#f5c518);"></i>';
|
||||||
favsTab.addEventListener('mousedown', e => e.preventDefault());
|
favsTab.addEventListener('mousedown', e => e.preventDefault());
|
||||||
favsTab.addEventListener('click', () => showDmTab('__favs__', packs));
|
favsTab.addEventListener('click', () => showDmTab('__favs__', packs));
|
||||||
|
addDmTabTouch(favsTab, () => showDmTab('__favs__', packs), () => toggleDmFavDefault(favsTab));
|
||||||
// Right-click on fav tab → set/unset as default
|
// Right-click on fav tab → set/unset as default
|
||||||
favsTab.addEventListener('contextmenu', (e) => {
|
favsTab.addEventListener('contextmenu', (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
if (navigator.maxTouchPoints > 0) return;
|
if (Date.now() - lastDmHoldTime < 1000) return;
|
||||||
const KEY = 'f0ck_emoji_default_tab';
|
toggleDmFavDefault(favsTab);
|
||||||
const isDefault = localStorage.getItem(KEY) === '__favs__';
|
|
||||||
if (isDefault) {
|
|
||||||
localStorage.removeItem(KEY);
|
|
||||||
favsTab.classList.remove('ep-tab-default');
|
|
||||||
window._showEmojiToast?.('Favorites unset as default tab');
|
|
||||||
} else {
|
|
||||||
localStorage.setItem(KEY, '__favs__');
|
|
||||||
favsTab.classList.add('ep-tab-default');
|
|
||||||
window._showEmojiToast?.('Favorites set as default tab');
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
// Mark as default if already set
|
// Mark as default if already set
|
||||||
if (localStorage.getItem('f0ck_emoji_default_tab') === '__favs__') {
|
if (localStorage.getItem('f0ck_emoji_default_tab') === '__favs__') {
|
||||||
@@ -2028,6 +2086,7 @@ if (window.__dmLoaded) {
|
|||||||
}
|
}
|
||||||
tab.addEventListener('mousedown', e => e.preventDefault());
|
tab.addEventListener('mousedown', e => e.preventDefault());
|
||||||
tab.addEventListener('click', () => showDmTab(pack.id ?? null, packs));
|
tab.addEventListener('click', () => showDmTab(pack.id ?? null, packs));
|
||||||
|
addDmTabTouch(tab, () => showDmTab(pack.id ?? null, packs));
|
||||||
dmTabBar.appendChild(tab);
|
dmTabBar.appendChild(tab);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1000,9 +1000,10 @@
|
|||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
const itemId = slide.dataset.id;
|
const itemId = slide.dataset.id;
|
||||||
try {
|
try {
|
||||||
|
const csrfToken = window.f0ckSession?.csrf_token || window.scrollerCsrf || '';
|
||||||
const resp = await fetch(`/api/v2/tags/${itemId}/${encodeURIComponent(tag)}`, {
|
const resp = await fetch(`/api/v2/tags/${itemId}/${encodeURIComponent(tag)}`, {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
headers: { 'x-csrf-token': window.scrollerCsrf || '' }
|
headers: { ...(csrfToken ? { 'X-CSRF-Token': csrfToken } : {}) }
|
||||||
});
|
});
|
||||||
const data = await resp.json();
|
const data = await resp.json();
|
||||||
if (data.success) pill.remove();
|
if (data.success) pill.remove();
|
||||||
@@ -1157,10 +1158,14 @@
|
|||||||
if (nowFaved) flashFav(slide);
|
if (nowFaved) flashFav(slide);
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
|
const csrfToken = window.f0ckSession?.csrf_token || window.scrollerCsrf || '';
|
||||||
const resp = await fetch('/api/v2/togglefav', {
|
const resp = await fetch('/api/v2/togglefav', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
headers: {
|
||||||
body: `postid=${id}`
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
...(csrfToken ? { 'X-CSRF-Token': csrfToken } : {})
|
||||||
|
},
|
||||||
|
body: `postid=${id}${csrfToken ? `&csrf_token=${encodeURIComponent(csrfToken)}` : ''}`
|
||||||
});
|
});
|
||||||
const data = await resp.json();
|
const data = await resp.json();
|
||||||
// Sync count to server truth (handles race conditions)
|
// Sync count to server truth (handles race conditions)
|
||||||
@@ -1582,9 +1587,10 @@
|
|||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
const itemId = slide.dataset.id;
|
const itemId = slide.dataset.id;
|
||||||
try {
|
try {
|
||||||
|
const csrfToken = window.f0ckSession?.csrf_token || window.scrollerCsrf || '';
|
||||||
const resp = await fetch(`/api/v2/tags/${itemId}/${encodeURIComponent(t)}`, {
|
const resp = await fetch(`/api/v2/tags/${itemId}/${encodeURIComponent(t)}`, {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
headers: { 'x-csrf-token': window.scrollerCsrf || '' }
|
headers: { ...(csrfToken ? { 'X-CSRF-Token': csrfToken } : {}) }
|
||||||
});
|
});
|
||||||
const data = await resp.json();
|
const data = await resp.json();
|
||||||
if (data.success) pill.remove();
|
if (data.success) pill.remove();
|
||||||
@@ -1850,11 +1856,12 @@
|
|||||||
else if (item.external_board === 'gif') rating = 'nsfw';
|
else if (item.external_board === 'gif') rating = 'nsfw';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const csrfToken = window.f0ckSession?.csrf_token || window.scrollerCsrf || '';
|
||||||
const resp = await fetch('/api/v2/scroller/rehost', {
|
const resp = await fetch('/api/v2/scroller/rehost', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/x-www-form-urlencoded',
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
'x-csrf-token': window.scrollerCsrf || ''
|
...(csrfToken ? { 'X-CSRF-Token': csrfToken } : {})
|
||||||
},
|
},
|
||||||
body: new URLSearchParams({
|
body: new URLSearchParams({
|
||||||
url: item.external_media_url || item.dest,
|
url: item.external_media_url || item.dest,
|
||||||
@@ -1966,9 +1973,10 @@
|
|||||||
showShareToast(info.toast);
|
showShareToast(info.toast);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const csrfToken = window.f0ckSession?.csrf_token || window.scrollerCsrf || '';
|
||||||
fetch(`/api/v2/tags/${id}/cycle-rating`, {
|
fetch(`/api/v2/tags/${id}/cycle-rating`, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
headers: { 'x-csrf-token': window.scrollerCsrf || '' }
|
headers: { ...(csrfToken ? { 'X-CSRF-Token': csrfToken } : {}) }
|
||||||
})
|
})
|
||||||
.then(r => r.json())
|
.then(r => r.json())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
@@ -2119,7 +2127,12 @@
|
|||||||
const contentEl = commentEl.querySelector('.comment-content');
|
const contentEl = commentEl.querySelector('.comment-content');
|
||||||
if (!contentEl) return;
|
if (!contentEl) return;
|
||||||
|
|
||||||
const raw = (contentEl.dataset.raw || '').replace(/<br\s*\/?>/gi, '\n').trim();
|
let raw = (contentEl.dataset.raw || '').replace(/<br\s*\/?>/gi, '\n').trim();
|
||||||
|
if (raw.includes('>') || raw.includes('<') || raw.includes('&')) {
|
||||||
|
const txt = document.createElement('textarea');
|
||||||
|
txt.innerHTML = raw;
|
||||||
|
raw = txt.value;
|
||||||
|
}
|
||||||
const lines = raw.split('\n');
|
const lines = raw.split('\n');
|
||||||
const quote = `>>${id}\n${lines.map(line => `>${line}`).join('\n')}\n`;
|
const quote = `>>${id}\n${lines.map(line => `>${line}`).join('\n')}\n`;
|
||||||
|
|
||||||
@@ -2787,11 +2800,16 @@
|
|||||||
if (!content || !commentsItemId || commentsPosting) return;
|
if (!content || !commentsItemId || commentsPosting) return;
|
||||||
commentsPosting = true; commentSendBtn.disabled = true;
|
commentsPosting = true; commentSendBtn.disabled = true;
|
||||||
try {
|
try {
|
||||||
|
const csrfToken = window.f0ckSession?.csrf_token || window.scrollerCsrf || '';
|
||||||
let postBody = `item_id=${commentsItemId}&content=${encodeURIComponent(content)}`;
|
let postBody = `item_id=${commentsItemId}&content=${encodeURIComponent(content)}`;
|
||||||
if (replyToCommentId) postBody += `&parent_id=${replyToCommentId}`;
|
if (replyToCommentId) postBody += `&parent_id=${replyToCommentId}`;
|
||||||
|
if (csrfToken) postBody += `&csrf_token=${encodeURIComponent(csrfToken)}`;
|
||||||
const resp = await fetch('/api/comments', {
|
const resp = await fetch('/api/comments', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
...(csrfToken ? { 'X-CSRF-Token': csrfToken } : {})
|
||||||
|
},
|
||||||
body: postBody
|
body: postBody
|
||||||
});
|
});
|
||||||
const data = await resp.json();
|
const data = await resp.json();
|
||||||
@@ -2899,10 +2917,14 @@
|
|||||||
if (addTagSendBtn) addTagSendBtn.disabled = true;
|
if (addTagSendBtn) addTagSendBtn.disabled = true;
|
||||||
closeSugg();
|
closeSugg();
|
||||||
try {
|
try {
|
||||||
|
const csrfToken = window.f0ckSession?.csrf_token || window.scrollerCsrf || '';
|
||||||
const resp = await fetch(`/api/v2/tags/${targetId}`, {
|
const resp = await fetch(`/api/v2/tags/${targetId}`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
headers: {
|
||||||
body: `tagname=${encodeURIComponent(tag)}`
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
...(csrfToken ? { 'X-CSRF-Token': csrfToken } : {})
|
||||||
|
},
|
||||||
|
body: `tagname=${encodeURIComponent(tag)}${csrfToken ? `&csrf_token=${encodeURIComponent(csrfToken)}` : ''}`
|
||||||
});
|
});
|
||||||
const data = await resp.json();
|
const data = await resp.json();
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
@@ -3658,7 +3680,11 @@
|
|||||||
if (sMarkAll) {
|
if (sMarkAll) {
|
||||||
sMarkAll.addEventListener('click', async () => {
|
sMarkAll.addEventListener('click', async () => {
|
||||||
try {
|
try {
|
||||||
await fetch('/api/notifications/read', { method: 'POST' });
|
const csrfToken = window.f0ckSession?.csrf_token || window.scrollerCsrf || '';
|
||||||
|
await fetch('/api/notifications/read', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { ...(csrfToken ? { 'X-CSRF-Token': csrfToken } : {}) }
|
||||||
|
});
|
||||||
updateScrollerNotifBadge(0);
|
updateScrollerNotifBadge(0);
|
||||||
sCachedNotifs = sCachedNotifs.map(n => ({ ...n, is_read: true }));
|
sCachedNotifs = sCachedNotifs.map(n => ({ ...n, is_read: true }));
|
||||||
updateScrollerTabBadges(sCachedNotifs);
|
updateScrollerTabBadges(sCachedNotifs);
|
||||||
@@ -3673,7 +3699,12 @@
|
|||||||
if (!item) return;
|
if (!item) return;
|
||||||
const nid = item.dataset.id;
|
const nid = item.dataset.id;
|
||||||
if (nid && item.classList.contains('unread')) {
|
if (nid && item.classList.contains('unread')) {
|
||||||
fetch(`/api/notifications/${nid}/read`, { method: 'POST', keepalive: true }).catch(() => {});
|
const csrfToken = window.f0ckSession?.csrf_token || window.scrollerCsrf || '';
|
||||||
|
fetch(`/api/notifications/${nid}/read`, {
|
||||||
|
method: 'POST',
|
||||||
|
keepalive: true,
|
||||||
|
headers: { ...(csrfToken ? { 'X-CSRF-Token': csrfToken } : {}) }
|
||||||
|
}).catch(() => {});
|
||||||
item.classList.remove('unread');
|
item.classList.remove('unread');
|
||||||
// Update cache
|
// Update cache
|
||||||
const cached = sCachedNotifs.find(n => String(n.id) === String(nid));
|
const cached = sCachedNotifs.find(n => String(n.id) === String(nid));
|
||||||
|
|||||||
@@ -1119,6 +1119,68 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const favsPrivateToggle = document.getElementById('favorites_private_toggle');
|
||||||
|
if (favsPrivateToggle) {
|
||||||
|
favsPrivateToggle.addEventListener('change', async () => {
|
||||||
|
const favorites_private = favsPrivateToggle.checked;
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/v2/settings/favorites_private', {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
'X-CSRF-Token': window.f0ckSession ? window.f0ckSession.csrf_token : ''
|
||||||
|
},
|
||||||
|
body: new URLSearchParams({ favorites_private })
|
||||||
|
});
|
||||||
|
const data = await res.json();
|
||||||
|
if (data.success) {
|
||||||
|
showStatus('Favorites privacy preference updated!', 'success');
|
||||||
|
if (window.f0ckSession) {
|
||||||
|
window.f0ckSession.favorites_private = favorites_private;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
alert(data.msg || 'Error saving preference');
|
||||||
|
favsPrivateToggle.checked = !favorites_private; // Revert
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Update Favorites Privacy error:', err);
|
||||||
|
alert('Connection error');
|
||||||
|
favsPrivateToggle.checked = !favorites_private; // Revert
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const hideFavBadgeToggle = document.getElementById('hide_fav_badge_toggle');
|
||||||
|
if (hideFavBadgeToggle) {
|
||||||
|
hideFavBadgeToggle.addEventListener('change', async function() {
|
||||||
|
const hide_fav_badge = hideFavBadgeToggle.checked;
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/v2/settings/hide_fav_badge', {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
'X-CSRF-Token': window.f0ckSession ? window.f0ckSession.csrf_token : ''
|
||||||
|
},
|
||||||
|
body: new URLSearchParams({ hide_fav_badge })
|
||||||
|
});
|
||||||
|
const data = await res.json();
|
||||||
|
if (data.success) {
|
||||||
|
showStatus('Hide favorite badge preference updated!', 'success');
|
||||||
|
if (window.f0ckSession) {
|
||||||
|
window.f0ckSession.hide_fav_badge = hide_fav_badge;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
alert(data.msg || 'Error saving preference');
|
||||||
|
hideFavBadgeToggle.checked = !hide_fav_badge;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Update Hide Fav Badge error:', err);
|
||||||
|
alert('Connection error');
|
||||||
|
hideFavBadgeToggle.checked = !hide_fav_badge;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// 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) {
|
||||||
|
|||||||
@@ -47,7 +47,7 @@
|
|||||||
if (!unsafe) return '';
|
if (!unsafe) return '';
|
||||||
const div = document.createElement('div');
|
const div = document.createElement('div');
|
||||||
div.textContent = unsafe;
|
div.textContent = unsafe;
|
||||||
return div.innerHTML;
|
return div.innerHTML.replace(/"/g, '"').replace(/'/g, ''');
|
||||||
};
|
};
|
||||||
|
|
||||||
const playSidebarEmojiVideos = (container) => {
|
const playSidebarEmojiVideos = (container) => {
|
||||||
@@ -153,12 +153,6 @@
|
|||||||
const renderCommentContent = (content, commentId = null, itemId = null) => {
|
const renderCommentContent = (content, commentId = null, itemId = null) => {
|
||||||
if (!content) return '';
|
if (!content) return '';
|
||||||
|
|
||||||
// Truncate extremely long comments before any processing to keep the sidebar
|
|
||||||
// fast and the DOM lean, regardless of markdown / regex complexity.
|
|
||||||
if (content.length > SIDEBAR_CONTENT_TRUNCATE) {
|
|
||||||
content = content.substring(0, SIDEBAR_CONTENT_TRUNCATE) + '\u2026';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof marked === 'undefined') {
|
if (typeof marked === 'undefined') {
|
||||||
return escapeHtml(content)
|
return escapeHtml(content)
|
||||||
.replace(/:([a-z0-9_]+):/g, (m, n) => renderEmoji(m, n));
|
.replace(/:([a-z0-9_]+):/g, (m, n) => renderEmoji(m, n));
|
||||||
@@ -268,9 +262,10 @@
|
|||||||
// Manual greentext handling — apply emoji if the user preference allows it
|
// Manual greentext handling — apply emoji if the user preference allows it
|
||||||
const quoteContent = line.substring(line.indexOf('>') + 1);
|
const quoteContent = line.substring(line.indexOf('>') + 1);
|
||||||
const quoteEmojis = window.f0ckSession?.quote_emojis === true;
|
const quoteEmojis = window.f0ckSession?.quote_emojis === true;
|
||||||
|
const escapedQuote = quoteContent.replace(/>/g, '>');
|
||||||
const rendered = quoteEmojis
|
const rendered = quoteEmojis
|
||||||
? quoteContent.replace(/:([a-z0-9_]+):/g, (m, n) => renderEmoji(m, n))
|
? escapedQuote.replace(/:([a-z0-9_]+):/g, (m, n) => renderEmoji(m, n))
|
||||||
: quoteContent;
|
: escapedQuote;
|
||||||
return `<span class="greentext">>${rendered}</span>`;
|
return `<span class="greentext">>${rendered}</span>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -306,7 +306,12 @@ if (!window.UserCommentSystem) {
|
|||||||
const trimmed = line.trimStart();
|
const trimmed = line.trimStart();
|
||||||
if (trimmed.startsWith('>') && !trimmed.match(/^>>\d+/)) {
|
if (trimmed.startsWith('>') && !trimmed.match(/^>>\d+/)) {
|
||||||
const quoteContent = line.substring(line.indexOf('>') + 1);
|
const quoteContent = line.substring(line.indexOf('>') + 1);
|
||||||
return `<span class="greentext">>${quoteContent}</span>`;
|
const quoteEmojis = window.f0ckSession?.quote_emojis === true;
|
||||||
|
const escapedQuote = quoteContent.replace(/>/g, '>');
|
||||||
|
const rendered = quoteEmojis
|
||||||
|
? escapedQuote.replace(/:([a-z0-9_]+):/g, (m, n) => this.renderEmoji(m, n))
|
||||||
|
: escapedQuote;
|
||||||
|
return `<span class="greentext">>${rendered}</span>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Per-line limit
|
// Per-line limit
|
||||||
@@ -450,7 +455,7 @@ if (!window.UserCommentSystem) {
|
|||||||
if (!unsafe) return '';
|
if (!unsafe) return '';
|
||||||
const div = document.createElement('div');
|
const div = document.createElement('div');
|
||||||
div.textContent = unsafe;
|
div.textContent = unsafe;
|
||||||
return div.innerHTML;
|
return div.innerHTML.replace(/"/g, '"').replace(/'/g, ''');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -460,4 +460,15 @@ export default new class {
|
|||||||
console.error(`[ERROR REF ${errId}] ${context}:`, err);
|
console.error(`[ERROR REF ${errId}] ${context}:`, err);
|
||||||
return `Internal Error. Reference: ${errId}`;
|
return `Internal Error. Reference: ${errId}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isOnionRequest(req) {
|
||||||
|
if (!req || !req.headers) return false;
|
||||||
|
const rawHost = req.headers['x-forwarded-host'] || req.headers['host'] || req.headers['x-forwarded-server'] || '';
|
||||||
|
if (!rawHost) return false;
|
||||||
|
const hostStr = Array.isArray(rawHost) ? rawHost[0] : String(rawHost);
|
||||||
|
const firstHost = hostStr.split(',')[0].trim();
|
||||||
|
const hostNoPort = firstHost.split(':')[0].trim().toLowerCase();
|
||||||
|
return hostNoPort.endsWith('.onion');
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -165,6 +165,10 @@
|
|||||||
"hide_item_ratings_hint": "Zeige keine SFW/NSFW-Bewertungsindikatoren bei Elementen auf der Hauptseite",
|
"hide_item_ratings_hint": "Zeige keine SFW/NSFW-Bewertungsindikatoren bei Elementen auf der Hauptseite",
|
||||||
"disable_swiping": "Wischen deaktivieren",
|
"disable_swiping": "Wischen deaktivieren",
|
||||||
"disable_swiping_hint": "Navigation per Wischen auf Mobilgeräten deaktivieren",
|
"disable_swiping_hint": "Navigation per Wischen auf Mobilgeräten deaktivieren",
|
||||||
|
"favorites_private": "Private Favoriten",
|
||||||
|
"favorites_private_hint": "Nur du und Administratoren können deine Favoritenliste sehen.",
|
||||||
|
"hide_fav_badge": "Favoriten-Badge-Avatar verbergen",
|
||||||
|
"hide_fav_badge_hint": "Zeigt auf Beitragsseiten stattdessen ein Geist-Icon ohne Profilverlinkung an",
|
||||||
"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",
|
||||||
@@ -535,6 +539,7 @@
|
|||||||
"subscribe_uploads_btn": "Benutzer für Uploads abonnieren",
|
"subscribe_uploads_btn": "Benutzer für Uploads abonnieren",
|
||||||
"no_uploads": "Keine Uploads gefunden",
|
"no_uploads": "Keine Uploads gefunden",
|
||||||
"no_favs": "Keine Favoriten",
|
"no_favs": "Keine Favoriten",
|
||||||
|
"private_favorites": "private Favoriten",
|
||||||
"back_to_profile": "Zurück zum Profil",
|
"back_to_profile": "Zurück zum Profil",
|
||||||
"ban_modal_title": "Benutzer sperren",
|
"ban_modal_title": "Benutzer sperren",
|
||||||
"ban_modal_reason": "Grund:",
|
"ban_modal_reason": "Grund:",
|
||||||
|
|||||||
@@ -165,6 +165,10 @@
|
|||||||
"hide_item_ratings_hint": "Don't show SFW/NSFW rating indicators on main page items",
|
"hide_item_ratings_hint": "Don't show SFW/NSFW rating indicators on main page items",
|
||||||
"disable_swiping": "Disable Swiping",
|
"disable_swiping": "Disable Swiping",
|
||||||
"disable_swiping_hint": "Disable swipe-to-navigate on mobile devices",
|
"disable_swiping_hint": "Disable swipe-to-navigate on mobile devices",
|
||||||
|
"favorites_private": "Private Favorites",
|
||||||
|
"favorites_private_hint": "Only you and administrators can view your favorites list.",
|
||||||
|
"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",
|
||||||
"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",
|
||||||
@@ -539,6 +543,7 @@
|
|||||||
"subscribe_uploads_btn": "Subscribe user to uploads",
|
"subscribe_uploads_btn": "Subscribe user to uploads",
|
||||||
"no_uploads": "no uploads found",
|
"no_uploads": "no uploads found",
|
||||||
"no_favs": "no favorites",
|
"no_favs": "no favorites",
|
||||||
|
"private_favorites": "private favorites",
|
||||||
"back_to_profile": "Back to Profile",
|
"back_to_profile": "Back to Profile",
|
||||||
"ban_modal_title": "Ban User",
|
"ban_modal_title": "Ban User",
|
||||||
"ban_modal_reason": "Reason:",
|
"ban_modal_reason": "Reason:",
|
||||||
|
|||||||
@@ -165,6 +165,10 @@
|
|||||||
"hide_item_ratings_hint": "Toon geen SFW/NSFW beoordelingsindicatoren op hoofdpagina items",
|
"hide_item_ratings_hint": "Toon geen SFW/NSFW beoordelingsindicatoren op hoofdpagina items",
|
||||||
"disable_swiping": "Swipen uitschakelen",
|
"disable_swiping": "Swipen uitschakelen",
|
||||||
"disable_swiping_hint": "Swipe-to-navigate uitschakelen op mobiele apparaten",
|
"disable_swiping_hint": "Swipe-to-navigate uitschakelen op mobiele apparaten",
|
||||||
|
"favorites_private": "Privé favorieten",
|
||||||
|
"favorites_private_hint": "Alleen jij en beheerders kunnen je favorietenlijst bekijken.",
|
||||||
|
"hide_fav_badge": "Favoriet-badge avatar verbergen",
|
||||||
|
"hide_fav_badge_hint": "Toon als een spook-icoon op berichtpagina's zonder koppeling naar je profiel",
|
||||||
"image_expand_on_click": "Afbeeldingen inline vergroten bij klikken",
|
"image_expand_on_click": "Afbeeldingen inline vergroten bij klikken",
|
||||||
"image_expand_on_click_hint": "In plaats van de scroll-zoom-modal te openen, wordt een afbeelding bij klikken vergroot tot volledige grootte binnen de pagina.",
|
"image_expand_on_click_hint": "In plaats van de scroll-zoom-modal te openen, wordt een afbeelding bij klikken vergroot tot volledige grootte binnen de pagina.",
|
||||||
"enable_bg_blur": "Achtergrondvervaging inschakelen",
|
"enable_bg_blur": "Achtergrondvervaging inschakelen",
|
||||||
@@ -535,6 +539,7 @@
|
|||||||
"subscribe_uploads_btn": "Gebruiker abonneren op uploads",
|
"subscribe_uploads_btn": "Gebruiker abonneren op uploads",
|
||||||
"no_uploads": "geen uploads gevonden",
|
"no_uploads": "geen uploads gevonden",
|
||||||
"no_favs": "geen favorieten",
|
"no_favs": "geen favorieten",
|
||||||
|
"private_favorites": "privé favorieten",
|
||||||
"back_to_profile": "Terug naar Profiel",
|
"back_to_profile": "Terug naar Profiel",
|
||||||
"ban_modal_title": "Gebruiker Bannen",
|
"ban_modal_title": "Gebruiker Bannen",
|
||||||
"ban_modal_reason": "Reden:",
|
"ban_modal_reason": "Reden:",
|
||||||
|
|||||||
@@ -165,6 +165,10 @@
|
|||||||
"hide_item_ratings_hint": "Zeige keine SFW/NSFW-Bewertungsindikatoren bei Elementen auf der Hauptseite",
|
"hide_item_ratings_hint": "Zeige keine SFW/NSFW-Bewertungsindikatoren bei Elementen auf der Hauptseite",
|
||||||
"disable_swiping": "Wischen deaktivieren",
|
"disable_swiping": "Wischen deaktivieren",
|
||||||
"disable_swiping_hint": "Deaktivieren der Wisch-Navigation auf Mobilgeräten",
|
"disable_swiping_hint": "Deaktivieren der Wisch-Navigation auf Mobilgeräten",
|
||||||
|
"favorites_private": "Private Favoriten",
|
||||||
|
"favorites_private_hint": "Nur du und Administratoren können deine Favoritenliste sehen.",
|
||||||
|
"hide_fav_badge": "Herzi-Badge-Avatar versteckeln",
|
||||||
|
"hide_fav_badge_hint": "Zeigt auf Beitragsseiten 1 geiles Geist-Icon an vong Anonymität her",
|
||||||
"image_expand_on_click": "Bildli beim Klickle inline uffblähe",
|
"image_expand_on_click": "Bildli beim Klickle inline uffblähe",
|
||||||
"image_expand_on_click_hint": "Anstatt dat Scroll-Zoom-Moped aufzumache, wird n Bild beim Klickle uff volle Größ im Bereich uffgepumpt.",
|
"image_expand_on_click_hint": "Anstatt dat Scroll-Zoom-Moped aufzumache, wird n Bild beim Klickle uff volle Größ im Bereich uffgepumpt.",
|
||||||
"enable_bg_blur": "Hintergrundunschärfe aktivieren",
|
"enable_bg_blur": "Hintergrundunschärfe aktivieren",
|
||||||
@@ -536,6 +540,7 @@
|
|||||||
"subscribe_uploads_btn": "Benutzer für Aufladierungen abonnieren",
|
"subscribe_uploads_btn": "Benutzer für Aufladierungen abonnieren",
|
||||||
"no_uploads": "keine Aufladierungen gefunden",
|
"no_uploads": "keine Aufladierungen gefunden",
|
||||||
"no_favs": "keine Favoriten",
|
"no_favs": "keine Favoriten",
|
||||||
|
"private_favorites": "private Favoriten",
|
||||||
"back_to_profile": "Zurück zum Profil",
|
"back_to_profile": "Zurück zum Profil",
|
||||||
"ban_modal_title": "Benutzer sperren",
|
"ban_modal_title": "Benutzer sperren",
|
||||||
"ban_modal_reason": "Grund:",
|
"ban_modal_reason": "Grund:",
|
||||||
|
|||||||
@@ -136,8 +136,60 @@ const xdScoreMeta = (score) => {
|
|||||||
return { tier: 5, label: 'xDDDDD+' };
|
return { tier: 5, label: 'xDDDDD+' };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
async function checkFavoritesAccess(rawUser, { session, user_id, is_admin } = {}) {
|
||||||
|
if (!rawUser) return { isPrivate: false, isAllowed: true };
|
||||||
|
const decodedUser = decodeURI(rawUser);
|
||||||
|
const targetUserRows = await db`
|
||||||
|
select u.id, u."user", u.admin, uo.favorites_private
|
||||||
|
from "user" u
|
||||||
|
left join user_options uo on uo.user_id = u.id
|
||||||
|
where u."user" ilike ${decodedUser} or u.login ilike ${decodedUser}
|
||||||
|
limit 1
|
||||||
|
`;
|
||||||
|
if (!targetUserRows.length || !targetUserRows[0].favorites_private) {
|
||||||
|
return { isPrivate: false, isAllowed: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
const targetUser = targetUserRows[0];
|
||||||
|
|
||||||
|
let reqUserId = user_id;
|
||||||
|
let reqIsAdmin = is_admin;
|
||||||
|
|
||||||
|
if (typeof session === 'object' && session !== null) {
|
||||||
|
if (reqUserId === undefined) reqUserId = session.id;
|
||||||
|
if (reqIsAdmin === undefined) reqIsAdmin = !!session.admin;
|
||||||
|
}
|
||||||
|
|
||||||
|
let isAllowed = false;
|
||||||
|
if (reqUserId) {
|
||||||
|
if (+reqUserId === +targetUser.id) {
|
||||||
|
isAllowed = true;
|
||||||
|
} else if (reqIsAdmin === true) {
|
||||||
|
isAllowed = true;
|
||||||
|
} else if (reqIsAdmin === undefined) {
|
||||||
|
const reqUserRows = await db`select admin from "user" where id = ${+reqUserId} limit 1`;
|
||||||
|
if (reqUserRows.length > 0 && reqUserRows[0].admin) {
|
||||||
|
isAllowed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { isPrivate: true, isAllowed };
|
||||||
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
getf0cks: async ({ user: rawUser, tag: rawTag, hall: rawHall, mime: rawMime, page, mode, ratings, fav, session, limit, strict, newer, exclude, user_id, random, userHall: rawUserHall, userHallOwner: rawUserHallOwner, minXdScore, tagger: rawTagger } = {}) => {
|
getf0cks: async ({ user: rawUser, tag: rawTag, hall: rawHall, mime: rawMime, page, mode, ratings, fav, session, limit, strict, newer, exclude, user_id, is_admin, random, userHall: rawUserHall, userHallOwner: rawUserHallOwner, minXdScore, tagger: rawTagger } = {}) => {
|
||||||
|
if (fav && rawUser) {
|
||||||
|
const { isPrivate, isAllowed } = await checkFavoritesAccess(rawUser, { session, user_id, is_admin });
|
||||||
|
if (isPrivate && !isAllowed) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
is_private: true,
|
||||||
|
message: "private favorites"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const user = rawUser ? lib.escapeLike(decodeURI(rawUser)) : null;
|
const user = rawUser ? lib.escapeLike(decodeURI(rawUser)) : null;
|
||||||
|
|
||||||
// --- title: prefix — search items.title instead of the tags table ---
|
// --- title: prefix — search items.title instead of the tags table ---
|
||||||
@@ -428,7 +480,18 @@ export default {
|
|||||||
view_mode: fav ? 'favs' : 'uploads'
|
view_mode: fav ? 'favs' : 'uploads'
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
getf0ck: async ({ user: rawUser, tag: rawTag, hall: rawHall, mime: rawMime, itemid: rawItemid, mode, ratings, session, strict, exclude, user_id, fav, random, userHall: rawUserHall, userHallOwner: rawUserHallOwner, lang } = {}) => {
|
getf0ck: async ({ user: rawUser, tag: rawTag, hall: rawHall, mime: rawMime, itemid: rawItemid, mode, ratings, session, strict, exclude, user_id, is_admin, fav, random, userHall: rawUserHall, userHallOwner: rawUserHallOwner, lang } = {}) => {
|
||||||
|
if (fav && rawUser) {
|
||||||
|
const { isPrivate, isAllowed } = await checkFavoritesAccess(rawUser, { session, user_id, is_admin });
|
||||||
|
if (isPrivate && !isAllowed) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
is_private: true,
|
||||||
|
message: "private favorites"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const user = rawUser ? lib.escapeLike(decodeURI(rawUser)) : null;
|
const user = rawUser ? lib.escapeLike(decodeURI(rawUser)) : null;
|
||||||
|
|
||||||
// --- title: prefix — search items.title instead of the tags table ---
|
// --- title: prefix — search items.title instead of the tags table ---
|
||||||
@@ -705,7 +768,7 @@ export default {
|
|||||||
link.suffix = '';
|
link.suffix = '';
|
||||||
}
|
}
|
||||||
const favorites = await db`
|
const favorites = await db`
|
||||||
select "user".user, "user_options".avatar, "user_options".avatar_file, "user_options".username_color, "user_options".display_name
|
select "user".user, "user_options".avatar, "user_options".avatar_file, "user_options".username_color, "user_options".display_name, "user_options".hide_fav_badge
|
||||||
from "favorites"
|
from "favorites"
|
||||||
left join "user" on "user".id = "favorites".user_id
|
left join "user" on "user".id = "favorites".user_id
|
||||||
left join "user_options" on "user_options".user_id = "favorites".user_id
|
left join "user_options" on "user_options".user_id = "favorites".user_id
|
||||||
@@ -884,7 +947,14 @@ export default {
|
|||||||
tmp
|
tmp
|
||||||
};
|
};
|
||||||
return data;
|
return data;
|
||||||
}, getRandom: async ({ user: rawUser, tag: rawTag, hall: rawHall, mime: rawMime, mode, ratings, fav, session, strict, exclude, userHall: rawUserHall, userHallOwner: rawUserHallOwner } = {}) => {
|
},
|
||||||
|
getRandom: async ({ user: rawUser, tag: rawTag, hall: rawHall, mime: rawMime, mode, ratings, fav, session, strict, exclude, user_id, is_admin, userHall: rawUserHall, userHallOwner: rawUserHallOwner } = {}) => {
|
||||||
|
if (fav && rawUser) {
|
||||||
|
const { isPrivate, isAllowed } = await checkFavoritesAccess(rawUser, { session, user_id, is_admin });
|
||||||
|
if (isPrivate && !isAllowed) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
const user = rawUser ? lib.escapeLike(decodeURI(rawUser)) : null;
|
const user = rawUser ? lib.escapeLike(decodeURI(rawUser)) : null;
|
||||||
const hall = rawHall || null;
|
const hall = rawHall || null;
|
||||||
|
|
||||||
|
|||||||
@@ -1428,8 +1428,8 @@ export default (router, tpl) => {
|
|||||||
const userId = newUser[0].id;
|
const userId = newUser[0].id;
|
||||||
|
|
||||||
await db`
|
await db`
|
||||||
INSERT INTO user_options (user_id, mode, theme, fullscreen, avatar, avatar_file, use_new_layout, disable_autoplay, disable_swiping)
|
INSERT INTO user_options (user_id, mode, theme, fullscreen, avatar, avatar_file, use_new_layout, disable_autoplay, disable_swiping, use_alternative_infobox)
|
||||||
VALUES (${userId}, 3, 'amoled', 0, NULL, 'default.png', ${getDefaultLayout() === 'modern'}, ${cfg.websrv.enable_autoplay === false}, ${cfg.websrv.enable_swiping === false})
|
VALUES (${userId}, 3, 'amoled', 0, NULL, 'default.png', ${getDefaultLayout() === 'modern'}, ${cfg.websrv.enable_autoplay === false}, ${cfg.websrv.enable_swiping === false}, ${cfg.websrv.user_alternative_infobox !== false})
|
||||||
`;
|
`;
|
||||||
|
|
||||||
await audit.log(req.session.id, 'admin_create_user', 'user', userId, {
|
await audit.log(req.session.id, 'admin_create_user', 'user', userId, {
|
||||||
|
|||||||
@@ -218,9 +218,10 @@ export default (router, tpl) => {
|
|||||||
mime: query.mime || (req.cookies.mime || null),
|
mime: query.mime || (req.cookies.mime || null),
|
||||||
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,
|
||||||
exclude: req.session ? (req.session.excluded_tags || []) : [],
|
|
||||||
user_id: req.session?.id,
|
user_id: req.session?.id,
|
||||||
|
is_admin: req.session?.admin,
|
||||||
|
exclude: req.session ? (req.session.excluded_tags || []) : [],
|
||||||
fav: query.fav === 'true',
|
fav: query.fav === 'true',
|
||||||
random: isRandom,
|
random: isRandom,
|
||||||
strict: query.strict === '1' || query.strict === 'true' || req.session?.strict_mode,
|
strict: query.strict === '1' || query.strict === 'true' || req.session?.strict_mode,
|
||||||
@@ -231,6 +232,19 @@ export default (router, tpl) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!data.success) {
|
if (!data.success) {
|
||||||
|
if (data.is_private) {
|
||||||
|
const { t: tErr } = createI18n(req.session?.language || req.lang || 'en');
|
||||||
|
const privateHtml = `<div class="private-favs-msg" style="padding: 30px; text-align: center; color: var(--text-muted); font-size: 1.1em;">${tErr('profile.private_favorites')}</div>`;
|
||||||
|
return res.reply({
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
success: false,
|
||||||
|
is_private: true,
|
||||||
|
html: privateHtml,
|
||||||
|
hasMore: false
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
return res.reply({
|
return res.reply({
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
|
|||||||
@@ -1066,7 +1066,7 @@ export default router => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const favs = await db`
|
const favs = await db`
|
||||||
select "user".user, "user_options".avatar, "user_options".avatar_file, "user_options".display_name, "user_options".username_color
|
select "user".user, "user_options".avatar, "user_options".avatar_file, "user_options".display_name, "user_options".username_color, "user_options".hide_fav_badge
|
||||||
from "favorites"
|
from "favorites"
|
||||||
left join "user" on "user".id = "favorites".user_id
|
left join "user" on "user".id = "favorites".user_id
|
||||||
left join "user_options" on "user_options".user_id = "favorites".user_id
|
left join "user_options" on "user_options".user_id = "favorites".user_id
|
||||||
|
|||||||
@@ -315,6 +315,44 @@ export default router => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Update Favorites Privacy preference
|
||||||
|
group.put(/\/favorites_private/, lib.loggedin, async (req, res) => {
|
||||||
|
const favorites_private = req.post.favorites_private === true || req.post.favorites_private === 'true';
|
||||||
|
|
||||||
|
try {
|
||||||
|
await db`
|
||||||
|
update user_options
|
||||||
|
set favorites_private = ${favorites_private}
|
||||||
|
where user_id = ${+req.session.id}
|
||||||
|
`;
|
||||||
|
// Sync session immediately
|
||||||
|
if (req.session) req.session.favorites_private = favorites_private;
|
||||||
|
return res.json({ success: true, favorites_private }, 200);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Update Favorites Privacy pref error:', e);
|
||||||
|
return res.json({ success: false, msg: 'Error updating preference' }, 500);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update Hide Fav Badge preference
|
||||||
|
group.put(/\/hide_fav_badge/, lib.loggedin, async (req, res) => {
|
||||||
|
const hide_fav_badge = req.post.hide_fav_badge === true || req.post.hide_fav_badge === 'true';
|
||||||
|
|
||||||
|
try {
|
||||||
|
await db`
|
||||||
|
update user_options
|
||||||
|
set hide_fav_badge = ${hide_fav_badge}
|
||||||
|
where user_id = ${+req.session.id}
|
||||||
|
`;
|
||||||
|
// Sync session immediately
|
||||||
|
if (req.session) req.session.hide_fav_badge = hide_fav_badge;
|
||||||
|
return res.json({ success: true, hide_fav_badge }, 200);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Update Hide Fav Badge 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;
|
||||||
|
|||||||
@@ -25,7 +25,8 @@ export default router => {
|
|||||||
|
|
||||||
group.post(/$/, lib.loggedin, async (req, res) => {
|
group.post(/$/, lib.loggedin, async (req, res) => {
|
||||||
// assign and/or create tag
|
// assign and/or create tag
|
||||||
if (!req.params.postid || !req.post.tagname) {
|
const rawTagname = req.post?.tagname || req.body?.tagname;
|
||||||
|
if (!req.params.postid || !rawTagname) {
|
||||||
return res.json({
|
return res.json({
|
||||||
success: false,
|
success: false,
|
||||||
msg: 'missing postid or tag'
|
msg: 'missing postid or tag'
|
||||||
@@ -33,7 +34,7 @@ export default router => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const postid = +req.params.postid;
|
const postid = +req.params.postid;
|
||||||
const tagname = req.post.tagname?.trim();
|
const tagname = rawTagname.trim();
|
||||||
const protectedTags = ['sfw', 'nsfw', 'nsfl'];
|
const protectedTags = ['sfw', 'nsfw', 'nsfl'];
|
||||||
|
|
||||||
if (protectedTags.includes(tagname.toLowerCase())) {
|
if (protectedTags.includes(tagname.toLowerCase())) {
|
||||||
|
|||||||
@@ -99,8 +99,9 @@ export default (router, tpl) => {
|
|||||||
ratings: ratingsArr,
|
ratings: ratingsArr,
|
||||||
mime: mime,
|
mime: mime,
|
||||||
fav: true,
|
fav: true,
|
||||||
session: !!req.session,
|
session: req.session,
|
||||||
user_id: req.session?.id,
|
user_id: req.session?.id,
|
||||||
|
is_admin: req.session?.admin,
|
||||||
random: isRandom
|
random: isRandom
|
||||||
});
|
});
|
||||||
if (favs && 'items' in favs) {
|
if (favs && 'items' in favs) {
|
||||||
@@ -236,8 +237,9 @@ export default (router, tpl) => {
|
|||||||
fav: req.params.mode == 'favs',
|
fav: req.params.mode == 'favs',
|
||||||
mode: req.mode,
|
mode: req.mode,
|
||||||
ratings: (() => { const r = req.cookies.ratings; return r ? decodeURIComponent(r).split(/[|,]/).filter(x => ['sfw','nsfw','nsfl','untagged'].includes(x)) : null; })(),
|
ratings: (() => { const r = req.cookies.ratings; return r ? decodeURIComponent(r).split(/[|,]/).filter(x => ['sfw','nsfw','nsfl','untagged'].includes(x)) : null; })(),
|
||||||
session: !!req.session,
|
session: req.session,
|
||||||
user_id: req.session?.id,
|
user_id: req.session?.id,
|
||||||
|
is_admin: req.session?.admin,
|
||||||
exclude: req.session ? (req.session.excluded_tags || []) : [],
|
exclude: req.session ? (req.session.excluded_tags || []) : [],
|
||||||
url: decodeURIComponent(req.url.pathname || req.url),
|
url: decodeURIComponent(req.url.pathname || req.url),
|
||||||
strict: !!(req.query?.strict || req.url.qs?.strict || req.session?.strict_mode),
|
strict: !!(req.query?.strict || req.url.qs?.strict || req.session?.strict_mode),
|
||||||
@@ -251,6 +253,20 @@ 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) {
|
||||||
|
const { t: tErr } = createI18n(req.session?.language || req.lang || 'en');
|
||||||
|
return res.reply({
|
||||||
|
code: 403,
|
||||||
|
body: tpl.render('error', {
|
||||||
|
message: tErr('profile.private_favorites'),
|
||||||
|
domain: cfg.main.url.domain,
|
||||||
|
tmp: null,
|
||||||
|
session: req.session ? { ...req.session } : false,
|
||||||
|
error_filter_hint: null,
|
||||||
|
error_filter_hint_link: null
|
||||||
|
}, req)
|
||||||
|
});
|
||||||
|
}
|
||||||
// For index/grid views with zero items (empty DB), render an empty grid instead of error
|
// For index/grid views with zero items (empty DB), render an empty grid instead of error
|
||||||
if (mode !== 'item') {
|
if (mode !== 'item') {
|
||||||
data.items = [];
|
data.items = [];
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ export default (router, tpl) => {
|
|||||||
return res.writeHead(200, { 'Content-Type': 'application/json' }).end(JSON.stringify({ success: false, msg: errorMsg }));
|
return res.writeHead(200, { 'Content-Type': 'application/json' }).end(JSON.stringify({ success: false, msg: errorMsg }));
|
||||||
}
|
}
|
||||||
return res.reply({
|
return res.reply({
|
||||||
body: tpl.render("register", { theme: req.cookies.theme ?? (cfg.websrv.theme || "f0ck"), error: errorMsg, registration_open: getRegistrationOpen() })
|
body: tpl.render("register", { theme: req.cookies?.theme ?? (cfg.websrv.theme || "f0ck"), error: errorMsg, registration_open: getRegistrationOpen() }, req)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,7 +58,7 @@ export default (router, tpl) => {
|
|||||||
return res.writeHead(200, { 'Content-Type': 'application/json' }).end(JSON.stringify({ success: false, msg }));
|
return res.writeHead(200, { 'Content-Type': 'application/json' }).end(JSON.stringify({ success: false, msg }));
|
||||||
}
|
}
|
||||||
return res.reply({
|
return res.reply({
|
||||||
body: tpl.render("register", { theme: req.cookies.theme ?? (cfg.websrv.theme || "f0ck"), error: msg, registration_open: getRegistrationOpen() })
|
body: tpl.render("register", { theme: req.cookies?.theme ?? (cfg.websrv.theme || "f0ck"), error: msg, registration_open: getRegistrationOpen() }, req)
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -67,7 +67,7 @@ export default (router, tpl) => {
|
|||||||
return res.writeHead(200, { 'Content-Type': 'application/json' }).end(JSON.stringify({ success: true, msg }));
|
return res.writeHead(200, { 'Content-Type': 'application/json' }).end(JSON.stringify({ success: true, msg }));
|
||||||
}
|
}
|
||||||
return res.reply({
|
return res.reply({
|
||||||
body: tpl.render("register", { theme: req.cookies.theme ?? (cfg.websrv.theme || "f0ck"), success: msg, registration_open: getRegistrationOpen() })
|
body: tpl.render("register", { theme: req.cookies?.theme ?? (cfg.websrv.theme || "f0ck"), success: msg, registration_open: getRegistrationOpen() }, req)
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -88,8 +88,9 @@ export default (router, tpl) => {
|
|||||||
return renderError("Passwords do not match.");
|
return renderError("Passwords do not match.");
|
||||||
}
|
}
|
||||||
|
|
||||||
// reCAPTCHA verification
|
// reCAPTCHA verification (bypassed for .onion requests as Google reCAPTCHA cannot validate .onion domains)
|
||||||
if (cfg.recaptcha?.enabled && cfg.recaptcha?.secret_key) {
|
const isOnion = lib.isOnionRequest(req);
|
||||||
|
if (!isOnion && cfg.recaptcha?.enabled && cfg.recaptcha?.secret_key) {
|
||||||
const rcToken = req.post['g-recaptcha-response'];
|
const rcToken = req.post['g-recaptcha-response'];
|
||||||
if (!rcToken) return renderError("Please complete the reCAPTCHA.");
|
if (!rcToken) return renderError("Please complete the reCAPTCHA.");
|
||||||
try {
|
try {
|
||||||
@@ -167,8 +168,8 @@ export default (router, tpl) => {
|
|||||||
const avatarFile = 'default.png';
|
const avatarFile = 'default.png';
|
||||||
|
|
||||||
await db`
|
await db`
|
||||||
insert into user_options (user_id, mode, theme, fullscreen, avatar, avatar_file, use_new_layout, disable_autoplay, disable_swiping)
|
insert into user_options (user_id, mode, theme, fullscreen, avatar, avatar_file, use_new_layout, disable_autoplay, disable_swiping, use_alternative_infobox)
|
||||||
values (${userId}, 3, 'amoled', 0, ${avatarId}, ${avatarFile}, ${getDefaultLayout() === 'modern'}, ${cfg.websrv.enable_autoplay === false}, ${cfg.websrv.enable_swiping === false})
|
values (${userId}, 3, 'amoled', 0, ${avatarId}, ${avatarFile}, ${getDefaultLayout() === 'modern'}, ${cfg.websrv.enable_autoplay === false}, ${cfg.websrv.enable_swiping === false}, ${cfg.websrv.user_alternative_infobox !== false})
|
||||||
`;
|
`;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(`[REGISTER] DB Error during user creation:`, err);
|
console.error(`[REGISTER] DB Error during user creation:`, err);
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ export default (router, tpl) => {
|
|||||||
if (req.headers['x-requested-with'] === 'XMLHttpRequest' || query.ajax) {
|
if (req.headers['x-requested-with'] === 'XMLHttpRequest' || query.ajax) {
|
||||||
return res.json({
|
return res.json({
|
||||||
success: true,
|
success: true,
|
||||||
html: tpl.render('tag-cards', { toptags: tags, session: (req.session && req.session.user) ? { ...req.session } : false }, req),
|
html: tpl.render('tag-cards', { toptags: tags, user: req.session?.user ? { user: req.session.user } : null, session: (req.session && req.session.user) ? { ...req.session } : false }, req),
|
||||||
currentPage: page,
|
currentPage: page,
|
||||||
hasMore: tags.length === TAGS_PER_PAGE
|
hasMore: tags.length === TAGS_PER_PAGE
|
||||||
});
|
});
|
||||||
@@ -128,6 +128,7 @@ export default (router, tpl) => {
|
|||||||
phrase,
|
phrase,
|
||||||
tmp: null,
|
tmp: null,
|
||||||
hidePagination: true,
|
hidePagination: true,
|
||||||
|
user: req.session?.user ? { user: req.session.user } : null,
|
||||||
session: (req.session && req.session.user) ? { ...req.session } : false,
|
session: (req.session && req.session.user) ? { ...req.session } : false,
|
||||||
page_meta: {
|
page_meta: {
|
||||||
title: 'Tags',
|
title: 'Tags',
|
||||||
|
|||||||
147
src/index.mjs
147
src/index.mjs
@@ -49,7 +49,7 @@ const gateOptions = {
|
|||||||
},
|
},
|
||||||
cloudflare_status: {
|
cloudflare_status: {
|
||||||
status: 'ok',
|
status: 'ok',
|
||||||
location: 'Frankfurt',
|
location: cfg.websrv.private_society_gate_location || 'Frankfurt',
|
||||||
name: 'Cloudflare',
|
name: 'Cloudflare',
|
||||||
status_text: 'Working',
|
status_text: 'Working',
|
||||||
},
|
},
|
||||||
@@ -79,11 +79,13 @@ const nginx502Fallback = `<html>
|
|||||||
</html>`;
|
</html>`;
|
||||||
|
|
||||||
// Login + Register modal injected before </body>
|
// Login + Register modal injected before </body>
|
||||||
// This string is built at startup so it can reference cfg.recaptcha values.
|
// Dynamic function so reCAPTCHA can be bypassed for .onion requests
|
||||||
const _rcEnabled = !!(cfg.recaptcha && cfg.recaptcha.enabled && cfg.recaptcha.site_key);
|
const _rcEnabled = !!(cfg.recaptcha && cfg.recaptcha.enabled && cfg.recaptcha.site_key);
|
||||||
const _rcSiteKey = (cfg.recaptcha && cfg.recaptcha.site_key) || '';
|
const _rcSiteKey = (cfg.recaptcha && cfg.recaptcha.site_key) || '';
|
||||||
|
|
||||||
const gateLoginInjection = `
|
function getGateLoginInjection(req) {
|
||||||
|
const rcEnabled = _rcEnabled && !lib.isOnionRequest(req);
|
||||||
|
return `
|
||||||
<div id="hot-corner" style="position:fixed;bottom:0;left:0;width:20px;height:20px;z-index:9999;"></div>
|
<div id="hot-corner" style="position:fixed;bottom:0;left:0;width:20px;height:20px;z-index:9999;"></div>
|
||||||
|
|
||||||
<div id="gate-modal" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,0.45);z-index:10000;align-items:center;justify-content:center;">
|
<div id="gate-modal" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,0.45);z-index:10000;align-items:center;justify-content:center;">
|
||||||
@@ -123,7 +125,7 @@ const gateLoginInjection = `
|
|||||||
<input type="text" name="token" placeholder="Invite token" autocomplete="off"
|
<input type="text" name="token" placeholder="Invite token" autocomplete="off"
|
||||||
style="background:white;color:black;border:1px solid #bbb;padding:7px 10px;width:100%;box-sizing:border-box;font-size:14px;font-family:inherit;" />
|
style="background:white;color:black;border:1px solid #bbb;padding:7px 10px;width:100%;box-sizing:border-box;font-size:14px;font-family:inherit;" />
|
||||||
<input type="text" name="email_confirm_field" style="display:none !important;" tabindex="-1" autocomplete="off" />
|
<input type="text" name="email_confirm_field" style="display:none !important;" tabindex="-1" autocomplete="off" />
|
||||||
${_rcEnabled ? '<div id="gate-recaptcha" style="margin:4px 0;transform-origin:left top;"></div>' : ''}
|
${rcEnabled ? '<div id="gate-recaptcha" style="margin:4px 0;transform-origin:left top;"></div>' : ''}
|
||||||
<button type="submit" id="gate-register-btn" style="background:#0051c3;color:white;border:none;padding:9px;font-weight:600;font-size:14px;cursor:pointer;font-family:inherit;"
|
<button type="submit" id="gate-register-btn" style="background:#0051c3;color:white;border:none;padding:9px;font-weight:600;font-size:14px;cursor:pointer;font-family:inherit;"
|
||||||
onmouseover="this.style.background='#003681'" onmouseout="if(!this.disabled)this.style.background='#0051c3'">Create account</button>
|
onmouseover="this.style.background='#003681'" onmouseout="if(!this.disabled)this.style.background='#0051c3'">Create account</button>
|
||||||
<p style="text-align:center;font-size:0.85em;margin:6px 0 0;color:#555;">
|
<p style="text-align:center;font-size:0.85em;margin:6px 0 0;color:#555;">
|
||||||
@@ -264,8 +266,9 @@ const gateLoginInjection = `
|
|||||||
if (_sb.length > 12) _sb = _sb.slice(-12);
|
if (_sb.length > 12) _sb = _sb.slice(-12);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
${_rcEnabled ? '<script src="https://www.google.com/recaptcha/api.js?onload=onRecaptchaGateReady&render=explicit" async defer><\/script>' : ''}
|
${rcEnabled ? '<script src="https://www.google.com/recaptcha/api.js?onload=onRecaptchaGateReady&render=explicit" async defer><\/script>' : ''}
|
||||||
`;
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// Text injected into the "What can I do?" section
|
// Text injected into the "What can I do?" section
|
||||||
@@ -282,11 +285,40 @@ try {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Called on every gated request — produces a fresh page with unique Ray ID + timestamp
|
// Called on every gated request — produces a fresh page with unique Ray ID + timestamp
|
||||||
// Accepts an optional request object to inject the visitor's real IP into the footer reveal.
|
// Accepts an optional request object to inject the visitor's real IP into the footer reveal and dynamic host into page text/title.
|
||||||
function buildGatePage(req) {
|
function buildGatePage(req) {
|
||||||
if (!_cfRender) return nginx502Fallback;
|
if (!_cfRender) return nginx502Fallback;
|
||||||
|
|
||||||
let html = _cfRender({ ...gateOptions, what_can_i_do: gateSignInButton });
|
let reqHost = null;
|
||||||
|
if (req && req.headers) {
|
||||||
|
const rawHost = req.headers['x-forwarded-host'] || req.headers['host'];
|
||||||
|
if (rawHost) {
|
||||||
|
const firstHost = rawHost.split(',')[0].trim();
|
||||||
|
let extracted = '';
|
||||||
|
if (firstHost.startsWith('[')) {
|
||||||
|
const closeBracket = firstHost.indexOf(']');
|
||||||
|
extracted = closeBracket !== -1 ? firstHost.substring(1, closeBracket) : firstHost;
|
||||||
|
} else {
|
||||||
|
extracted = firstHost.split(':')[0].trim();
|
||||||
|
}
|
||||||
|
const isLocalHost = ['localhost', '127.0.0.1', '::1', 'f0ckm', '0.0.0.0', 'localhost.localdomain'].includes(extracted.toLowerCase());
|
||||||
|
if (extracted && !isLocalHost) {
|
||||||
|
reqHost = extracted;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!reqHost) reqHost = cfg.main.url.domain;
|
||||||
|
|
||||||
|
const currentGateOptions = {
|
||||||
|
...gateOptions,
|
||||||
|
host_status: {
|
||||||
|
...gateOptions.host_status,
|
||||||
|
location: reqHost,
|
||||||
|
},
|
||||||
|
what_can_i_do: gateSignInButton,
|
||||||
|
};
|
||||||
|
|
||||||
|
let html = _cfRender(currentGateOptions);
|
||||||
|
|
||||||
// Inject real visitor IP into the footer "Your IP: [Click to reveal]" span
|
// Inject real visitor IP into the footer "Your IP: [Click to reveal]" span
|
||||||
if (req) {
|
if (req) {
|
||||||
@@ -305,9 +337,9 @@ function buildGatePage(req) {
|
|||||||
// Patch the <title> to include the domain — matches real Cloudflare behaviour
|
// Patch the <title> to include the domain — matches real Cloudflare behaviour
|
||||||
html = html.replace(
|
html = html.replace(
|
||||||
'<title>502: Bad Gateway</title>',
|
'<title>502: Bad Gateway</title>',
|
||||||
`<title>${cfg.main.url.domain} | 502: Bad gateway</title>`
|
`<title>${reqHost} | 502: Bad gateway</title>`
|
||||||
);
|
);
|
||||||
html = html.replace('</body>', gateLoginInjection + '\n</body>');
|
html = html.replace('</body>', getGateLoginInjection(req) + '\n</body>');
|
||||||
return html;
|
return html;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -316,7 +348,14 @@ const nginx502 = (cfg.websrv.private_society && cfg.websrv.private_society_gate
|
|||||||
? null
|
? null
|
||||||
: nginx502Fallback;
|
: nginx502Fallback;
|
||||||
|
|
||||||
|
// Custom gate template — resolved once at boot from config
|
||||||
|
// Set private_society_gate: "custom" and private_society_gate_template: "your-template-name" (no .html)
|
||||||
|
const _customGateTemplate = (cfg.websrv.private_society && cfg.websrv.private_society_gate === 'custom' && cfg.websrv.private_society_gate_template)
|
||||||
|
? String(cfg.websrv.private_society_gate_template).replace(/\.html$/i, '')
|
||||||
|
: null;
|
||||||
|
|
||||||
if (nginx502 === null) console.log('[BOOT] Private society gate: Cloudflare dynamic mode');
|
if (nginx502 === null) console.log('[BOOT] Private society gate: Cloudflare dynamic mode');
|
||||||
|
if (_customGateTemplate) console.log(`[BOOT] Private society gate: Custom template → views/${_customGateTemplate}.html (flummpress resolves extension)`);
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
@@ -456,6 +495,22 @@ process.on('uncaughtException', err => {
|
|||||||
res.setHeader('Permissions-Policy', 'geolocation=(), microphone=(), camera=()');
|
res.setHeader('Permissions-Policy', 'geolocation=(), microphone=(), camera=()');
|
||||||
// Encourage connection reuse — helps external tools like ShareX avoid repeated TCP/TLS handshakes
|
// Encourage connection reuse — helps external tools like ShareX avoid repeated TCP/TLS handshakes
|
||||||
res.setHeader('Connection', 'keep-alive');
|
res.setHeader('Connection', 'keep-alive');
|
||||||
|
|
||||||
|
// Block incoming requests to .onion if Tor Hidden Service is explicitly disabled in config
|
||||||
|
if (cfg.websrv?.enable_tor_hs === false && lib.isOnionRequest(req)) {
|
||||||
|
res.writeHead(503).end();
|
||||||
|
req.url.pathname = '/tor_hs_disabled_bypass';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tor Onion-Location header: advertises .onion service counterpart to Tor Browser users when enabled
|
||||||
|
if (cfg.websrv?.enable_tor_hs !== false && cfg.main?.onion && !lib.isOnionRequest(req)) {
|
||||||
|
const p = req.url?.pathname || '/';
|
||||||
|
const s = req.url?.search || '';
|
||||||
|
const onionHost = cfg.main.onion.replace(/\/+$/, '');
|
||||||
|
res.setHeader('Onion-Location', `${onionHost}${p}${s}`);
|
||||||
|
}
|
||||||
|
|
||||||
if (isSecure) {
|
if (isSecure) {
|
||||||
res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
|
res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
|
||||||
}
|
}
|
||||||
@@ -617,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".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".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
|
||||||
@@ -801,18 +856,21 @@ process.on('uncaughtException', err => {
|
|||||||
req.url.pathname = '/private_society_bypass';
|
req.url.pathname = '/private_society_bypass';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// For page requests, return 502 Bad Gateway for all paths except homepage (which shows the gate)
|
// For page requests, serve the gate for all paths (custom template or fallback)
|
||||||
if (req.url.pathname !== '/') {
|
if (_customGateTemplate) {
|
||||||
|
res.writeHead(200, { 'Content-Type': 'text/html' }).end(tpl.render(_customGateTemplate, {}, req));
|
||||||
|
} else if (req.url.pathname !== '/') {
|
||||||
|
// Non-home paths: always show the static/cloudflare gate
|
||||||
res.writeHead(200, { 'Content-Type': 'text/html' }).end(nginx502 ?? buildGatePage(req));
|
res.writeHead(200, { 'Content-Type': 'text/html' }).end(nginx502 ?? buildGatePage(req));
|
||||||
req.url.pathname = '/private_society_bypass';
|
} else if (nginx502 === null) {
|
||||||
return;
|
// Homepage in cloudflare dynamic mode: serve the CF gate directly
|
||||||
}
|
|
||||||
// Homepage: in cloudflare dynamic mode serve gate directly; otherwise fall through to template
|
|
||||||
if (nginx502 === null) {
|
|
||||||
res.writeHead(200, { 'Content-Type': 'text/html' }).end(buildGatePage(req));
|
res.writeHead(200, { 'Content-Type': 'text/html' }).end(buildGatePage(req));
|
||||||
req.url.pathname = '/private_society_bypass';
|
} else {
|
||||||
|
// Homepage in plain nginx mode: fall through to index.html template
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
req.url.pathname = '/private_society_bypass';
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -1292,6 +1350,7 @@ process.on('uncaughtException', err => {
|
|||||||
show_mime_picker: cfg.websrv.show_mime_picker !== false,
|
show_mime_picker: cfg.websrv.show_mime_picker !== false,
|
||||||
private_society: cfg.websrv.private_society || false,
|
private_society: cfg.websrv.private_society || false,
|
||||||
private_society_gate: cfg.websrv.private_society_gate || '',
|
private_society_gate: cfg.websrv.private_society_gate || '',
|
||||||
|
private_society_gate_location: cfg.websrv.private_society_gate_location || 'Frankfurt',
|
||||||
show_content_warning: cfg.websrv.show_content_warning !== false,
|
show_content_warning: cfg.websrv.show_content_warning !== false,
|
||||||
web_url_upload: !!cfg.websrv.web_url_upload,
|
web_url_upload: !!cfg.websrv.web_url_upload,
|
||||||
enable_youtube_upload: cfg.websrv.enable_youtube_upload !== false,
|
enable_youtube_upload: cfg.websrv.enable_youtube_upload !== false,
|
||||||
@@ -1403,26 +1462,39 @@ process.on('uncaughtException', err => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Resolve per-request recaptcha preference (disabled for .onion requests or when passed false)
|
||||||
|
const effectiveReq = req || (data && data.req);
|
||||||
|
const defaultRecaptcha = !!(cfg.recaptcha && cfg.recaptcha.enabled && cfg.recaptcha.site_key);
|
||||||
|
let perRequestRecaptcha = defaultRecaptcha;
|
||||||
|
|
||||||
|
if (effectiveReq && lib.isOnionRequest(effectiveReq)) {
|
||||||
|
perRequestRecaptcha = false;
|
||||||
|
} else if (data && typeof data.recaptcha_enabled === 'boolean') {
|
||||||
|
perRequestRecaptcha = data.recaptcha_enabled;
|
||||||
|
}
|
||||||
|
|
||||||
// Build data: globals first, then caller-supplied data, then per-request i18n last
|
// Build data: globals first, then caller-supplied data, then per-request i18n last
|
||||||
// so t/lang always reflect the user's language, not the site default.
|
// so t/lang always reflect the user's language, not the site default.
|
||||||
// ALSO mutate globals.t and globals.lang: flummpress spreads this.#globals LAST
|
// ALSO mutate globals.t, globals.lang, globals.recaptcha_enabled: flummpress spreads this.#globals LAST
|
||||||
// inside render(), so globals must carry the per-request values too.
|
// inside render(), so globals must carry the per-request values too.
|
||||||
globals.t = perRequestT;
|
globals.t = perRequestT;
|
||||||
globals.lang = perRequestLang;
|
globals.lang = perRequestLang;
|
||||||
|
globals.recaptcha_enabled = perRequestRecaptcha;
|
||||||
|
|
||||||
// Resolve per-request infobox preference
|
// Resolve per-request infobox preference
|
||||||
// Guests always get false — the alternative infobox is a logged-in user preference only
|
// Guests always get false — the alternative infobox is a logged-in user preference only
|
||||||
const useAltInfobox = (req && req.session && typeof req.session.use_alternative_infobox === 'boolean')
|
const activeReq = req || effectiveReq;
|
||||||
? req.session.use_alternative_infobox
|
const useAltInfobox = (activeReq && activeReq.session && typeof activeReq.session.use_alternative_infobox === 'boolean')
|
||||||
: (req && !req.session
|
? activeReq.session.use_alternative_infobox
|
||||||
|
: (activeReq && !activeReq.session
|
||||||
? false
|
? false
|
||||||
: (data && typeof data.user_alternative_infobox === 'boolean'
|
: (data && typeof data.user_alternative_infobox === 'boolean'
|
||||||
? data.user_alternative_infobox
|
? data.user_alternative_infobox
|
||||||
: (cfg.websrv.user_alternative_infobox !== false)));
|
: (cfg.websrv.user_alternative_infobox !== false)));
|
||||||
|
|
||||||
const useAltSteuerung = (req && req.session && typeof req.session.use_alternative_steuerung === 'boolean')
|
const useAltSteuerung = (activeReq && activeReq.session && typeof activeReq.session.use_alternative_steuerung === 'boolean')
|
||||||
? req.session.use_alternative_steuerung
|
? activeReq.session.use_alternative_steuerung
|
||||||
: (req && !req.session
|
: (activeReq && !activeReq.session
|
||||||
? false
|
? false
|
||||||
: (data && typeof data.user_alternative_steuerung === 'boolean'
|
: (data && typeof data.user_alternative_steuerung === 'boolean'
|
||||||
? data.user_alternative_steuerung
|
? data.user_alternative_steuerung
|
||||||
@@ -1431,11 +1503,12 @@ process.on('uncaughtException', err => {
|
|||||||
data = Object.assign({}, globals, data || {}, {
|
data = Object.assign({}, globals, data || {}, {
|
||||||
t: perRequestT,
|
t: perRequestT,
|
||||||
lang: perRequestLang,
|
lang: perRequestLang,
|
||||||
|
recaptcha_enabled: perRequestRecaptcha,
|
||||||
user_alternative_infobox: useAltInfobox,
|
user_alternative_infobox: useAltInfobox,
|
||||||
user_alternative_steuerung: useAltSteuerung,
|
user_alternative_steuerung: useAltSteuerung,
|
||||||
user_banner_enabled: cfg.websrv.user_banner_enabled !== false,
|
user_banner_enabled: cfg.websrv.user_banner_enabled !== false,
|
||||||
comment_display_mode: (req && req.session && typeof req.session.comment_display_mode === 'number')
|
comment_display_mode: (activeReq && activeReq.session && typeof activeReq.session.comment_display_mode === 'number')
|
||||||
? req.session.comment_display_mode
|
? activeReq.session.comment_display_mode
|
||||||
: (data && typeof data.comment_display_mode === 'number'
|
: (data && typeof data.comment_display_mode === 'number'
|
||||||
? data.comment_display_mode
|
? data.comment_display_mode
|
||||||
: (cfg.websrv.default_comment_display_mode || 0))
|
: (cfg.websrv.default_comment_display_mode || 0))
|
||||||
@@ -1447,17 +1520,19 @@ process.on('uncaughtException', err => {
|
|||||||
data.custom_brand_image = brand[Math.floor(Math.random() * brand.length)];
|
data.custom_brand_image = brand[Math.floor(Math.random() * brand.length)];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (req) {
|
if (activeReq) {
|
||||||
if (req.mode !== undefined) data.mode = req.mode;
|
data.recaptcha_enabled = perRequestRecaptcha;
|
||||||
data.theme = req.theme || req.cookies?.theme || cfg.websrv.theme || 'f0ck';
|
if (activeReq.mode !== undefined) data.mode = activeReq.mode;
|
||||||
if (!data.url) data.url = req.url;
|
data.theme = activeReq.theme || activeReq.cookies?.theme || cfg.websrv.theme || 'f0ck';
|
||||||
data.user_strict_bool = (req.session && req.session.strict_mode) ? true : false;
|
if (!data.url) data.url = activeReq.url;
|
||||||
data.user_logged_in_bool = !!req.session;
|
data.user_strict_bool = (activeReq.session && activeReq.session.strict_mode) ? true : false;
|
||||||
data.csrf_token = req.session?.csrf_token || '';
|
data.user_logged_in_bool = !!activeReq.session;
|
||||||
data.max_file_size = lib.formatSize(cfg.main.maxfilesize * (req.session?.admin ? cfg.main.adminmultiplier : 1));
|
data.csrf_token = activeReq.session?.csrf_token || '';
|
||||||
data.max_file_size_bytes = Math.floor(cfg.main.maxfilesize * (req.session?.admin ? cfg.main.adminmultiplier : 1));
|
data.max_file_size = lib.formatSize(cfg.main.maxfilesize * (activeReq.session?.admin ? cfg.main.adminmultiplier : 1));
|
||||||
|
data.max_file_size_bytes = Math.floor(cfg.main.maxfilesize * (activeReq.session?.admin ? cfg.main.adminmultiplier : 1));
|
||||||
data.web_url_upload = data.web_url_upload !== undefined ? data.web_url_upload : !!cfg.websrv.web_url_upload;
|
data.web_url_upload = data.web_url_upload !== undefined ? data.web_url_upload : !!cfg.websrv.web_url_upload;
|
||||||
} else {
|
} else {
|
||||||
|
data.recaptcha_enabled = perRequestRecaptcha;
|
||||||
data.theme = data.theme || cfg.websrv.theme || 'f0ck';
|
data.theme = data.theme || cfg.websrv.theme || 'f0ck';
|
||||||
data.user_strict_bool = false;
|
data.user_strict_bool = false;
|
||||||
data.user_logged_in_bool = false;
|
data.user_logged_in_bool = false;
|
||||||
|
|||||||
35
views/_gate_template.html
Normal file
35
views/_gate_template.html
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html theme="@if(typeof theme !== 'undefined'){{ theme }}@else{{ default_theme || 'amoled' }}@endif">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>{{ domain }}</title>
|
||||||
|
<link rel="icon" type="image/gif" href="/s/img/favicon.gif" />
|
||||||
|
<link rel="stylesheet" id="injected-gate-styles" href="/s/css/f0ckm.css?v={{ ts }}">
|
||||||
|
</head>
|
||||||
|
<body class="{{ default_layout === 'legacy' ? 'layout-legacy' : 'layout-modern' }}">
|
||||||
|
|
||||||
|
<div class="wrapper" style="text-align: center;">
|
||||||
|
<h2>Example Gate</h2>
|
||||||
|
<button type="button" onclick="openLoginGate();">Login</button>
|
||||||
|
|
||||||
|
<div id="gate-container" style="display:none;">
|
||||||
|
@include(snippets/navbar)
|
||||||
|
@include(snippets/footer)
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function openLoginGate() {
|
||||||
|
document.getElementById('gate-container').style.display = 'block';
|
||||||
|
const tryOpen = () => {
|
||||||
|
const m = document.getElementById('login-modal');
|
||||||
|
if (m) { m.style.display = 'flex'; }
|
||||||
|
else { setTimeout(tryOpen, 50); }
|
||||||
|
};
|
||||||
|
tryOpen();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -319,7 +319,7 @@
|
|||||||
ModAction.confirm('Verify User', 'Manually verify account for <strong>' + escHTML(userName) + '</strong>?', async () => {
|
ModAction.confirm('Verify User', 'Manually verify account for <strong>' + escHTML(userName) + '</strong>?', async () => {
|
||||||
var res = await fetch('/api/v2/admin/users/activate', {
|
var res = await fetch('/api/v2/admin/users/activate', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': window.f0ckSession?.csrf_token },
|
||||||
body: JSON.stringify({ user_id: id })
|
body: JSON.stringify({ user_id: id })
|
||||||
});
|
});
|
||||||
var data = await res.json();
|
var data = await res.json();
|
||||||
@@ -339,7 +339,7 @@
|
|||||||
ModAction.confirm('Ban User', 'Reason for banning <strong>' + escHTML(userName) + '</strong>?', async (reason) => {
|
ModAction.confirm('Ban User', 'Reason for banning <strong>' + escHTML(userName) + '</strong>?', async (reason) => {
|
||||||
var res = await fetch('/api/v2/admin/ban', {
|
var res = await fetch('/api/v2/admin/ban', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': window.f0ckSession?.csrf_token },
|
||||||
body: JSON.stringify({ user_id: id, reason: reason, duration: 'permanent' })
|
body: JSON.stringify({ user_id: id, reason: reason, duration: 'permanent' })
|
||||||
});
|
});
|
||||||
var data = await res.json();
|
var data = await res.json();
|
||||||
@@ -359,7 +359,7 @@
|
|||||||
ModAction.confirm('Unban User', 'Unban account for <strong>' + escHTML(userName) + '</strong>?', async () => {
|
ModAction.confirm('Unban User', 'Unban account for <strong>' + escHTML(userName) + '</strong>?', async () => {
|
||||||
var res = await fetch('/api/v2/admin/unban', {
|
var res = await fetch('/api/v2/admin/unban', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': window.f0ckSession?.csrf_token },
|
||||||
body: JSON.stringify({ user_id: id })
|
body: JSON.stringify({ user_id: id })
|
||||||
});
|
});
|
||||||
var data = await res.json();
|
var data = await res.json();
|
||||||
@@ -379,7 +379,7 @@
|
|||||||
ModAction.confirm('Delete Uploads', 'Are you SURE you want to delete ALL uploads by <strong>' + escHTML(userName) + '</strong>? This cannot be undone.', async () => {
|
ModAction.confirm('Delete Uploads', 'Are you SURE you want to delete ALL uploads by <strong>' + escHTML(userName) + '</strong>? This cannot be undone.', async () => {
|
||||||
var res = await fetch('/api/v2/admin/users/bulk-delete-items', {
|
var res = await fetch('/api/v2/admin/users/bulk-delete-items', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': window.f0ckSession?.csrf_token },
|
||||||
body: JSON.stringify({ user_id: id, username: btn.dataset.username })
|
body: JSON.stringify({ user_id: id, username: btn.dataset.username })
|
||||||
});
|
});
|
||||||
var data = await res.json();
|
var data = await res.json();
|
||||||
@@ -398,7 +398,7 @@
|
|||||||
ModAction.confirm('Delete Comments', 'Are you SURE you want to delete ALL comments by <strong>' + escHTML(userName) + '</strong>? This will be permanent.', async () => {
|
ModAction.confirm('Delete Comments', 'Are you SURE you want to delete ALL comments by <strong>' + escHTML(userName) + '</strong>? This will be permanent.', async () => {
|
||||||
var res = await fetch('/api/v2/admin/users/bulk-delete-comments', {
|
var res = await fetch('/api/v2/admin/users/bulk-delete-comments', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': window.f0ckSession?.csrf_token },
|
||||||
body: JSON.stringify({ user_id: id, username: btn.dataset.username })
|
body: JSON.stringify({ user_id: id, username: btn.dataset.username })
|
||||||
});
|
});
|
||||||
var data = await res.json();
|
var data = await res.json();
|
||||||
@@ -422,7 +422,7 @@
|
|||||||
ModAction.confirm('Set Display Name', hint, async (newName) => {
|
ModAction.confirm('Set Display Name', hint, async (newName) => {
|
||||||
var res = await fetch('/api/v2/admin/users/set-display-name', {
|
var res = await fetch('/api/v2/admin/users/set-display-name', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': window.f0ckSession?.csrf_token },
|
||||||
body: JSON.stringify({ user_id: id, display_name: newName || '' })
|
body: JSON.stringify({ user_id: id, display_name: newName || '' })
|
||||||
});
|
});
|
||||||
var data = await res.json();
|
var data = await res.json();
|
||||||
@@ -457,7 +457,7 @@
|
|||||||
ModAction.confirm('Unlock Layout', 'Unlock comment layout for <strong>' + escHTML(userName) + '</strong>? They will be able to change it again.', async () => {
|
ModAction.confirm('Unlock Layout', 'Unlock comment layout for <strong>' + escHTML(userName) + '</strong>? They will be able to change it again.', async () => {
|
||||||
var res = await fetch('/api/v2/admin/users/lock-layout', {
|
var res = await fetch('/api/v2/admin/users/lock-layout', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': window.f0ckSession?.csrf_token },
|
||||||
body: JSON.stringify({ user_id: id, lock: false })
|
body: JSON.stringify({ user_id: id, lock: false })
|
||||||
});
|
});
|
||||||
var data = await res.json();
|
var data = await res.json();
|
||||||
@@ -481,7 +481,7 @@
|
|||||||
var mode = document.getElementById('force-mode-select').value;
|
var mode = document.getElementById('force-mode-select').value;
|
||||||
var res = await fetch('/api/v2/admin/users/lock-layout', {
|
var res = await fetch('/api/v2/admin/users/lock-layout', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': window.f0ckSession?.csrf_token },
|
||||||
body: JSON.stringify({ user_id: id, lock: true, mode: mode })
|
body: JSON.stringify({ user_id: id, lock: true, mode: mode })
|
||||||
});
|
});
|
||||||
var data = await res.json();
|
var data = await res.json();
|
||||||
@@ -616,7 +616,7 @@
|
|||||||
try {
|
try {
|
||||||
var res = await fetch('/api/v2/admin/users/create', {
|
var res = await fetch('/api/v2/admin/users/create', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': window.f0ckSession?.csrf_token },
|
||||||
body: JSON.stringify({ username, email: email || null, password, role })
|
body: JSON.stringify({ username, email: email || null, password, role })
|
||||||
});
|
});
|
||||||
var data = await res.json();
|
var data = await res.json();
|
||||||
@@ -662,7 +662,7 @@
|
|||||||
var role = selectEl.value;
|
var role = selectEl.value;
|
||||||
var res = await fetch('/api/v2/admin/users/set-role', {
|
var res = await fetch('/api/v2/admin/users/set-role', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': window.f0ckSession?.csrf_token },
|
||||||
body: JSON.stringify({ user_id: id, role })
|
body: JSON.stringify({ user_id: id, role })
|
||||||
});
|
});
|
||||||
var data = await res.json();
|
var data = await res.json();
|
||||||
|
|||||||
@@ -197,7 +197,11 @@
|
|||||||
</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)
|
||||||
|
<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
|
||||||
<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>
|
||||||
|
@endif
|
||||||
@endeach
|
@endeach
|
||||||
</span>
|
</span>
|
||||||
@if(enable_xd_score && item.xd_score > 0)
|
@if(enable_xd_score && item.xd_score > 0)
|
||||||
@@ -296,7 +300,7 @@
|
|||||||
@if(item.src.short)
|
@if(item.src.short)
|
||||||
<tr>
|
<tr>
|
||||||
<th>{{ t('info_modal.source') || 'Source' }}</th>
|
<th>{{ t('info_modal.source') || 'Source' }}</th>
|
||||||
<td><a href="{{ item.src.long }}" target="_blank">{{ item.src.short }}</a></td>
|
<td><a href="{{ item.src.long }}" target="_blank" style="word-break: break-all;">{{ item.src.short }}</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
@endif
|
@endif
|
||||||
</table>
|
</table>
|
||||||
|
|||||||
@@ -157,7 +157,11 @@
|
|||||||
</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)
|
||||||
|
<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
|
||||||
<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>
|
||||||
|
@endif
|
||||||
@endeach
|
@endeach
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
@@ -238,7 +242,7 @@
|
|||||||
@if(item.src.short)
|
@if(item.src.short)
|
||||||
<tr>
|
<tr>
|
||||||
<th>{{ t('info_modal.source') || 'Source' }}</th>
|
<th>{{ t('info_modal.source') || 'Source' }}</th>
|
||||||
<td><a href="{{ item.src.long }}" target="_blank">{{ item.src.short }}</a></td>
|
<td><a href="{{ item.src.long }}" target="_blank" style="word-break: break-all;">{{ item.src.short }}</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
@endif
|
@endif
|
||||||
</table>
|
</table>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
.meme-title { font-family: 'Impact', 'VCR', sans-serif; }
|
.meme-title { font-family: 'Impact', 'VCR', sans-serif; }
|
||||||
|
|
||||||
/* NUCLEAR VISIBILITY RESET: Override any and all parent constraints for this page */
|
/* NUCLEAR VISIBILITY RESET: Override any and all parent constraints for this page */
|
||||||
@media (max-width: 950px) {
|
@media (max-width: 1250px) {
|
||||||
/* Target all possible shell triggers */
|
/* Target all possible shell triggers */
|
||||||
html, body, .pagewrapper, #main, .layout-modern react-wrapper, .layout-legacy react-wrapper {
|
html, body, .pagewrapper, #main, .layout-modern react-wrapper, .layout-legacy react-wrapper {
|
||||||
height: auto !important;
|
height: auto !important;
|
||||||
|
|||||||
@@ -320,6 +320,20 @@
|
|||||||
</label>
|
</label>
|
||||||
<small class="text-muted" style="margin-left: 25px;">{{ t('settings.disable_swiping_hint') }}</small>
|
<small class="text-muted" style="margin-left: 25px;">{{ t('settings.disable_swiping_hint') }}</small>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="setting-item" style="margin-top: 15px;">
|
||||||
|
<label for="favorites_private_toggle" style="cursor: pointer; display: flex; align-items: center; gap: 10px;">
|
||||||
|
<input type="checkbox" id="favorites_private_toggle" @if(session.favorites_private===true) checked @endif>
|
||||||
|
<span>{{ t('settings.favorites_private') }}</span>
|
||||||
|
</label>
|
||||||
|
<small class="text-muted" style="margin-left: 25px;">{{ t('settings.favorites_private_hint') }}</small>
|
||||||
|
<div class="sub-setting-item" style="margin-left: 25px; margin-top: 10px;">
|
||||||
|
<label for="hide_fav_badge_toggle" style="cursor: pointer; display: flex; align-items: center; gap: 10px;">
|
||||||
|
<input type="checkbox" id="hide_fav_badge_toggle" @if(session.hide_fav_badge===true) checked @endif>
|
||||||
|
<span>{{ t('settings.hide_fav_badge') }}</span>
|
||||||
|
</label>
|
||||||
|
<small class="text-muted" style="margin-left: 25px;">{{ t('settings.hide_fav_badge_hint') }}</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<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>
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
<meta name="mobile-web-app-capable" content="yes">
|
<meta name="mobile-web-app-capable" content="yes">
|
||||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
||||||
<meta name="apple-mobile-web-app-title" content="{{ domain }}">
|
<meta name="apple-mobile-web-app-title" content="{{ domain }}">
|
||||||
|
<script>(function(){var h=document.documentElement;if(localStorage.getItem('hideItemRatings')==='true')h.classList.add('hide-item-ratings-active');if(localStorage.getItem('blurNsfw')==='true')h.classList.add('blur-nsfw-active');if(localStorage.getItem('blurNsfl')==='true')h.classList.add('blur-nsfl-active');if(localStorage.getItem('blurSfw')==='true')h.classList.add('blur-sfw-active');if(localStorage.getItem('blurUntagged')==='true')h.classList.add('blur-untagged-active');if(localStorage.getItem('blurDetail')!=='false')h.classList.add('blur-detail-active');if(localStorage.getItem('imageExpandOnClick')!=='false')h.classList.add('image-expand-active');})();</script>
|
||||||
<style>
|
<style>
|
||||||
html { background-color: #000; color: #fff; }
|
html { background-color: #000; color: #fff; }
|
||||||
@if(session && session.font)
|
@if(session && session.font)
|
||||||
@@ -22,6 +23,7 @@
|
|||||||
}
|
}
|
||||||
@endif
|
@endif
|
||||||
:root {
|
:root {
|
||||||
|
--favicon-url: url('@if(custom_favicon && custom_favicon.length > 0){{ custom_favicon }}@else/s/img/favicon.gif@endif');
|
||||||
@if(session && session.font)
|
@if(session && session.font)
|
||||||
--font: 'CustomUserFont', monospace !important;
|
--font: 'CustomUserFont', monospace !important;
|
||||||
@elseif(typeof default_font !== 'undefined' && default_font && default_font.length > 0)
|
@elseif(typeof default_font !== 'undefined' && default_font && default_font.length > 0)
|
||||||
|
|||||||
@@ -48,7 +48,7 @@
|
|||||||
<div class="profile_head_username">
|
<div class="profile_head_username">
|
||||||
<div style="display: flex; justify-content: space-between; align-items: flex-start; gap: 10px;">
|
<div style="display: flex; justify-content: space-between; align-items: flex-start; gap: 10px;">
|
||||||
<div style="flex: 1; min-width: 0; word-break: break-word; line-height: 1.2; align-self: flex-start; margin-top: 0; display: flex; align-items: center; gap: 5px;" id="profile-name-container">
|
<div style="flex: 1; min-width: 0; word-break: break-word; line-height: 1.2; align-self: flex-start; margin-top: 0; display: flex; align-items: center; gap: 5px;" id="profile-name-container">
|
||||||
<span id="nav-display-name" @if(user.username_color) style="color: {{ user.username_color }}" @endif>@if(user.admin)⚡ @elseif(user.is_moderator)🛡 @endif{!! user.display_name || user.user !!}@if(user.is_ghost) <span class="badge badge-secondary" style="font-size: 0.5em; vertical-align: middle; background-color: #5bc0de; color: #fff; padding: 2px 5px; border-radius: 3px; margin-left: 5px;">LEGACY</span>@endif @if(user.banned) <span class="badge badge-danger" tooltip="{{ user.ban_duration }}" style="font-size: 0.5em; vertical-align: middle; background-color: #d9534f; color: #fff; padding: 2px 5px; border-radius: 3px; margin-left: 5px;">BANNED</span>@endif</span>@if(user.display_name) <span style="font-size: 0.65em; color: #666; font-weight: 400; margin-left: 5px; letter-spacing: 0.5px;" id="username-bracket">({!! user.user !!})</span>@endif
|
<span id="profile-display-name" @if(user.username_color) style="color: {{ user.username_color }}" @endif>@if(user.admin)⚡ @elseif(user.is_moderator)🛡 @endif{!! user.display_name || user.user !!}@if(user.is_ghost) <span class="badge badge-secondary" style="font-size: 0.5em; vertical-align: middle; background-color: #5bc0de; color: #fff; padding: 2px 5px; border-radius: 3px; margin-left: 5px;">LEGACY</span>@endif @if(user.banned) <span class="badge badge-danger" tooltip="{{ user.ban_duration }}" style="font-size: 0.5em; vertical-align: middle; background-color: #d9534f; color: #fff; padding: 2px 5px; border-radius: 3px; margin-left: 5px;">BANNED</span>@endif</span>@if(user.display_name) <span style="font-size: 0.65em; color: #666; font-weight: 400; margin-left: 5px; letter-spacing: 0.5px;" id="username-bracket">({!! user.user !!})</span>@endif
|
||||||
@if(session && session.id === user.user_id)
|
@if(session && session.id === user.user_id)
|
||||||
<button id="inline-name-edit-btn" style="background: transparent; border:none; color: var(--text-muted); cursor: pointer; font-size: 0.7em; padding: 0;" title="Edit Name & Color">
|
<button id="inline-name-edit-btn" style="background: transparent; border:none; color: var(--text-muted); cursor: pointer; font-size: 0.7em; padding: 0;" title="Edit Name & Color">
|
||||||
<i class="fa-solid fa-pen"></i>
|
<i class="fa-solid fa-pen"></i>
|
||||||
@@ -142,6 +142,14 @@
|
|||||||
</div>
|
</div>
|
||||||
@if(!user.is_ghost)
|
@if(!user.is_ghost)
|
||||||
<div class="favs">
|
<div class="favs">
|
||||||
|
@if(favs && favs.is_private)
|
||||||
|
<div class="favs-header">
|
||||||
|
{{ t('profile.favs_label') }}
|
||||||
|
</div>
|
||||||
|
<div class="private-favs-msg" style="text-align: center;">
|
||||||
|
{{ t('profile.private_favorites') }}
|
||||||
|
</div>
|
||||||
|
@else
|
||||||
<div class="favs-header">
|
<div class="favs-header">
|
||||||
{{ t('profile.favs_label') }}: {{ count.favs }} <a href="@if(favs.link && favs.link.main){{ favs.link.main }}@else#@endif">{{ t('profile.view_all') }}</a>
|
{{ t('profile.favs_label') }}: {{ count.favs }} <a href="@if(favs.link && favs.link.main){{ favs.link.main }}@else#@endif">{{ t('profile.view_all') }}</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -164,6 +172,7 @@
|
|||||||
@else
|
@else
|
||||||
{{ t('profile.no_favs') }}
|
{{ t('profile.no_favs') }}
|
||||||
@endif
|
@endif
|
||||||
|
@endif
|
||||||
</div>
|
</div>
|
||||||
@endif
|
@endif
|
||||||
</div>
|
</div>
|
||||||
@@ -509,18 +518,27 @@
|
|||||||
nameContainer.style.display = 'flex';
|
nameContainer.style.display = 'flex';
|
||||||
nameEditContainer.style.display = 'none';
|
nameEditContainer.style.display = 'none';
|
||||||
|
|
||||||
const nameSpan = document.getElementById('nav-display-name');
|
const updateNameNode = (el, text) => {
|
||||||
if (nameSpan) {
|
if (!el) return;
|
||||||
nameSpan.style.color = color;
|
for (let i = el.childNodes.length - 1; i >= 0; i--) {
|
||||||
for (let i = nameSpan.childNodes.length - 1; i >= 0; i--) {
|
let node = el.childNodes[i];
|
||||||
let node = nameSpan.childNodes[i];
|
|
||||||
if (node.nodeType === Node.TEXT_NODE && node.nodeValue.trim().length > 0) {
|
if (node.nodeType === Node.TEXT_NODE && node.nodeValue.trim().length > 0) {
|
||||||
// Match and preserve any non-breaking spaces at the start (from admin/mod badges)
|
const match = node.nodeValue.match(new RegExp('^(\\s*)(.*)'));
|
||||||
const match = node.nodeValue.match(new RegExp('^(\\\\s*)(.*)'));
|
node.nodeValue = (match ? match[1] : '') + text;
|
||||||
node.nodeValue = (match ? match[1] : '') + (displayName ? displayName : '{{ user.user }}');
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
const targetName = displayName ? displayName : '{{ user.user }}';
|
||||||
|
const profileSpan = document.getElementById('profile-display-name');
|
||||||
|
if (profileSpan) {
|
||||||
|
profileSpan.style.color = color;
|
||||||
|
updateNameNode(profileSpan, targetName);
|
||||||
|
}
|
||||||
|
const navSpan = document.getElementById('nav-display-name');
|
||||||
|
if (navSpan) {
|
||||||
|
navSpan.style.color = color;
|
||||||
|
updateNameNode(navSpan, targetName);
|
||||||
}
|
}
|
||||||
const bracket = document.getElementById('username-bracket');
|
const bracket = document.getElementById('username-bracket');
|
||||||
if (bracket) {
|
if (bracket) {
|
||||||
|
|||||||
Reference in New Issue
Block a user