Compare commits
58 Commits
chat-overh
...
98
| Author | SHA1 | Date | |
|---|---|---|---|
| 9ccebc6d95 | |||
| 1262b15a5b | |||
| 3927085e1f | |||
| 2cd8bd91ff | |||
| 7b186dcdb2 | |||
| d5c294c940 | |||
| 973753d231 | |||
| eee7c899ab | |||
| c30b3535ae | |||
| 6e8ae2f7cb | |||
| 90ef78bbf3 | |||
| eed8b993e1 | |||
| 2eae7f93e5 | |||
| 9c239769e6 | |||
| 6e07194b27 | |||
| 2758f2a72e | |||
| 866a8003fe | |||
| 5ab1a9614a | |||
| e45df6a769 | |||
| 99eb9735a1 | |||
| b651621cee | |||
| d3ce2e38f0 | |||
| 096786516e | |||
| ec895a285b | |||
| 20aaf183d8 | |||
| c0c32ddfe4 | |||
| 25ab4ba949 | |||
| a6e2689909 | |||
| 6a52445987 | |||
| 63c39ebe90 | |||
| d1dd088bc6 | |||
| 0a21b6c8f8 | |||
| 1bb3dcff21 | |||
| a7aabac956 | |||
| 9f5510bc04 | |||
| fc6749cd11 | |||
| 54e2e8871b | |||
| 4d685dff06 | |||
| c99f3e11d8 | |||
| b0053a8de8 | |||
| d14feaf3eb | |||
| 280020c19f | |||
| 5e35fd48d4 | |||
| 0d3819c113 | |||
| a07c21f22b | |||
| 34d2b62909 | |||
| 65fb96f675 | |||
| 18c898c910 | |||
| 20a6ffcb04 | |||
| cbfbcb04bf | |||
| 8518e87c8d | |||
| a3148848f2 | |||
| abedf7a789 | |||
| d2ce6f412d | |||
| ec4db7a22b | |||
| c0d70baf83 | |||
| 122f57eacc | |||
| 07de9d4488 |
@@ -1,9 +1,9 @@
|
||||
POSTGRES_USER=f0ckm
|
||||
POSTGRES_DB=f0ckm
|
||||
POSTGRES_PASSWORD=f0ckm
|
||||
# --- Nginx & Let's Encrypt Configuration (Optional) ---
|
||||
# Set to 'f0ckm-nginx' to enable the Nginx & Let's Encrypt proxy stack
|
||||
# COMPOSE_PROFILES=f0ckm-nginx
|
||||
# --- Nginx & Tor Profiles (Optional) ---
|
||||
# Set to 'f0ckm-nginx' for Nginx, 'tor' for Tor hidden service, or 'f0ckm-nginx,tor' for both
|
||||
# COMPOSE_PROFILES=tor
|
||||
#
|
||||
# VIRTUAL_HOST=yourdomain.com
|
||||
# LETSENCRYPT_HOST=yourdomain.com
|
||||
|
||||
20
README.md
20
README.md
@@ -78,3 +78,23 @@ now visit http://localhost:1337 in your browser, you can develop without needing
|
||||
## NGINX
|
||||
|
||||
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"
|
||||
}
|
||||
```
|
||||
|
||||
11
build.bash
11
build.bash
@@ -16,7 +16,7 @@ fi
|
||||
# Parse arguments
|
||||
TARGET="master"
|
||||
CACHE_FLAG=""
|
||||
BRANCH_TARGET=""
|
||||
CUSTOM_BRANCH=""
|
||||
|
||||
for arg in "$@"; do
|
||||
case $arg in
|
||||
@@ -27,8 +27,8 @@ for arg in "$@"; do
|
||||
CACHE_FLAG="--no-cache"
|
||||
;;
|
||||
*)
|
||||
if [ -z "$BRANCH_TARGET" ]; then
|
||||
BRANCH_TARGET=$arg
|
||||
if [ -z "$CUSTOM_BRANCH" ]; then
|
||||
CUSTOM_BRANCH=$arg
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
@@ -43,7 +43,10 @@ fi
|
||||
# Set Image name and branch defaults
|
||||
IMAGE_NAME="f0ckm"
|
||||
|
||||
if [ "$TARGET" == "master" ]; then
|
||||
if [ -n "$CUSTOM_BRANCH" ]; then
|
||||
BRANCH_TARGET="$CUSTOM_BRANCH"
|
||||
echo "--- CUSTOM BRANCH DEPLOYMENT (${BRANCH_TARGET}) ---"
|
||||
elif [ "$TARGET" == "master" ]; then
|
||||
echo "--- PRODUCTION DEPLOYMENT (master) ---"
|
||||
BRANCH_TARGET="master"
|
||||
elif [ "$TARGET" == "stg" ]; then
|
||||
|
||||
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",
|
||||
"regex": "example\\.com"
|
||||
},
|
||||
"onion": "http://your-onion-address.onion",
|
||||
"socks": "socks5://127.0.0.1:9050",
|
||||
"mail": "admin@example.com",
|
||||
"maxfilesize": 104857600,
|
||||
@@ -61,6 +62,7 @@
|
||||
"show_koepfe": false,
|
||||
"hide_sidebar_default": true,
|
||||
"koepfe": [],
|
||||
"enable_tor_hs": true,
|
||||
"enable_global_chat": true,
|
||||
"enable_danmaku": true,
|
||||
"private_messages": true,
|
||||
@@ -132,6 +134,8 @@
|
||||
"open_registration_require_mail_andor_token": false,
|
||||
"private_society": false,
|
||||
"private_society_gate": "cloudflare",
|
||||
"private_society_gate_location": "Frankfurt",
|
||||
"private_society_gate_template": "_gate_template",
|
||||
"public_nsfw": false,
|
||||
"paths": {
|
||||
"images": "/b",
|
||||
|
||||
@@ -49,9 +49,14 @@ services:
|
||||
tor:
|
||||
image: dockurr/tor
|
||||
container_name: tor
|
||||
profiles:
|
||||
- tor
|
||||
ports:
|
||||
- "9050:9050"
|
||||
- "8118:8118"
|
||||
- "127.0.0.1:9050:9050"
|
||||
- "127.0.0.1:8118:8118"
|
||||
volumes:
|
||||
- ./config/tor:/etc/tor:ro
|
||||
- ./f0ckm-data/tor:/var/lib/tor:Z
|
||||
networks:
|
||||
- f0ckm-net
|
||||
restart: always
|
||||
|
||||
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,
|
||||
do_not_disturb boolean DEFAULT false,
|
||||
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
|
||||
);
|
||||
|
||||
|
||||
|
||||
357
public/s/css/98.css
Normal file
357
public/s/css/98.css
Normal file
@@ -0,0 +1,357 @@
|
||||
git pu/* 98.css framework - Scoped exclusively for f0ck98 theme */
|
||||
|
||||
@font-face {
|
||||
font-family: "Pixelated MS Sans Serif";
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: url("/s/fonts/ms_sans_serif.woff2") format("woff2");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Pixelated MS Sans Serif";
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
src: url("/s/fonts/ms_sans_serif_bold.woff2") format("woff2");
|
||||
}
|
||||
|
||||
html[theme="f0ck98"] {
|
||||
--w98-silver: #c0c0c0;
|
||||
--w98-teal: #008080;
|
||||
--w98-navy: #000080;
|
||||
--w98-navy-gradient: linear-gradient(90deg, navy, #1084d0);
|
||||
--w98-dark-shadow: #0a0a0a;
|
||||
--w98-light-highlight: #ffffff;
|
||||
--w98-mid-gray: grey;
|
||||
--w98-light-gray: #dfdfdf;
|
||||
|
||||
color: #000;
|
||||
font-family: "Pixelated MS Sans Serif", Arial, sans-serif;
|
||||
font-size: 11px;
|
||||
background-color: var(--w98-silver) !important;
|
||||
background: var(--w98-silver) !important;
|
||||
}
|
||||
|
||||
html[theme="f0ck98"] body {
|
||||
color: #000;
|
||||
font-family: "Pixelated MS Sans Serif", Arial, sans-serif;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
html[theme="f0ck98"] .title-bar,
|
||||
html[theme="f0ck98"] .window,
|
||||
html[theme="f0ck98"] button,
|
||||
html[theme="f0ck98"] input,
|
||||
html[theme="f0ck98"] label,
|
||||
html[theme="f0ck98"] legend,
|
||||
html[theme="f0ck98"] li[role=tab],
|
||||
html[theme="f0ck98"] option,
|
||||
html[theme="f0ck98"] select,
|
||||
html[theme="f0ck98"] table,
|
||||
html[theme="f0ck98"] textarea,
|
||||
html[theme="f0ck98"] ul.tree-view {
|
||||
-webkit-font-smoothing: none;
|
||||
font-family: "Pixelated MS Sans Serif", Arial, sans-serif;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
html[theme="f0ck98"] u {
|
||||
border-bottom: 0.5px solid #222;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
/* Windows 98 Buttons */
|
||||
html[theme="f0ck98"] button,
|
||||
html[theme="f0ck98"] input[type=reset],
|
||||
html[theme="f0ck98"] input[type=submit],
|
||||
html[theme="f0ck98"] .btn {
|
||||
background: silver;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
box-shadow: inset -1px -1px #0a0a0a, inset 1px 1px #fff, inset -2px -2px grey, inset 2px 2px #dfdfdf;
|
||||
box-sizing: border-box;
|
||||
color: #000;
|
||||
min-height: 23px;
|
||||
}
|
||||
|
||||
html[theme="f0ck98"] button.default,
|
||||
html[theme="f0ck98"] input[type=reset].default,
|
||||
html[theme="f0ck98"] input[type=submit].default,
|
||||
html[theme="f0ck98"] .btn.default {
|
||||
box-shadow: inset -2px -2px #0a0a0a, inset 1px 1px #0a0a0a, inset 2px 2px #fff, inset -3px -3px grey, inset 3px 3px #dfdfdf;
|
||||
}
|
||||
|
||||
html[theme="f0ck98"] .vertical-bar {
|
||||
background: silver;
|
||||
box-shadow: inset -1px -1px #0a0a0a, inset 1px 1px #fff, inset -2px -2px grey, inset 2px 2px #dfdfdf;
|
||||
height: 20px;
|
||||
width: 4px;
|
||||
}
|
||||
|
||||
html[theme="f0ck98"] button:not(:disabled):active,
|
||||
html[theme="f0ck98"] input[type=reset]:not(:disabled):active,
|
||||
html[theme="f0ck98"] input[type=submit]:not(:disabled):active,
|
||||
html[theme="f0ck98"] .btn:not(:disabled):active {
|
||||
box-shadow: inset -1px -1px #fff, inset 1px 1px #0a0a0a, inset -2px -2px #dfdfdf, inset 2px 2px grey;
|
||||
}
|
||||
|
||||
html[theme="f0ck98"] button.default:not(:disabled):active,
|
||||
html[theme="f0ck98"] input[type=reset].default:not(:disabled):active,
|
||||
html[theme="f0ck98"] input[type=submit].default:not(:disabled):active {
|
||||
box-shadow: inset 2px 2px #0a0a0a, inset -1px -1px #0a0a0a, inset -2px -2px #fff, inset 3px 3px grey, inset -3px -3px #dfdfdf;
|
||||
}
|
||||
|
||||
html[theme="f0ck98"] button:focus,
|
||||
html[theme="f0ck98"] input[type=reset]:focus,
|
||||
html[theme="f0ck98"] input[type=submit]:focus,
|
||||
html[theme="f0ck98"] .btn:focus {
|
||||
outline: 1px dotted #000;
|
||||
outline-offset: -4px;
|
||||
}
|
||||
|
||||
html[theme="f0ck98"] :disabled,
|
||||
html[theme="f0ck98"] :disabled+label,
|
||||
html[theme="f0ck98"] input[readonly],
|
||||
html[theme="f0ck98"] input[readonly]+label {
|
||||
color: grey;
|
||||
}
|
||||
|
||||
html[theme="f0ck98"] :disabled+label,
|
||||
html[theme="f0ck98"] button:disabled,
|
||||
html[theme="f0ck98"] input[type=reset]:disabled,
|
||||
html[theme="f0ck98"] input[type=submit]:disabled,
|
||||
html[theme="f0ck98"] .btn:disabled {
|
||||
text-shadow: 1px 1px 0 #fff;
|
||||
}
|
||||
|
||||
/* Window Shell & Title Bars */
|
||||
html[theme="f0ck98"] .window {
|
||||
background: silver;
|
||||
box-shadow: inset -1px -1px #0a0a0a, inset 1px 1px #dfdfdf, inset -2px -2px grey, inset 2px 2px #fff;
|
||||
padding: 3px;
|
||||
}
|
||||
|
||||
html[theme="f0ck98"] .title-bar {
|
||||
align-items: center;
|
||||
background: linear-gradient(90deg, navy, #1084d0);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 3px 2px 3px 3px;
|
||||
}
|
||||
|
||||
html[theme="f0ck98"] .title-bar.inactive {
|
||||
background: linear-gradient(90deg, grey, #b5b5b5);
|
||||
}
|
||||
|
||||
html[theme="f0ck98"] .title-bar-text {
|
||||
color: #fff;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0;
|
||||
margin-right: 24px;
|
||||
}
|
||||
|
||||
html[theme="f0ck98"] .title-bar-controls {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
html[theme="f0ck98"] .title-bar-controls button {
|
||||
display: block;
|
||||
min-height: 14px;
|
||||
min-width: 16px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
html[theme="f0ck98"] .title-bar-controls button:active {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
html[theme="f0ck98"] .title-bar-controls button:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
html[theme="f0ck98"] .title-bar-controls button[aria-label=Minimize],
|
||||
html[theme="f0ck98"] .title-bar-controls button[aria-label].minimize {
|
||||
background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg width='6' height='2' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill='%23000' d='M0 0h6v2H0z'/%3E%3C/svg%3E");
|
||||
background-position: bottom 3px left 4px;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
html[theme="f0ck98"] .title-bar-controls button[aria-label=Maximize],
|
||||
html[theme="f0ck98"] .title-bar-controls button[aria-label].maximize {
|
||||
background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg width='9' height='9' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M9 0H0v9h9V0zM8 2H1v6h7V2z' fill='%23000'/%3E%3C/svg%3E");
|
||||
background-position: top 2px left 3px;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
html[theme="f0ck98"] .title-bar-controls button[aria-label=Close],
|
||||
html[theme="f0ck98"] .title-bar-controls button[aria-label].close {
|
||||
background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg width='8' height='7' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M0 0h2v1h1v1h2V1h1V0h2v1H7v1H6v1H5v1h1v1h1v1h1v1H6V6H5V5H3v1H2v1H0V6h1V5h1V4h1V3H2V2H1V1H0V0z' fill='%23000'/%3E%3C/svg%3E");
|
||||
background-position: top 3px left 4px;
|
||||
background-repeat: no-repeat;
|
||||
margin-left: 2px;
|
||||
}
|
||||
|
||||
/* Status Bar */
|
||||
html[theme="f0ck98"] .status-bar {
|
||||
gap: 1px;
|
||||
display: flex;
|
||||
margin: 0 1px;
|
||||
}
|
||||
|
||||
html[theme="f0ck98"] .status-bar-field {
|
||||
box-shadow: inset -1px -1px #dfdfdf, inset 1px 1px grey;
|
||||
flex-grow: 1;
|
||||
margin: 0;
|
||||
padding: 2px 3px;
|
||||
}
|
||||
|
||||
html[theme="f0ck98"] .window-body {
|
||||
margin: 8px;
|
||||
}
|
||||
|
||||
html[theme="f0ck98"] fieldset {
|
||||
border-image: url("data:image/svg+xml;charset=utf-8,%3Csvg width='5' height='5' fill='gray' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M0 0h5v5H0V2h2v1h1V2H0' fill='%23fff'/%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M0 0h4v4H0V1h1v2h2V1H0'/%3E%3C/svg%3E") 2;
|
||||
margin: 0;
|
||||
padding: 10px;
|
||||
padding-block-start: 8px;
|
||||
}
|
||||
|
||||
html[theme="f0ck98"] legend {
|
||||
background: silver;
|
||||
}
|
||||
|
||||
html[theme="f0ck98"] .field-row {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
html[theme="f0ck98"] [class^=field-row]+[class^=field-row] {
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
html[theme="f0ck98"] .field-row>*+* {
|
||||
margin-left: 6px;
|
||||
}
|
||||
|
||||
html[theme="f0ck98"] label {
|
||||
align-items: center;
|
||||
display: inline-flex;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
/* Form Inputs & Textarea */
|
||||
html[theme="f0ck98"] input[type=email],
|
||||
html[theme="f0ck98"] input[type=number],
|
||||
html[theme="f0ck98"] input[type=password],
|
||||
html[theme="f0ck98"] input[type=search],
|
||||
html[theme="f0ck98"] input[type=tel],
|
||||
html[theme="f0ck98"] input[type=text],
|
||||
html[theme="f0ck98"] input[type=url],
|
||||
html[theme="f0ck98"] select,
|
||||
html[theme="f0ck98"] textarea {
|
||||
background-color: #fff;
|
||||
box-shadow: inset -1px -1px #fff, inset 1px 1px grey, inset -2px -2px #dfdfdf, inset 2px 2px #0a0a0a;
|
||||
box-sizing: border-box;
|
||||
padding: 3px 4px;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
html[theme="f0ck98"] input[type=email]:disabled,
|
||||
html[theme="f0ck98"] input[type=number]:disabled,
|
||||
html[theme="f0ck98"] input[type=password]:disabled,
|
||||
html[theme="f0ck98"] input[type=search]:disabled,
|
||||
html[theme="f0ck98"] input[type=tel]:disabled,
|
||||
html[theme="f0ck98"] input[type=text]:disabled,
|
||||
html[theme="f0ck98"] input[type=url]:disabled,
|
||||
html[theme="f0ck98"] textarea:disabled {
|
||||
background-color: silver;
|
||||
}
|
||||
|
||||
html[theme="f0ck98"] select {
|
||||
appearance: none;
|
||||
-webkit-appearance: none;
|
||||
background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg width='16' height='17' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M15 0H0v16h1V1h14V0z' fill='%23DFDFDF'/%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M2 1H1v14h1V2h12V1H2z' fill='%23fff'/%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M16 17H0v-1h15V0h1v17z' fill='%23000'/%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M15 1h-1v14H1v1h14V1z' fill='gray'/%3E%3Cpath fill='silver' d='M2 2h12v13H2z'/%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M11 6H4v1h1v1h1v1h1v1h1V9h1V8h1V7h1V6z' fill='%23000'/%3E%3C/svg%3E");
|
||||
background-position: top 2px right 2px;
|
||||
background-repeat: no-repeat;
|
||||
border-radius: 0;
|
||||
padding-right: 32px;
|
||||
}
|
||||
|
||||
html[theme="f0ck98"] input[type=email]:focus,
|
||||
html[theme="f0ck98"] input[type=number]:focus,
|
||||
html[theme="f0ck98"] input[type=password]:focus,
|
||||
html[theme="f0ck98"] input[type=search]:focus,
|
||||
html[theme="f0ck98"] input[type=tel]:focus,
|
||||
html[theme="f0ck98"] input[type=text]:focus,
|
||||
html[theme="f0ck98"] input[type=url]:focus,
|
||||
html[theme="f0ck98"] select:focus,
|
||||
html[theme="f0ck98"] textarea:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
html[theme="f0ck98"] a {
|
||||
color: #000080;
|
||||
}
|
||||
|
||||
html[theme="f0ck98"] a:focus {
|
||||
outline: 1px dotted #000080;
|
||||
}
|
||||
|
||||
html[theme="f0ck98"] .sunken-panel {
|
||||
background-color: #fff;
|
||||
box-shadow: inset -1px -1px #fff, inset 1px 1px grey, inset -2px -2px #dfdfdf, inset 2px 2px #0a0a0a;
|
||||
box-sizing: border-box;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
/* Scoped Scrollbars for f0ck98 */
|
||||
html[theme="f0ck98"] ::-webkit-scrollbar {
|
||||
width: 16px;
|
||||
height: 17px;
|
||||
}
|
||||
|
||||
html[theme="f0ck98"] ::-webkit-scrollbar-corner {
|
||||
background: #dfdfdf;
|
||||
}
|
||||
|
||||
html[theme="f0ck98"] ::-webkit-scrollbar-track {
|
||||
background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg width='2' height='2' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M1 0H0v1h1v1h1V1H1V0z' fill='silver'/%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M2 0H1v1H0v1h1V1h1V0z' fill='%23fff'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
html[theme="f0ck98"] ::-webkit-scrollbar-thumb {
|
||||
background-color: #dfdfdf;
|
||||
box-shadow: inset -1px -1px #0a0a0a, inset 1px 1px #fff, inset -2px -2px grey, inset 2px 2px #dfdfdf;
|
||||
}
|
||||
|
||||
html[theme="f0ck98"] ::-webkit-scrollbar-button:vertical:start {
|
||||
background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg width='16' height='17' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M15 0H0v16h1V1h14V0z' fill='%23DFDFDF'/%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M2 1H1v14h1V2h12V1H2z' fill='%23fff'/%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M16 17H0v-1h15V0h1v17z' fill='%23000'/%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M15 1h-1v14H1v1h14V1z' fill='gray'/%3E%3Cpath fill='silver' d='M2 2h12v13H2z'/%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M8 6H7v1H6v1H5v1H4v1h7V9h-1V8H9V7H8V6z' fill='%23000'/%3E%3C/svg%3E");
|
||||
height: 17px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
html[theme="f0ck98"] ::-webkit-scrollbar-button:vertical:end {
|
||||
background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg width='16' height='17' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M15 0H0v16h1V1h14V0z' fill='%23DFDFDF'/%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M2 1H1v14h1V2h12V1H2z' fill='%23fff'/%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M16 17H0v-1h15V0h1v17z' fill='%23000'/%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M15 1h-1v14H1v1h14V1z' fill='gray'/%3E%3Cpath fill='silver' d='M2 2h12v13H2z'/%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M11 6H4v1h1v1h1v1h1v1h1V9h1V8h1V7h1V6z' fill='%23000'/%3E%3C/svg%3E");
|
||||
height: 17px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
html[theme="f0ck98"] .sidebar-activity .comment-content.has-overflow:not(.expanded) .comment-content-inner::after {
|
||||
background: linear-gradient(to bottom, transparent, rgb(192, 192, 192));
|
||||
}
|
||||
|
||||
html[theme="f0ck98"] .item-sidebar-right, .index-sidebar-right, .global-sidebar-right {
|
||||
box-shadow: none!important;
|
||||
}
|
||||
|
||||
html[theme="f0ck98"] .sidebar-activity .read-more-btn, .sidebar-activity .see-less-btn {
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
html[theme="f0ck98"] .modal-content p {
|
||||
color: var(--black);
|
||||
}
|
||||
|
||||
html[theme="f0ck98"] .drop-zone {
|
||||
border: 2px dashed rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
@@ -1237,7 +1237,7 @@ html[theme='95'] .item-main-content .metadata {
|
||||
|
||||
.notif-count {
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
top: 0;
|
||||
right: -5px;
|
||||
background: var(--badge-nsfw);
|
||||
color: white;
|
||||
@@ -1650,6 +1650,10 @@ body.layout-modern .global-sidebar-right {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
body.sidebar-right-hidden #sidebar-drag-zone {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
/* Always show the handle on touch devices (no hover state available) */
|
||||
@media (pointer: coarse) {
|
||||
#sidebar-drag-zone {
|
||||
@@ -1984,7 +1988,7 @@ body.layout-modern .global-sidebar-right {
|
||||
right: 0;
|
||||
width: 100%;
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -3874,7 +3878,7 @@ html[theme="f0ck95d"] ._204863 {
|
||||
line-height: 1.7;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 2px 4px;
|
||||
padding: 0px 5px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
@@ -3893,8 +3897,6 @@ html[theme="f0ck95d"] ._204863 .location::before {
|
||||
background-size: contain;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
image-rendering: pixelated;
|
||||
image-rendering: crisp-edges;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
@@ -4026,11 +4028,338 @@ html[theme="f0ck95d"] .admin-search button {
|
||||
color: black;
|
||||
}
|
||||
|
||||
html[theme="f0ck95d"] .embed-responsive.embed-responsive-16by9 {
|
||||
html[theme="f0ck98"] .embed-responsive.embed-responsive-16by9 {
|
||||
background: black;
|
||||
|
||||
}
|
||||
|
||||
/* ==========================================
|
||||
f0ck98 Theme - Windows 98 Authentic Retro
|
||||
========================================== */
|
||||
|
||||
html[theme="f0ck98"] {
|
||||
--accent: #000080;
|
||||
--bg: silver;
|
||||
--black: #000000;
|
||||
--white: #ffffff;
|
||||
--gray: silver;
|
||||
--nav-bg: silver;
|
||||
--nav-brand-border: outset 2px silver;
|
||||
--nav-brand-bg: silver;
|
||||
--navigation-links-bg: silver;
|
||||
--navigation-links-background-linear-gradient: none;
|
||||
--navigation-links-border-color: #808080;
|
||||
--navigation-links-box-shadow: none;
|
||||
--nav-link-background-linear-gradient: none;
|
||||
--nav-link-box-shadow: inset -1px -1px #0a0a0a, inset 1px 1px #fff, inset -2px -2px grey, inset 2px 2px #dfdfdf;
|
||||
--nav-link-hover-bg: #000080;
|
||||
--nav-border-color: #808080;
|
||||
--dropdown-bg: silver;
|
||||
--dropdown-item-hover: #000080;
|
||||
--nav-brand-font: 'Pixelated MS Sans Serif', Arial, sans-serif;
|
||||
--font: 'Pixelated MS Sans Serif', Arial, sans-serif;
|
||||
--pagination-background: silver;
|
||||
--pagination-box-shadow: inset -1px -1px #0a0a0a, inset 1px 1px #fff, inset -2px -2px grey, inset 2px 2px #dfdfdf;
|
||||
--pagination-anchor-box-shadow: inset -1px -1px #0a0a0a, inset 1px 1px #fff, inset -2px -2px grey, inset 2px 2px #dfdfdf;
|
||||
--pagination-background-hover: #000080;
|
||||
--pagination-border-color: #808080;
|
||||
--metadata-bg: silver;
|
||||
--badge-bg: silver;
|
||||
--posts-meta-bg: rgba(192, 192, 192, 0.95);
|
||||
--badge-sfw: #008080;
|
||||
--badge-nsfw: #a72828;
|
||||
--badge-nsfl: #660000;
|
||||
--badge-tag: #808080;
|
||||
--scrollbar-color: silver;
|
||||
--scroller-bg: silver;
|
||||
--footbar-color: #000000;
|
||||
--loading-indicator-color: #000000;
|
||||
--img-border-width: 0;
|
||||
--img-border-color: #808080;
|
||||
--bg-gradient: none;
|
||||
--motd-bg: silver;
|
||||
--comment-bg: #ffffff;
|
||||
background-color: silver !important;
|
||||
background: silver !important;
|
||||
}
|
||||
|
||||
html[theme="f0ck98"] body {
|
||||
color: #000000;
|
||||
font-family: 'Pixelated MS Sans Serif', Arial, sans-serif;
|
||||
}
|
||||
|
||||
html[theme="f0ck98"] a {
|
||||
color: #000080;
|
||||
}
|
||||
|
||||
html[theme="f0ck98"] a:hover {
|
||||
color: #1084d0;
|
||||
}
|
||||
|
||||
/* Header & Titlebar */
|
||||
html[theme="f0ck98"] ._204863 {
|
||||
background: linear-gradient(90deg, #000080, #1084d0);
|
||||
color: #ffffff;
|
||||
width: 100%;
|
||||
border: 2px outset silver;
|
||||
line-height: 1.7;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 2px 4px;
|
||||
box-sizing: border-box;
|
||||
font-family: 'Pixelated MS Sans Serif', Arial, sans-serif;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
html[theme="f0ck98"] ._204863 .location {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
html[theme="f0ck98"] ._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="f0ck98"] ._204863::after {
|
||||
content: " _ ❐ ✕ ";
|
||||
font-family: 'Pixelated MS Sans Serif', Arial, sans-serif;
|
||||
font-size: 10px;
|
||||
font-weight: bold;
|
||||
background: silver;
|
||||
color: #000000;
|
||||
box-shadow: inset -1px -1px #0a0a0a, inset 1px 1px #fff, inset -2px -2px grey, inset 2px 2px #dfdfdf;
|
||||
padding: 0 4px;
|
||||
height: 14px;
|
||||
line-height: 12px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
margin-left: auto;
|
||||
letter-spacing: 1px;
|
||||
cursor: default;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
html[theme="f0ck98"] #tags .badge>a:first-child {
|
||||
text-shadow: none;
|
||||
}
|
||||
|
||||
html[theme="f0ck98"] .item_title {
|
||||
background: silver;
|
||||
color: #000000;
|
||||
border-left: 2px outset silver;
|
||||
border-right: 2px outset silver;
|
||||
box-shadow: inset -1px -1px #dfdfdf, inset 1px 1px grey;
|
||||
font-family: 'Pixelated MS Sans Serif', Arial, sans-serif;
|
||||
font-size: 11px;
|
||||
font-weight: normal;
|
||||
padding: 3px 8px;
|
||||
-webkit-font-smoothing: none;
|
||||
}
|
||||
|
||||
/* Navbar */
|
||||
html[theme="f0ck98"] .nav,
|
||||
html[theme="f0ck98"] .navbar {
|
||||
background: silver !important;
|
||||
border-bottom: 2px outset #fff;
|
||||
box-shadow: inset -1px -1px #0a0a0a, inset 1px 1px #dfdfdf, inset -2px -2px grey, inset 2px 2px #fff;
|
||||
}
|
||||
|
||||
html[theme="f0ck98"] .nav-link {
|
||||
color: #000000 !important;
|
||||
background: silver;
|
||||
box-shadow: inset -1px -1px #0a0a0a, inset 1px 1px #fff, inset -2px -2px grey, inset 2px 2px #dfdfdf;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
html[theme="f0ck98"] .nav-link:hover,
|
||||
html[theme="f0ck98"] .nav-link.active {
|
||||
background: #000080 !important;
|
||||
color: #ffffff !important;
|
||||
box-shadow: inset -1px -1px #fff, inset 1px 1px #0a0a0a, inset -2px -2px #dfdfdf, inset 2px 2px grey;
|
||||
}
|
||||
|
||||
/* Sidebars & Window Containers */
|
||||
html[theme="f0ck98"] .global-sidebar-left,
|
||||
html[theme="f0ck98"] .global-sidebar-right,
|
||||
html[theme="f0ck98"] .about,
|
||||
html[theme="f0ck98"] .modal-content,
|
||||
html[theme="f0ck98"] .card {
|
||||
background: silver;
|
||||
box-shadow: inset -1px -1px #0a0a0a, inset 1px 1px #dfdfdf, inset -2px -2px grey, inset 2px 2px #fff;
|
||||
color: #000000;
|
||||
padding: 6px;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
/* Item Grid Tiles */
|
||||
html[theme="f0ck98"] div.posts > a:not(.notif-item) {
|
||||
box-shadow: inset -1px -1px #0a0a0a, inset 1px 1px #fff, inset -2px -2px grey, inset 2px 2px #dfdfdf;
|
||||
background-color: silver;
|
||||
}
|
||||
|
||||
html[theme="f0ck98"] div.posts > a:not(.notif-item):hover,
|
||||
html[theme="f0ck98"] div.posts > a:not(.notif-item):focus-visible {
|
||||
box-shadow: inset -1px -1px #fff, inset 1px 1px #0a0a0a, inset -2px -2px #dfdfdf, inset 2px 2px grey;
|
||||
}
|
||||
|
||||
html[theme="f0ck98"] div.posts > a:not(.notif-item)::after {
|
||||
color: #000000;
|
||||
background: silver;
|
||||
border-top: 1px solid grey;
|
||||
font-family: 'Pixelated MS Sans Serif', Arial, sans-serif;
|
||||
text-shadow: none;
|
||||
}
|
||||
|
||||
/* Comments Section */
|
||||
html[theme="f0ck98"] .comment-item,
|
||||
html[theme="f0ck98"] .comments-container {
|
||||
background: #ffffff;
|
||||
color: #000000;
|
||||
box-shadow: inset -1px -1px #fff, inset 1px 1px grey, inset -2px -2px #dfdfdf, inset 2px 2px #0a0a0a;
|
||||
}
|
||||
|
||||
html[theme="f0ck98"] .comment-author {
|
||||
color: #000080 !important;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
html[theme="f0ck98"] .comment-content {
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
html[theme="f0ck98"] .comment-children {
|
||||
border-left: 1px dotted grey;
|
||||
}
|
||||
|
||||
html[theme="f0ck98"] .comment-actions button,
|
||||
html[theme="f0ck98"] .report-comment-btn,
|
||||
html[theme="f0ck98"] .reply-btn,
|
||||
html[theme="f0ck98"] .quote-btn {
|
||||
color: #000000 !important;
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
html[theme="f0ck98"] .comment-actions button svg,
|
||||
html[theme="f0ck98"] .report-comment-btn svg,
|
||||
html[theme="f0ck98"] .reply-btn i,
|
||||
html[theme="f0ck98"] .quote-btn i,
|
||||
html[theme="f0ck98"] .report-comment-btn i {
|
||||
color: #000000 !important;
|
||||
fill: #000000 !important;
|
||||
}
|
||||
|
||||
html[theme="f0ck98"] .report-comment-btn:hover,
|
||||
html[theme="f0ck98"] .report-comment-btn:hover svg,
|
||||
html[theme="f0ck98"] .report-comment-btn:hover i {
|
||||
color: #a72828 !important;
|
||||
fill: #a72828 !important;
|
||||
}
|
||||
|
||||
html[theme="f0ck98"] .reply-btn:hover,
|
||||
html[theme="f0ck98"] .quote-btn:hover,
|
||||
html[theme="f0ck98"] .reply-btn:hover i,
|
||||
html[theme="f0ck98"] .quote-btn:hover i {
|
||||
color: #000080 !important;
|
||||
}
|
||||
|
||||
/* Navigation Controls (.steuerung) */
|
||||
html[theme="f0ck98"] .steuerung a,
|
||||
html[theme="f0ck98"] .steuerung button,
|
||||
html[theme="f0ck98"] .steuerung.steuerung-icon a,
|
||||
html[theme="f0ck98"] .steuerung.steuerung-icon button {
|
||||
background: silver !important;
|
||||
color: #000000 !important;
|
||||
border: none !important;
|
||||
border-radius: 0 !important;
|
||||
box-shadow: inset -1px -1px #0a0a0a, inset 1px 1px #fff, inset -2px -2px grey, inset 2px 2px #dfdfdf !important;
|
||||
box-sizing: border-box !important;
|
||||
display: inline-flex !important;
|
||||
align-items: center !important;
|
||||
justify-content: center !important;
|
||||
min-height: 23px !important;
|
||||
min-width: 28px !important;
|
||||
width: auto !important;
|
||||
padding: 2px 8px !important;
|
||||
text-decoration: none !important;
|
||||
cursor: pointer !important;
|
||||
}
|
||||
|
||||
html[theme="f0ck98"] .steuerung a:active,
|
||||
html[theme="f0ck98"] .steuerung button:active,
|
||||
html[theme="f0ck98"] .steuerung.steuerung-icon a:active,
|
||||
html[theme="f0ck98"] .steuerung.steuerung-icon button:active {
|
||||
box-shadow: inset -1px -1px #fff, inset 1px 1px #0a0a0a, inset -2px -2px #dfdfdf, inset 2px 2px grey !important;
|
||||
}
|
||||
|
||||
html[theme="f0ck98"] .steuerung a:hover,
|
||||
html[theme="f0ck98"] .steuerung button:hover,
|
||||
html[theme="f0ck98"] .steuerung.steuerung-icon a:hover,
|
||||
html[theme="f0ck98"] .steuerung.steuerung-icon button:hover {
|
||||
color: #000080 !important;
|
||||
}
|
||||
|
||||
html[theme="f0ck98"] .steuerung.steuerung-icon .fa-solid {
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
html[theme="f0ck98"] .steuerung.steuerung-icon a:hover .fa-solid,
|
||||
html[theme="f0ck98"] .steuerung.steuerung-icon button:hover .fa-solid {
|
||||
color: #000080;
|
||||
}
|
||||
|
||||
/* Badges & Tags */
|
||||
html[theme="f0ck98"] .badge,
|
||||
html[theme="f0ck98"] .badge-light,
|
||||
html[theme="f0ck98"] .badge-dark,
|
||||
html[theme="f0ck98"] .badge-success,
|
||||
html[theme="f0ck98"] .badge-danger {
|
||||
background: silver;
|
||||
color: #000000;
|
||||
box-shadow: inset -1px -1px #0a0a0a, inset 1px 1px #fff, inset -2px -2px grey, inset 2px 2px #dfdfdf;
|
||||
border-radius: 0;
|
||||
text-shadow: none;
|
||||
}
|
||||
|
||||
/* Dropdown Menu */
|
||||
html[theme="f0ck98"] .dropdown-menu {
|
||||
background: silver;
|
||||
box-shadow: inset -1px -1px #0a0a0a, inset 1px 1px #dfdfdf, inset -2px -2px grey, inset 2px 2px #fff;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
html[theme="f0ck98"] .dropdown-item {
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
html[theme="f0ck98"] .dropdown-item:hover {
|
||||
background: #000080;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
/* Footer / Status Bar */
|
||||
html[theme="f0ck98"] footer,
|
||||
html[theme="f0ck98"] .footbar {
|
||||
background: silver;
|
||||
color: #000000;
|
||||
box-shadow: inset -1px -1px #dfdfdf, inset 1px 1px grey;
|
||||
padding: 3px 6px;
|
||||
font-family: 'Pixelated MS Sans Serif', Arial, sans-serif;
|
||||
}
|
||||
|
||||
/* fullscreen */
|
||||
html[res="fullscreen"] ._204863 {
|
||||
border-top: none;
|
||||
@@ -4623,16 +4952,17 @@ span.placeholder {
|
||||
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 */
|
||||
body.layout-modern .pagination-container-fluid {
|
||||
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 {
|
||||
margin: 10px auto;
|
||||
pointer-events: auto;
|
||||
@@ -4641,7 +4971,6 @@ body.layout-modern .pagination-container-fluid {
|
||||
-webkit-backdrop-filter: blur(12px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.4);
|
||||
padding: 2px;
|
||||
max-width: fit-content;
|
||||
}
|
||||
@@ -10032,7 +10361,6 @@ html[theme="orange"] .image-brand {
|
||||
.pagewrapper:has(.index-layout-wrapper),
|
||||
.pagewrapper:has(.item-layout-container),
|
||||
.pagewrapper:has(.meme-layout-wrapper),
|
||||
.pagewrapper:has(.chat-page),
|
||||
body.private-gate-active .pagewrapper {
|
||||
padding-right: 0 !important;
|
||||
}
|
||||
@@ -10177,7 +10505,7 @@ ol {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
padding: 0 15px;
|
||||
padding: 0 5px;
|
||||
flex-shrink: 0;
|
||||
margin-left: auto;
|
||||
}
|
||||
@@ -14705,7 +15033,6 @@ body.layout-modern .xd-score-wrapper {
|
||||
z-index: 950;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-end;
|
||||
align-items: flex-end;
|
||||
pointer-events: none;
|
||||
font-family: var(--font, monospace);
|
||||
@@ -14790,9 +15117,8 @@ body.scroller-active #gchat-reopen-bubble {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 300px;
|
||||
height: 400px;
|
||||
max-height: calc(100dvh - var(--navbar-h, 50px) - 15px);
|
||||
min-height: 180px;
|
||||
height: 40dvh;
|
||||
max-height: calc(100dvh - var(--navbar-h, 50px));
|
||||
background: var(--bg, #1a1a1a);
|
||||
border: 1px solid var(--nav-border-color, rgba(255, 255, 255, 0.08));
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.65);
|
||||
@@ -14977,12 +15303,22 @@ body.scroller-active #gchat-reopen-bubble {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
#gchat-topic {
|
||||
padding: 5px 10px;
|
||||
font-size: 0.78em;
|
||||
font-weight: 600;
|
||||
color: var(--bg, #111);
|
||||
background: var(--accent, #f2ef0b);
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.15);
|
||||
word-break: break-word;
|
||||
white-space: pre-wrap;
|
||||
flex-shrink: 0;
|
||||
opacity: 0.95;
|
||||
}
|
||||
|
||||
#gchat-messages {
|
||||
flex: 1 1 0%;
|
||||
min-height: 0;
|
||||
overflow-y: auto !important;
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
padding: 8px;
|
||||
display: flex;
|
||||
@@ -15089,9 +15425,7 @@ body.scroller-active #gchat-reopen-bubble {
|
||||
|
||||
/* Emoji images inside chat bubbles */
|
||||
.gchat-bubble .emoji {
|
||||
height: 1.4em;
|
||||
max-height: 28px;
|
||||
width: auto;
|
||||
height: 10.4em;
|
||||
vertical-align: middle;
|
||||
display: inline-block;
|
||||
}
|
||||
@@ -15199,15 +15533,33 @@ body.scroller-active #gchat-reopen-bubble {
|
||||
transform: scale(0.92);
|
||||
}
|
||||
|
||||
/* Emoji picker grid (above input area, inside panel) */
|
||||
#gchat-emoji-picker {
|
||||
display: none;
|
||||
flex-direction: column;
|
||||
max-height: 220px;
|
||||
grid-template-columns: repeat(6, 1fr);
|
||||
gap: 4px;
|
||||
padding: 8px;
|
||||
overflow-y: auto;
|
||||
max-height: 140px;
|
||||
background: var(--nav-bg, #111);
|
||||
border-top: 1px solid var(--nav-border-color, rgba(255, 255, 255, 0.08));
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
|
||||
#gchat-emoji-picker img {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
object-fit: contain;
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
padding: 2px;
|
||||
transition: background 0.1s;
|
||||
}
|
||||
|
||||
#gchat-emoji-picker img:hover {
|
||||
background: rgba(255, 255, 255, 0.12);
|
||||
}
|
||||
|
||||
/* :emoji inline autocomplete — portaled to body */
|
||||
#gchat-emoji-ac {
|
||||
position: fixed;
|
||||
@@ -15720,319 +16072,14 @@ body.scroller-active #gchat-reopen-bubble {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* ── Dedicated /chat Page ── */
|
||||
html:has(.chat-page),
|
||||
body:has(.chat-page) {
|
||||
height: 100% !important;
|
||||
overflow: hidden !important;
|
||||
}
|
||||
|
||||
body:has(.chat-page) .pagewrapper {
|
||||
height: calc(100dvh - var(--navbar-h, 60px)) !important;
|
||||
min-height: 0 !important;
|
||||
max-width: 100% !important;
|
||||
width: 100% !important;
|
||||
padding: 0 !important;
|
||||
margin: 0 !important;
|
||||
overflow: hidden !important;
|
||||
display: flex !important;
|
||||
flex-direction: column !important;
|
||||
transition: padding-right 0.3s ease-in-out !important;
|
||||
}
|
||||
|
||||
@media (min-width: 1000px) {
|
||||
body:has(.chat-page):not(.sidebar-right-hidden) .pagewrapper {
|
||||
padding-right: 300px !important;
|
||||
}
|
||||
}
|
||||
|
||||
body:has(.chat-page) footer,
|
||||
body:has(.chat-page) .footer {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.chat-page {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
max-width: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.site-chat-container {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
border-radius: 0;
|
||||
overflow: hidden;
|
||||
border: none;
|
||||
background: var(--bg, #1a1a1a);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.gchat-page-panel {
|
||||
position: relative !important;
|
||||
inset: auto !important;
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
max-width: 100% !important;
|
||||
min-width: 0 !important;
|
||||
border: none !important;
|
||||
border-radius: 0 !important;
|
||||
box-shadow: none !important;
|
||||
display: flex !important;
|
||||
flex-direction: column !important;
|
||||
transform: none !important;
|
||||
}
|
||||
|
||||
.gchat-page-panel #gchat-resize-h,
|
||||
.gchat-page-panel #gchat-resize-w,
|
||||
.gchat-page-panel #gchat-resize-e,
|
||||
.gchat-page-panel .gchat-header-btns {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Flex layout containers for chat body */
|
||||
.gchat-body-wrap {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#gchat-panel:not(.gchat-page-panel) .gchat-body-wrap {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.gchat-page-panel .gchat-body-wrap {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.gchat-main-area {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.gchat-page-panel #gchat-messages {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
padding: 14px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.gchat-page-panel .gchat-msg {
|
||||
max-width: 100% !important;
|
||||
}
|
||||
|
||||
/* Right-side user list in page mode */
|
||||
.gchat-page-panel #gchat-online {
|
||||
width: 250px;
|
||||
min-width: 200px;
|
||||
max-width: 300px;
|
||||
border-left: 1px solid var(--border-color, rgba(255, 255, 255, 0.1));
|
||||
background: rgba(0, 0, 0, 0.25);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.gchat-page-panel .gchat-online-header {
|
||||
padding: 12px 14px 4px 14px;
|
||||
font-size: 0.92rem;
|
||||
font-weight: 700;
|
||||
color: var(--accent, #f2ef0b);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.gchat-page-panel .gchat-online-subhead {
|
||||
padding: 2px 14px 10px 14px;
|
||||
font-size: 0.75rem;
|
||||
opacity: 0.6;
|
||||
border-bottom: 1px solid var(--border-color, rgba(255, 255, 255, 0.08));
|
||||
}
|
||||
|
||||
.gchat-page-panel .gchat-online-list {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
padding: 8px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.gchat-page-panel .gchat-online-user-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 6px 10px;
|
||||
border-radius: 6px;
|
||||
text-decoration: none;
|
||||
color: var(--fg, #e0e0e0);
|
||||
transition: background 0.15s ease;
|
||||
}
|
||||
|
||||
.gchat-page-panel .gchat-online-user-item:hover {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
.gchat-page-panel .gchat-online-user-avatar {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
flex-shrink: 0;
|
||||
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
|
||||
.gchat-page-panel .gchat-online-user-avatar.guest-avatar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
font-size: 0.8rem;
|
||||
color: #aaa;
|
||||
}
|
||||
|
||||
.gchat-page-panel .gchat-online-user-name {
|
||||
font-size: 0.88rem;
|
||||
font-weight: 500;
|
||||
.gchat-item-card-meta {
|
||||
font-size: 0.7rem;
|
||||
opacity: 0.5;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.gchat-page-panel .gchat-body-wrap {
|
||||
flex-direction: column !important;
|
||||
flex: 1 !important;
|
||||
min-height: 0 !important;
|
||||
height: 100% !important;
|
||||
overflow: hidden !important;
|
||||
}
|
||||
|
||||
.gchat-page-panel #gchat-online {
|
||||
width: 100% !important;
|
||||
max-width: 100% !important;
|
||||
height: 40px !important;
|
||||
min-height: 40px !important;
|
||||
max-height: 40px !important;
|
||||
border-left: none !important;
|
||||
border-bottom: 1px solid var(--border-color, rgba(255, 255, 255, 0.12)) !important;
|
||||
order: -1 !important;
|
||||
flex-shrink: 0 !important;
|
||||
display: flex !important;
|
||||
flex-direction: row !important;
|
||||
align-items: center !important;
|
||||
padding: 0 10px !important;
|
||||
overflow: hidden !important;
|
||||
background: rgba(0, 0, 0, 0.3) !important;
|
||||
}
|
||||
|
||||
.gchat-page-panel .gchat-online-header {
|
||||
padding: 0 !important;
|
||||
font-size: 0.8rem !important;
|
||||
white-space: nowrap !important;
|
||||
flex-shrink: 0 !important;
|
||||
gap: 4px !important;
|
||||
}
|
||||
|
||||
.gchat-page-panel .gchat-online-header span {
|
||||
font-size: 0.78rem !important;
|
||||
}
|
||||
|
||||
.gchat-page-panel .gchat-online-subhead {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.gchat-page-panel .gchat-online-list {
|
||||
flex: 1 !important;
|
||||
min-width: 0 !important;
|
||||
display: flex !important;
|
||||
flex-direction: row !important;
|
||||
flex-wrap: nowrap !important;
|
||||
overflow-x: auto !important;
|
||||
overflow-y: hidden !important;
|
||||
gap: 6px !important;
|
||||
padding: 0 4px !important;
|
||||
align-items: center !important;
|
||||
scrollbar-width: none !important;
|
||||
-webkit-overflow-scrolling: touch !important;
|
||||
touch-action: pan-x !important;
|
||||
}
|
||||
|
||||
.gchat-page-panel .gchat-online-list::-webkit-scrollbar {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.gchat-page-panel .gchat-online-user-item {
|
||||
flex-shrink: 0 !important;
|
||||
padding: 2px 8px !important;
|
||||
gap: 6px !important;
|
||||
border-radius: 12px !important;
|
||||
background: rgba(255, 255, 255, 0.08) !important;
|
||||
height: 28px !important;
|
||||
align-items: center !important;
|
||||
}
|
||||
|
||||
.gchat-page-panel .gchat-online-user-avatar {
|
||||
width: 20px !important;
|
||||
height: 20px !important;
|
||||
}
|
||||
|
||||
.gchat-page-panel .gchat-online-user-name {
|
||||
font-size: 0.75rem !important;
|
||||
max-width: 110px !important;
|
||||
}
|
||||
|
||||
.gchat-page-panel .gchat-main-area {
|
||||
flex: 1 !important;
|
||||
min-height: 0 !important;
|
||||
height: auto !important;
|
||||
display: flex !important;
|
||||
flex-direction: column !important;
|
||||
overflow: hidden !important;
|
||||
}
|
||||
|
||||
.gchat-page-panel #gchat-messages {
|
||||
flex: 1 !important;
|
||||
min-height: 0 !important;
|
||||
overflow-y: auto !important;
|
||||
padding: 10px !important;
|
||||
-webkit-overflow-scrolling: touch !important;
|
||||
}
|
||||
|
||||
.gchat-page-panel .gchat-msg {
|
||||
max-width: 92% !important;
|
||||
}
|
||||
|
||||
.gchat-page-panel #gchat-input-area {
|
||||
flex-shrink: 0 !important;
|
||||
background: var(--nav-bg, #0d0d0d) !important;
|
||||
padding-bottom: env(safe-area-inset-bottom, 0px) !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* =============================================
|
||||
SETTINGS PAGE – MOBILE OVERFLOW FIXES
|
||||
Prevents long i18n strings (Deutsch / Zange)
|
||||
@@ -16415,12 +16462,21 @@ body:has(.chat-page) .footer {
|
||||
color: #ccc;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
|
||||
font-size: 0.9em;
|
||||
word-break: break-word;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.info-table td a {
|
||||
color: var(--accent, #9f0);
|
||||
text-decoration: none;
|
||||
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 {
|
||||
|
||||
BIN
public/s/fonts/ms_sans_serif.woff2
Normal file
BIN
public/s/fonts/ms_sans_serif.woff2
Normal file
Binary file not shown.
BIN
public/s/fonts/ms_sans_serif_bold.woff2
Normal file
BIN
public/s/fonts/ms_sans_serif_bold.woff2
Normal file
Binary file not shown.
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 |
@@ -1160,17 +1160,13 @@ class CommentSystem {
|
||||
const author = openerEl.dataset.display || openerEl.dataset.username || 'System';
|
||||
const contentEl = body.querySelector('.comment-content');
|
||||
if (contentEl) {
|
||||
const LINE_MAX = 200;
|
||||
let rawText = (contentEl.dataset.raw || '').trim();
|
||||
if (rawText.includes('>') || rawText.includes('<') || rawText.includes('&')) {
|
||||
const txt = document.createElement('textarea');
|
||||
txt.innerHTML = rawText;
|
||||
rawText = txt.value;
|
||||
}
|
||||
// Preserve all lines but cap any single line exceeding LINE_MAX chars
|
||||
const lines = rawText.split('\n').map(line =>
|
||||
line.length > LINE_MAX ? line.substring(0, LINE_MAX) + '\u2026' : line
|
||||
);
|
||||
const lines = rawText.split('\n');
|
||||
const quote = `>>${id} \n>${author}\n${lines.map(line => `>${line}`).join('\n')}\n`;
|
||||
|
||||
if (isNew) {
|
||||
@@ -1625,17 +1621,25 @@ class CommentSystem {
|
||||
syncLevel(roots, list, false);
|
||||
}
|
||||
|
||||
// Maximum characters to fully render in the item view per comment
|
||||
static get ITEM_VIEW_MAX_CHARS() { return 2000; }
|
||||
// Maximum characters & lines to fully render in the item view per comment
|
||||
static get ITEM_VIEW_MAX_CHARS() { return 500; }
|
||||
static get ITEM_VIEW_MAX_LINES() { return 6; }
|
||||
|
||||
renderCommentContent(content, commentId = null, bypassTruncation = false) {
|
||||
if (!content) return '';
|
||||
|
||||
// Truncate extremely long comments before any processing
|
||||
// Truncate long comments before any processing
|
||||
let truncated = false;
|
||||
if (!bypassTruncation && content.length > CommentSystem.ITEM_VIEW_MAX_CHARS) {
|
||||
content = content.substring(0, CommentSystem.ITEM_VIEW_MAX_CHARS) + '\u2026';
|
||||
truncated = true;
|
||||
if (!bypassTruncation) {
|
||||
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;
|
||||
}
|
||||
if (content.length > CommentSystem.ITEM_VIEW_MAX_CHARS) {
|
||||
content = content.substring(0, CommentSystem.ITEM_VIEW_MAX_CHARS);
|
||||
truncated = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof marked === 'undefined') {
|
||||
@@ -1784,10 +1788,11 @@ class CommentSystem {
|
||||
if (trimmed.startsWith('>') && !trimmed.match(/^>>\d+/)) {
|
||||
const quoteContent = line.substring(line.indexOf('>') + 1);
|
||||
const quoteEmojis = window.f0ckSession?.quote_emojis === true;
|
||||
const escapedQuote = quoteContent.replace(/>/g, '>');
|
||||
const renderedContent = quoteEmojis
|
||||
? quoteContent.replace(/:([a-z0-9_]+):/g, (m, n) => this.renderEmoji(m, n))
|
||||
: quoteContent;
|
||||
return `<span class="greentext">>${renderedContent.replace(/>/g, '>')}</span>`;
|
||||
? escapedQuote.replace(/:([a-z0-9_]+):/g, (m, n) => this.renderEmoji(m, n))
|
||||
: escapedQuote;
|
||||
return `<span class="greentext">>${renderedContent}</span>`;
|
||||
}
|
||||
|
||||
// 2. Per-line limit to prevent marked.parse recursion on single giant lines
|
||||
@@ -3603,9 +3608,9 @@ class CommentSystem {
|
||||
// element whenever the raw content exceeds ITEM_VIEW_MAX_CHARS. Called after every
|
||||
// optimistic DOM insert to guarantee the button regardless of rendering path.
|
||||
_ensureTruncationButton(commentEl, rawContent) {
|
||||
console.log('[ensureBtn] called, rawContent.length:', rawContent?.length, 'threshold:', CommentSystem.ITEM_VIEW_MAX_CHARS);
|
||||
if (!rawContent || rawContent.length <= CommentSystem.ITEM_VIEW_MAX_CHARS) {
|
||||
console.log('[ensureBtn] below threshold, skipping');
|
||||
if (!rawContent) return;
|
||||
const lineCount = rawContent.split('\n').length;
|
||||
if (rawContent.length <= CommentSystem.ITEM_VIEW_MAX_CHARS && lineCount <= CommentSystem.ITEM_VIEW_MAX_LINES) {
|
||||
return;
|
||||
}
|
||||
const contentEl = commentEl.querySelector('.comment-content');
|
||||
|
||||
@@ -2438,7 +2438,7 @@ window.cancelAnimFrame = (function () {
|
||||
const isAdmin = !!pathname.match(/^\/admin/);
|
||||
const isMod = !!pathname.match(/^\/mod/);
|
||||
const isSettings = pathname.match(/\/settings\/?(?:$|\?)/);
|
||||
const isStatic = pathname.match(/\/(about|rules|terms|upload|subscriptions|stats|docs|discord|ranking|meme|memes|chat)($|\/|\?)/);
|
||||
const isStatic = pathname.match(/\/(about|rules|terms|upload|subscriptions|stats|docs|discord|ranking|meme|memes)($|\/|\?)/);
|
||||
const isUpload = pathname.match(/\/upload\/?(?:$|\?)/);
|
||||
const parts = pathname.split('/').filter(Boolean);
|
||||
const isItem = !pathname.match(/\/p\//) && (
|
||||
@@ -2446,9 +2446,8 @@ window.cancelAnimFrame = (function () {
|
||||
(parts.length >= 3 && (parts[0] === 'tag' || parts[0] === 'user' || parts[0] === 'h') && /^\d+$/.test(parts[parts.length - 1]))
|
||||
);
|
||||
const isMessages = !!pathname.match(/^\/messages(\/|$)/);
|
||||
const isChat = !!pathname.match(/^\/chat(\/|$)/);
|
||||
const isAbyss = !!pathname.match(/^\/abyss(\/|$|\?|#)/) || pathname === '/abyss';
|
||||
const isGrid = !isProfile && !isUserHall && !isUserHalls && !isHall && !isHalls && !isTags && !isComments && !isNotifs && !isItem && !isAdmin && !isMod && !isSettings && !isStatic && !isUpload && !isMessages && !isChat && !isAbyss;
|
||||
const isGrid = !isProfile && !isUserHall && !isUserHalls && !isHall && !isHalls && !isTags && !isComments && !isNotifs && !isItem && !isAdmin && !isMod && !isSettings && !isStatic && !isUpload && !isMessages && !isAbyss;
|
||||
|
||||
if (isItem) {
|
||||
isNavigating = false;
|
||||
@@ -4080,7 +4079,7 @@ window.cancelAnimFrame = (function () {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!isSpecialLink && (pathname === '/' || pathname.startsWith('/halls') || pathname.startsWith('/h/') || pathname.startsWith('/notifications') || pathname.startsWith('/tag') || pathname.startsWith('/user/') || pathname.match(/^\/(image|video|audio)/) || pathname.match(/\/p\/\d+/) || pathname.match(/^\/\d+/) || pathname.match(/^\/(about|rules|terms|upload|subscriptions|stats|docs|settings|admin|mod|ranking|messages|meme|memes|chat)/) || pathname.startsWith('/abyss'))) {
|
||||
if (!isSpecialLink && (pathname === '/' || pathname.startsWith('/halls') || pathname.startsWith('/h/') || pathname.startsWith('/notifications') || pathname.startsWith('/tag') || pathname.startsWith('/user/') || pathname.match(/^\/(image|video|audio)/) || pathname.match(/\/p\/\d+/) || pathname.match(/^\/\d+/) || pathname.match(/^\/(about|rules|terms|upload|subscriptions|stats|docs|settings|admin|mod|ranking|messages|meme|memes)/) || pathname.startsWith('/abyss'))) {
|
||||
e.preventDefault();
|
||||
e.stopImmediatePropagation();
|
||||
|
||||
@@ -4534,7 +4533,7 @@ window.cancelAnimFrame = (function () {
|
||||
(parts.length >= 3 && parts[0] === 'tag' && /^\d+$/.test(parts[parts.length - 1])) ||
|
||||
(parts.length >= 3 && parts[0] === 'user' && /^\d+$/.test(parts[parts.length - 1]))
|
||||
);
|
||||
const isSpecial = p.startsWith('/notifications') || p.startsWith('/tags') || p.startsWith('/user/') || p.startsWith('/subscriptions') || p.startsWith('/ranking') || p.startsWith('/chat');
|
||||
const isSpecial = p.startsWith('/notifications') || p.startsWith('/tags') || p.startsWith('/user/') || p.startsWith('/subscriptions') || p.startsWith('/ranking');
|
||||
const isGridLike = url.match(/\/p\/\d+/) || url.match(/[?&]page=\d+/) || p === '/';
|
||||
|
||||
window.f0ckDebug("[popstate] Navigation to:", url, { isItem, isSpecial, isGridLike });
|
||||
@@ -7568,29 +7567,48 @@ class NotificationSystem {
|
||||
|
||||
// Helper: apply a display name change to all relevant DOM nodes
|
||||
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
|
||||
const oldDisplayName = window.f0ckSession?.display_name || null;
|
||||
if (window.f0ckSession) window.f0ckSession.display_name = display_name || null;
|
||||
const oldDisplayName = isSelf ? (window.f0ckSession?.display_name || null) : null;
|
||||
if (isSelf && window.f0ckSession) {
|
||||
window.f0ckSession.display_name = display_name || null;
|
||||
}
|
||||
|
||||
const userName = user || window.f0ckSession?.user;
|
||||
|
||||
// Update ALL nav-display-name instances (handles the profile page having a duplicate ID)
|
||||
document.querySelectorAll('#nav-display-name').forEach(nameSpan => {
|
||||
// preserve the badges (Admin, Mod)
|
||||
for (let i = nameSpan.childNodes.length - 1; i >= 0; i--) {
|
||||
let node = nameSpan.childNodes[i];
|
||||
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*)(.*)'));
|
||||
node.nodeValue = (match ? match[1] : '') + (display_name || userName || '');
|
||||
break;
|
||||
// Update navbar display name only if this update is for the logged-in user
|
||||
if (isSelf) {
|
||||
document.querySelectorAll('#nav-display-name').forEach(nameSpan => {
|
||||
for (let i = nameSpan.childNodes.length - 1; i >= 0; i--) {
|
||||
let node = nameSpan.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;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 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');
|
||||
if (bracket) {
|
||||
const bracket = document.getElementById('username-bracket');
|
||||
if (bracket) {
|
||||
bracket.style.display = display_name ? 'inline' : 'none';
|
||||
}
|
||||
}
|
||||
|
||||
if (userName) {
|
||||
@@ -7617,6 +7635,7 @@ class NotificationSystem {
|
||||
|
||||
// Helper: apply a username color change to relevant DOM nodes
|
||||
this.applyUsernameColorUpdate = (username_color, user) => {
|
||||
const isSelf = !user || (window.f0ckSession?.user && user.toLowerCase() === window.f0ckSession.user.toLowerCase());
|
||||
const userName = user || window.f0ckSession?.user;
|
||||
if (!userName) return;
|
||||
|
||||
@@ -7663,22 +7682,21 @@ class NotificationSystem {
|
||||
}
|
||||
});
|
||||
|
||||
// Special case for settings page preview block
|
||||
if (window.f0ckSession && userName === window.f0ckSession.user) {
|
||||
const previewBlock = document.getElementById('live-profile-preview-box');
|
||||
if (previewBlock) {
|
||||
// Profile header display name color (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()) {
|
||||
const profileNameSpan = document.getElementById('profile-display-name');
|
||||
if (profileNameSpan) {
|
||||
if (username_color) {
|
||||
previewBlock.style.setProperty('--author-accent', username_color);
|
||||
previewBlock.style.setProperty('--author-border', username_color);
|
||||
profileNameSpan.style.color = username_color;
|
||||
} else {
|
||||
previewBlock.style.setProperty('--author-accent', 'var(--accent)');
|
||||
previewBlock.style.setProperty('--author-border', 'var(--accent)');
|
||||
profileNameSpan.style.removeProperty('color');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle the nav-display-name span elements (profile header and top navbar)
|
||||
if (window.f0ckSession && userName === window.f0ckSession.user) {
|
||||
// Navbar display name color (only for current user)
|
||||
if (isSelf) {
|
||||
document.querySelectorAll('#nav-display-name').forEach(el => {
|
||||
if (username_color) {
|
||||
el.style.color = username_color;
|
||||
@@ -7691,14 +7709,21 @@ class NotificationSystem {
|
||||
|
||||
// Helper: apply an avatar update to relevant DOM nodes
|
||||
this.applyAvatarUpdate = (avatar_file, user) => {
|
||||
const isSelf = !user || (window.f0ckSession?.user && user.toLowerCase() === window.f0ckSession.user.toLowerCase());
|
||||
const userName = user || window.f0ckSession?.user;
|
||||
if (!userName) return;
|
||||
|
||||
const newSrc = avatar_file ? `/a/${avatar_file}?t=${Date.now()}` : '/a/default.png';
|
||||
|
||||
// Update Nav Avatar & profile header Avatar (only for current user)
|
||||
if (window.f0ckSession && userName === window.f0ckSession.user) {
|
||||
document.querySelectorAll('.nav-avatar-img, .profile_head_avatar img').forEach(img => img.src = newSrc);
|
||||
// Update Nav Avatar only for current user
|
||||
if (isSelf) {
|
||||
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)
|
||||
@@ -8743,23 +8768,39 @@ class NotificationSystem {
|
||||
// Build DOM nodes imperatively (avoids Sanitizer stripping inline border-color)
|
||||
const fragment = document.createDocumentFragment();
|
||||
data.favs.forEach(fav => {
|
||||
const avatarUrl = fav.avatar_file
|
||||
? `/a/${fav.avatar_file}`
|
||||
: (fav.avatar ? `/t/${fav.avatar}.webp` : '/a/default.png');
|
||||
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 a = document.createElement('a');
|
||||
a.href = `/user/${fav.user.toLowerCase()}`;
|
||||
a.setAttribute('tooltip', fav.display_name || fav.user);
|
||||
a.setAttribute('flow', 'up');
|
||||
const img = document.createElement('img');
|
||||
img.src = '/s/img/ghost_fav.svg';
|
||||
img.style.height = '32px';
|
||||
img.style.width = '32px';
|
||||
|
||||
const img = document.createElement('img');
|
||||
img.src = avatarUrl;
|
||||
img.style.height = '32px';
|
||||
img.style.width = '32px';
|
||||
if (fav.username_color) img.style.borderColor = fav.username_color;
|
||||
a.appendChild(img);
|
||||
fragment.appendChild(a);
|
||||
} else {
|
||||
const avatarUrl = fav.avatar_file
|
||||
? `/a/${fav.avatar_file}`
|
||||
: (fav.avatar ? `/t/${fav.avatar}.webp` : '/a/default.png');
|
||||
|
||||
a.appendChild(img);
|
||||
fragment.appendChild(a);
|
||||
const a = document.createElement('a');
|
||||
a.href = `/user/${fav.user.toLowerCase()}`;
|
||||
a.setAttribute('tooltip', fav.display_name || fav.user);
|
||||
a.setAttribute('flow', 'up');
|
||||
|
||||
const img = document.createElement('img');
|
||||
img.src = avatarUrl;
|
||||
img.style.height = '32px';
|
||||
img.style.width = '32px';
|
||||
if (fav.username_color) img.style.borderColor = fav.username_color;
|
||||
|
||||
a.appendChild(img);
|
||||
fragment.appendChild(a);
|
||||
}
|
||||
});
|
||||
|
||||
favsContainer.innerHTML = '';
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
const layoutToggle = document.getElementById('use_new_layout_toggle');
|
||||
if (layoutToggle) {
|
||||
|
||||
@@ -153,12 +153,6 @@
|
||||
const renderCommentContent = (content, commentId = null, itemId = null) => {
|
||||
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') {
|
||||
return escapeHtml(content)
|
||||
.replace(/:([a-z0-9_]+):/g, (m, n) => renderEmoji(m, n));
|
||||
@@ -268,10 +262,11 @@
|
||||
// Manual greentext handling — apply emoji if the user preference allows it
|
||||
const quoteContent = line.substring(line.indexOf('>') + 1);
|
||||
const quoteEmojis = window.f0ckSession?.quote_emojis === true;
|
||||
const escapedQuote = quoteContent.replace(/>/g, '>');
|
||||
const rendered = quoteEmojis
|
||||
? quoteContent.replace(/:([a-z0-9_]+):/g, (m, n) => renderEmoji(m, n))
|
||||
: quoteContent;
|
||||
return `<span class="greentext">>${rendered.replace(/>/g, '>')}</span>`;
|
||||
? escapedQuote.replace(/:([a-z0-9_]+):/g, (m, n) => renderEmoji(m, n))
|
||||
: escapedQuote;
|
||||
return `<span class="greentext">>${rendered}</span>`;
|
||||
}
|
||||
|
||||
// Per-line limit to prevent marked.parse recursion on single giant lines
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
}
|
||||
};
|
||||
|
||||
const themes = window.f0ckThemes || ['amoled', 'atmos', 'f0ck', 'f0ck95d', 'iced', 'orange', 'p1nk', '4d'];
|
||||
const themes = window.f0ckThemes || ['amoled', 'atmos', 'f0ck', 'f0ck95d', 'f0ck98', 'iced', 'orange', 'p1nk', '4d'];
|
||||
const defaultTheme = window.f0ckDefaultTheme || (window.f0ckSession && window.f0ckSession.default_theme) || themes[0] || 'amoled';
|
||||
|
||||
const setTheme = (theme) => {
|
||||
|
||||
@@ -306,7 +306,12 @@ if (!window.UserCommentSystem) {
|
||||
const trimmed = line.trimStart();
|
||||
if (trimmed.startsWith('>') && !trimmed.match(/^>>\d+/)) {
|
||||
const quoteContent = line.substring(line.indexOf('>') + 1);
|
||||
return `<span class="greentext">>${quoteContent.replace(/>/g, '>')}</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
|
||||
|
||||
@@ -2,7 +2,7 @@ import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
const cssDir = path.join(process.cwd(), 'public/s/css');
|
||||
const files = ['f0ckm.css', 'v0ck.css'];
|
||||
const files = ['f0ckm.css', 'v0ck.css', '98.css'];
|
||||
const outputFile = path.join(cssDir, 'bundle.css');
|
||||
|
||||
let combined = '';
|
||||
|
||||
@@ -460,4 +460,15 @@ export default new class {
|
||||
console.error(`[ERROR REF ${errId}] ${context}:`, err);
|
||||
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');
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
"meme": "Meme",
|
||||
"halls": "Hallen",
|
||||
"tags": "Tags",
|
||||
"chat": "Chat",
|
||||
"search": "Suchen",
|
||||
"random": "Zufall",
|
||||
"profile": "Profil",
|
||||
@@ -166,6 +165,10 @@
|
||||
"hide_item_ratings_hint": "Zeige keine SFW/NSFW-Bewertungsindikatoren bei Elementen auf der Hauptseite",
|
||||
"disable_swiping": "Wischen 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_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",
|
||||
@@ -536,6 +539,7 @@
|
||||
"subscribe_uploads_btn": "Benutzer für Uploads abonnieren",
|
||||
"no_uploads": "Keine Uploads gefunden",
|
||||
"no_favs": "Keine Favoriten",
|
||||
"private_favorites": "private Favoriten",
|
||||
"back_to_profile": "Zurück zum Profil",
|
||||
"ban_modal_title": "Benutzer sperren",
|
||||
"ban_modal_reason": "Grund:",
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
"meme": "Meme",
|
||||
"halls": "Halls",
|
||||
"tags": "Tags",
|
||||
"chat": "Chat",
|
||||
"search": "Search",
|
||||
"random": "Random",
|
||||
"profile": "Profile",
|
||||
@@ -166,6 +165,10 @@
|
||||
"hide_item_ratings_hint": "Don't show SFW/NSFW rating indicators on main page items",
|
||||
"disable_swiping": "Disable Swiping",
|
||||
"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_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",
|
||||
@@ -540,6 +543,7 @@
|
||||
"subscribe_uploads_btn": "Subscribe user to uploads",
|
||||
"no_uploads": "no uploads found",
|
||||
"no_favs": "no favorites",
|
||||
"private_favorites": "private favorites",
|
||||
"back_to_profile": "Back to Profile",
|
||||
"ban_modal_title": "Ban User",
|
||||
"ban_modal_reason": "Reason:",
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
"meme": "meme",
|
||||
"halls": "Hallen",
|
||||
"tags": "Tags",
|
||||
"chat": "Chat",
|
||||
"search": "Zoeken",
|
||||
"random": "Willekeurig",
|
||||
"profile": "profiel",
|
||||
@@ -166,6 +165,10 @@
|
||||
"hide_item_ratings_hint": "Toon geen SFW/NSFW beoordelingsindicatoren op hoofdpagina items",
|
||||
"disable_swiping": "Swipen uitschakelen",
|
||||
"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_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",
|
||||
@@ -536,6 +539,7 @@
|
||||
"subscribe_uploads_btn": "Gebruiker abonneren op uploads",
|
||||
"no_uploads": "geen uploads gevonden",
|
||||
"no_favs": "geen favorieten",
|
||||
"private_favorites": "privé favorieten",
|
||||
"back_to_profile": "Terug naar Profiel",
|
||||
"ban_modal_title": "Gebruiker Bannen",
|
||||
"ban_modal_reason": "Reden:",
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
"meme": "Memen",
|
||||
"halls": "Hallen",
|
||||
"tags": "Etiketten",
|
||||
"chat": "Geschwafel",
|
||||
"search": "Suche",
|
||||
"random": "Zufall",
|
||||
"profile": "Profil",
|
||||
@@ -166,6 +165,10 @@
|
||||
"hide_item_ratings_hint": "Zeige keine SFW/NSFW-Bewertungsindikatoren bei Elementen auf der Hauptseite",
|
||||
"disable_swiping": "Wischen deaktivieren",
|
||||
"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_hint": "Anstatt dat Scroll-Zoom-Moped aufzumache, wird n Bild beim Klickle uff volle Größ im Bereich uffgepumpt.",
|
||||
"enable_bg_blur": "Hintergrundunschärfe aktivieren",
|
||||
@@ -537,6 +540,7 @@
|
||||
"subscribe_uploads_btn": "Benutzer für Aufladierungen abonnieren",
|
||||
"no_uploads": "keine Aufladierungen gefunden",
|
||||
"no_favs": "keine Favoriten",
|
||||
"private_favorites": "private Favoriten",
|
||||
"back_to_profile": "Zurück zum Profil",
|
||||
"ban_modal_title": "Benutzer sperren",
|
||||
"ban_modal_reason": "Grund:",
|
||||
|
||||
@@ -136,8 +136,60 @@ const xdScoreMeta = (score) => {
|
||||
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 {
|
||||
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;
|
||||
|
||||
// --- title: prefix — search items.title instead of the tags table ---
|
||||
@@ -428,7 +480,18 @@ export default {
|
||||
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;
|
||||
|
||||
// --- title: prefix — search items.title instead of the tags table ---
|
||||
@@ -705,7 +768,7 @@ export default {
|
||||
link.suffix = '';
|
||||
}
|
||||
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"
|
||||
left join "user" on "user".id = "favorites".user_id
|
||||
left join "user_options" on "user_options".user_id = "favorites".user_id
|
||||
@@ -884,7 +947,14 @@ export default {
|
||||
tmp
|
||||
};
|
||||
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 hall = rawHall || null;
|
||||
|
||||
|
||||
@@ -1428,8 +1428,8 @@ export default (router, tpl) => {
|
||||
const userId = newUser[0].id;
|
||||
|
||||
await db`
|
||||
INSERT INTO user_options (user_id, mode, theme, fullscreen, avatar, avatar_file, use_new_layout, disable_autoplay, disable_swiping)
|
||||
VALUES (${userId}, 3, 'amoled', 0, NULL, 'default.png', ${getDefaultLayout() === 'modern'}, ${cfg.websrv.enable_autoplay === false}, ${cfg.websrv.enable_swiping === false})
|
||||
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}, ${cfg.websrv.user_alternative_infobox !== false})
|
||||
`;
|
||||
|
||||
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),
|
||||
mode: query.mode !== undefined ? +query.mode : req.mode,
|
||||
ratings: ratingsArr,
|
||||
session: !!req.session,
|
||||
exclude: req.session ? (req.session.excluded_tags || []) : [],
|
||||
session: req.session,
|
||||
user_id: req.session?.id,
|
||||
is_admin: req.session?.admin,
|
||||
exclude: req.session ? (req.session.excluded_tags || []) : [],
|
||||
fav: query.fav === 'true',
|
||||
random: isRandom,
|
||||
strict: query.strict === '1' || query.strict === 'true' || req.session?.strict_mode,
|
||||
@@ -231,6 +232,19 @@ export default (router, tpl) => {
|
||||
});
|
||||
|
||||
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({
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
|
||||
@@ -1066,7 +1066,7 @@ export default router => {
|
||||
}
|
||||
|
||||
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"
|
||||
left join "user" on "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
|
||||
group.put(/\/username_color/, lib.loggedin, async (req, res) => {
|
||||
const { color } = req.post;
|
||||
|
||||
@@ -47,25 +47,6 @@ function buildBgHostRegex() {
|
||||
|
||||
export default (router, tpl) => {
|
||||
|
||||
// GET /chat — Dedicated site chat page
|
||||
router.get('/chat', async (req, res) => {
|
||||
if (!cfg.websrv.enable_global_chat) {
|
||||
return res.reply({ code: 404, body: 'Not found' });
|
||||
}
|
||||
if (!req.session) {
|
||||
return res.redirect('/login');
|
||||
}
|
||||
if (req.session.banned) {
|
||||
return res.reply({ code: 403, body: 'Access denied: You are banned' });
|
||||
}
|
||||
return res.html(tpl.render('chat', {
|
||||
session: req.session,
|
||||
hidePagination: true,
|
||||
link: { main: '/chat', path: '/chat' },
|
||||
domain: cfg.main.url.domain
|
||||
}, req));
|
||||
});
|
||||
|
||||
// GET /api/chat — fetch recent messages
|
||||
router.get('/api/chat', async (req, res) => {
|
||||
if (!cfg.websrv.enable_global_chat) {
|
||||
|
||||
@@ -99,8 +99,9 @@ export default (router, tpl) => {
|
||||
ratings: ratingsArr,
|
||||
mime: mime,
|
||||
fav: true,
|
||||
session: !!req.session,
|
||||
session: req.session,
|
||||
user_id: req.session?.id,
|
||||
is_admin: req.session?.admin,
|
||||
random: isRandom
|
||||
});
|
||||
if (favs && 'items' in favs) {
|
||||
@@ -236,8 +237,9 @@ export default (router, tpl) => {
|
||||
fav: req.params.mode == 'favs',
|
||||
mode: req.mode,
|
||||
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,
|
||||
is_admin: req.session?.admin,
|
||||
exclude: req.session ? (req.session.excluded_tags || []) : [],
|
||||
url: decodeURIComponent(req.url.pathname || req.url),
|
||||
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`);
|
||||
|
||||
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
|
||||
if (mode !== 'item') {
|
||||
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.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.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.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.");
|
||||
}
|
||||
|
||||
// reCAPTCHA verification
|
||||
if (cfg.recaptcha?.enabled && cfg.recaptcha?.secret_key) {
|
||||
// reCAPTCHA verification (bypassed for .onion requests as Google reCAPTCHA cannot validate .onion domains)
|
||||
const isOnion = lib.isOnionRequest(req);
|
||||
if (!isOnion && cfg.recaptcha?.enabled && cfg.recaptcha?.secret_key) {
|
||||
const rcToken = req.post['g-recaptcha-response'];
|
||||
if (!rcToken) return renderError("Please complete the reCAPTCHA.");
|
||||
try {
|
||||
@@ -167,8 +168,8 @@ export default (router, tpl) => {
|
||||
const avatarFile = 'default.png';
|
||||
|
||||
await db`
|
||||
insert into user_options (user_id, mode, theme, fullscreen, avatar, avatar_file, use_new_layout, disable_autoplay, disable_swiping)
|
||||
values (${userId}, 3, 'amoled', 0, ${avatarId}, ${avatarFile}, ${getDefaultLayout() === 'modern'}, ${cfg.websrv.enable_autoplay === false}, ${cfg.websrv.enable_swiping === false})
|
||||
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}, ${cfg.websrv.user_alternative_infobox !== false})
|
||||
`;
|
||||
} catch (err) {
|
||||
console.error(`[REGISTER] DB Error during user creation:`, err);
|
||||
|
||||
149
src/index.mjs
149
src/index.mjs
@@ -49,7 +49,7 @@ const gateOptions = {
|
||||
},
|
||||
cloudflare_status: {
|
||||
status: 'ok',
|
||||
location: 'Frankfurt',
|
||||
location: cfg.websrv.private_society_gate_location || 'Frankfurt',
|
||||
name: 'Cloudflare',
|
||||
status_text: 'Working',
|
||||
},
|
||||
@@ -79,11 +79,13 @@ const nginx502Fallback = `<html>
|
||||
</html>`;
|
||||
|
||||
// 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 _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="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"
|
||||
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" />
|
||||
${_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;"
|
||||
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;">
|
||||
@@ -264,8 +266,9 @@ const gateLoginInjection = `
|
||||
if (_sb.length > 12) _sb = _sb.slice(-12);
|
||||
});
|
||||
</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
|
||||
@@ -282,11 +285,40 @@ try {
|
||||
}
|
||||
|
||||
// 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) {
|
||||
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
|
||||
if (req) {
|
||||
@@ -305,9 +337,9 @@ function buildGatePage(req) {
|
||||
// Patch the <title> to include the domain — matches real Cloudflare behaviour
|
||||
html = html.replace(
|
||||
'<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;
|
||||
}
|
||||
|
||||
@@ -316,7 +348,14 @@ const nginx502 = (cfg.websrv.private_society && cfg.websrv.private_society_gate
|
||||
? null
|
||||
: 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 (_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=()');
|
||||
// Encourage connection reuse — helps external tools like ShareX avoid repeated TCP/TLS handshakes
|
||||
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) {
|
||||
res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
|
||||
}
|
||||
@@ -604,7 +659,7 @@ process.on('uncaughtException', err => {
|
||||
}
|
||||
return;
|
||||
}
|
||||
const availableThemes = cfg.websrv.themes || ['amoled', 'atmos', 'f0ck', 'f0ck95d', 'iced', 'orange', 'p1nk', '4d'];
|
||||
const availableThemes = cfg.websrv.themes || ['amoled', 'atmos', 'f0ck', 'f0ck95d', 'f0ck98', 'iced', 'orange', 'p1nk', '4d'];
|
||||
const defaultTheme = cfg.websrv.theme || 'amoled';
|
||||
req.theme = (req.cookies?.theme && availableThemes.includes(req.cookies.theme)) ? req.cookies.theme : defaultTheme;
|
||||
req.fullscreen = req.cookies.fullscreen || 0;
|
||||
@@ -617,7 +672,7 @@ process.on('uncaughtException', err => {
|
||||
user = [_cachedRow];
|
||||
} else {
|
||||
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"
|
||||
left join "user" on "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';
|
||||
return;
|
||||
}
|
||||
// For page requests, return 502 Bad Gateway for all paths except homepage (which shows the gate)
|
||||
if (req.url.pathname !== '/') {
|
||||
// For page requests, serve the gate for all paths (custom template or fallback)
|
||||
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));
|
||||
req.url.pathname = '/private_society_bypass';
|
||||
return;
|
||||
}
|
||||
// Homepage: in cloudflare dynamic mode serve gate directly; otherwise fall through to template
|
||||
if (nginx502 === null) {
|
||||
} else if (nginx502 === null) {
|
||||
// Homepage in cloudflare dynamic mode: serve the CF gate directly
|
||||
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;
|
||||
}
|
||||
req.url.pathname = '/private_society_bypass';
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -1292,6 +1350,7 @@ process.on('uncaughtException', err => {
|
||||
show_mime_picker: cfg.websrv.show_mime_picker !== false,
|
||||
private_society: cfg.websrv.private_society || false,
|
||||
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,
|
||||
web_url_upload: !!cfg.websrv.web_url_upload,
|
||||
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
|
||||
// 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.
|
||||
globals.t = perRequestT;
|
||||
globals.lang = perRequestLang;
|
||||
globals.recaptcha_enabled = perRequestRecaptcha;
|
||||
|
||||
// Resolve per-request infobox preference
|
||||
// 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')
|
||||
? req.session.use_alternative_infobox
|
||||
: (req && !req.session
|
||||
const activeReq = req || effectiveReq;
|
||||
const useAltInfobox = (activeReq && activeReq.session && typeof activeReq.session.use_alternative_infobox === 'boolean')
|
||||
? activeReq.session.use_alternative_infobox
|
||||
: (activeReq && !activeReq.session
|
||||
? false
|
||||
: (data && typeof data.user_alternative_infobox === 'boolean'
|
||||
? data.user_alternative_infobox
|
||||
: (cfg.websrv.user_alternative_infobox !== false)));
|
||||
|
||||
const useAltSteuerung = (req && req.session && typeof req.session.use_alternative_steuerung === 'boolean')
|
||||
? req.session.use_alternative_steuerung
|
||||
: (req && !req.session
|
||||
const useAltSteuerung = (activeReq && activeReq.session && typeof activeReq.session.use_alternative_steuerung === 'boolean')
|
||||
? activeReq.session.use_alternative_steuerung
|
||||
: (activeReq && !activeReq.session
|
||||
? false
|
||||
: (data && typeof data.user_alternative_steuerung === 'boolean'
|
||||
? data.user_alternative_steuerung
|
||||
@@ -1431,11 +1503,12 @@ process.on('uncaughtException', err => {
|
||||
data = Object.assign({}, globals, data || {}, {
|
||||
t: perRequestT,
|
||||
lang: perRequestLang,
|
||||
recaptcha_enabled: perRequestRecaptcha,
|
||||
user_alternative_infobox: useAltInfobox,
|
||||
user_alternative_steuerung: useAltSteuerung,
|
||||
user_banner_enabled: cfg.websrv.user_banner_enabled !== false,
|
||||
comment_display_mode: (req && req.session && typeof req.session.comment_display_mode === 'number')
|
||||
? req.session.comment_display_mode
|
||||
comment_display_mode: (activeReq && activeReq.session && typeof activeReq.session.comment_display_mode === 'number')
|
||||
? activeReq.session.comment_display_mode
|
||||
: (data && typeof data.comment_display_mode === 'number'
|
||||
? data.comment_display_mode
|
||||
: (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)];
|
||||
}
|
||||
|
||||
if (req) {
|
||||
if (req.mode !== undefined) data.mode = req.mode;
|
||||
data.theme = req.theme || req.cookies?.theme || cfg.websrv.theme || 'f0ck';
|
||||
if (!data.url) data.url = req.url;
|
||||
data.user_strict_bool = (req.session && req.session.strict_mode) ? true : false;
|
||||
data.user_logged_in_bool = !!req.session;
|
||||
data.csrf_token = req.session?.csrf_token || '';
|
||||
data.max_file_size = lib.formatSize(cfg.main.maxfilesize * (req.session?.admin ? cfg.main.adminmultiplier : 1));
|
||||
data.max_file_size_bytes = Math.floor(cfg.main.maxfilesize * (req.session?.admin ? cfg.main.adminmultiplier : 1));
|
||||
if (activeReq) {
|
||||
data.recaptcha_enabled = perRequestRecaptcha;
|
||||
if (activeReq.mode !== undefined) data.mode = activeReq.mode;
|
||||
data.theme = activeReq.theme || activeReq.cookies?.theme || cfg.websrv.theme || 'f0ck';
|
||||
if (!data.url) data.url = activeReq.url;
|
||||
data.user_strict_bool = (activeReq.session && activeReq.session.strict_mode) ? true : false;
|
||||
data.user_logged_in_bool = !!activeReq.session;
|
||||
data.csrf_token = activeReq.session?.csrf_token || '';
|
||||
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;
|
||||
} else {
|
||||
data.recaptcha_enabled = perRequestRecaptcha;
|
||||
data.theme = data.theme || cfg.websrv.theme || 'f0ck';
|
||||
data.user_strict_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 () => {
|
||||
var res = await fetch('/api/v2/admin/users/activate', {
|
||||
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 })
|
||||
});
|
||||
var data = await res.json();
|
||||
@@ -339,7 +339,7 @@
|
||||
ModAction.confirm('Ban User', 'Reason for banning <strong>' + escHTML(userName) + '</strong>?', async (reason) => {
|
||||
var res = await fetch('/api/v2/admin/ban', {
|
||||
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' })
|
||||
});
|
||||
var data = await res.json();
|
||||
@@ -359,7 +359,7 @@
|
||||
ModAction.confirm('Unban User', 'Unban account for <strong>' + escHTML(userName) + '</strong>?', async () => {
|
||||
var res = await fetch('/api/v2/admin/unban', {
|
||||
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 })
|
||||
});
|
||||
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 () => {
|
||||
var res = await fetch('/api/v2/admin/users/bulk-delete-items', {
|
||||
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 })
|
||||
});
|
||||
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 () => {
|
||||
var res = await fetch('/api/v2/admin/users/bulk-delete-comments', {
|
||||
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 })
|
||||
});
|
||||
var data = await res.json();
|
||||
@@ -422,7 +422,7 @@
|
||||
ModAction.confirm('Set Display Name', hint, async (newName) => {
|
||||
var res = await fetch('/api/v2/admin/users/set-display-name', {
|
||||
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 || '' })
|
||||
});
|
||||
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 () => {
|
||||
var res = await fetch('/api/v2/admin/users/lock-layout', {
|
||||
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 })
|
||||
});
|
||||
var data = await res.json();
|
||||
@@ -481,7 +481,7 @@
|
||||
var mode = document.getElementById('force-mode-select').value;
|
||||
var res = await fetch('/api/v2/admin/users/lock-layout', {
|
||||
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 })
|
||||
});
|
||||
var data = await res.json();
|
||||
@@ -616,7 +616,7 @@
|
||||
try {
|
||||
var res = await fetch('/api/v2/admin/users/create', {
|
||||
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 })
|
||||
});
|
||||
var data = await res.json();
|
||||
@@ -662,7 +662,7 @@
|
||||
var role = selectEl.value;
|
||||
var res = await fetch('/api/v2/admin/users/set-role', {
|
||||
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 })
|
||||
});
|
||||
var data = await res.json();
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
@include(snippets/header)
|
||||
<div class="pagewrapper">
|
||||
<div id="main" class="chat-page">
|
||||
<div id="site-chat-container" class="site-chat-container">
|
||||
<!-- Site chat widget is mounted here by globalchat.js -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@include(snippets/footer)
|
||||
@@ -197,7 +197,11 @@
|
||||
</span>
|
||||
<span class="badge" id="favs" @if(!item.favorites.length) hidden@endif>
|
||||
@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>
|
||||
@endif
|
||||
@endeach
|
||||
</span>
|
||||
@if(enable_xd_score && item.xd_score > 0)
|
||||
@@ -296,7 +300,7 @@
|
||||
@if(item.src.short)
|
||||
<tr>
|
||||
<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>
|
||||
@endif
|
||||
</table>
|
||||
|
||||
@@ -157,7 +157,11 @@
|
||||
</div>
|
||||
<span id="favs" @if(!item.favorites.length) hidden@endif style="margin-top: 5px;">
|
||||
@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>
|
||||
@endif
|
||||
@endeach
|
||||
</span>
|
||||
|
||||
@@ -238,7 +242,7 @@
|
||||
@if(item.src.short)
|
||||
<tr>
|
||||
<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>
|
||||
@endif
|
||||
</table>
|
||||
|
||||
@@ -320,6 +320,20 @@
|
||||
</label>
|
||||
<small class="text-muted" style="margin-left: 25px;">{{ t('settings.disable_swiping_hint') }}</small>
|
||||
</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;">
|
||||
<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>
|
||||
|
||||
@@ -56,6 +56,7 @@
|
||||
@if(!private_society || session)
|
||||
@if(development || (private_society && !session))
|
||||
<link rel="stylesheet" href="/s/css/f0ckm.css?v={{ ts }}">
|
||||
<link rel="stylesheet" href="/s/css/98.css?v={{ ts }}">
|
||||
@endif
|
||||
@if(development)
|
||||
<link rel="stylesheet" href="/s/css/v0ck.css?v={{ ts }}">
|
||||
|
||||
@@ -26,9 +26,6 @@
|
||||
</div>
|
||||
@endif
|
||||
<a href="/tags" title="{{ t('nav.tags') }}"><i class="fa-solid fa-tags"></i></a>
|
||||
@if(enable_global_chat)
|
||||
<a href="/chat" title="{{ t('nav.chat') }}"><i class="fa-solid fa-comments"></i></a>
|
||||
@endif
|
||||
@if(abyss_enabled)
|
||||
<a href="/abyss" title="{{ t('nav.abyss') }}"><i class="fa-solid fa-dice-d6"></i></a>
|
||||
@endif
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
<div class="profile_head_username">
|
||||
<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">
|
||||
<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)
|
||||
<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>
|
||||
@@ -142,6 +142,14 @@
|
||||
</div>
|
||||
@if(!user.is_ghost)
|
||||
<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">
|
||||
{{ 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>
|
||||
@@ -164,6 +172,7 @@
|
||||
@else
|
||||
{{ t('profile.no_favs') }}
|
||||
@endif
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@@ -509,18 +518,27 @@
|
||||
nameContainer.style.display = 'flex';
|
||||
nameEditContainer.style.display = 'none';
|
||||
|
||||
const nameSpan = document.getElementById('nav-display-name');
|
||||
if (nameSpan) {
|
||||
nameSpan.style.color = color;
|
||||
for (let i = nameSpan.childNodes.length - 1; i >= 0; i--) {
|
||||
let node = nameSpan.childNodes[i];
|
||||
const updateNameNode = (el, text) => {
|
||||
if (!el) return;
|
||||
for (let i = el.childNodes.length - 1; i >= 0; i--) {
|
||||
let node = el.childNodes[i];
|
||||
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*)(.*)'));
|
||||
node.nodeValue = (match ? match[1] : '') + (displayName ? displayName : '{{ user.user }}');
|
||||
const match = node.nodeValue.match(new RegExp('^(\\s*)(.*)'));
|
||||
node.nodeValue = (match ? match[1] : '') + text;
|
||||
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');
|
||||
if (bracket) {
|
||||
|
||||
Reference in New Issue
Block a user