fdsfads
This commit is contained in:
+442
-134
@@ -3101,7 +3101,6 @@ body.sidebar-right-hidden #sidebar-drag-zone::after {
|
|||||||
.sidebar-video-card.touch-active {
|
.sidebar-video-card.touch-active {
|
||||||
background: rgba(255, 255, 255, 0.06);
|
background: rgba(255, 255, 255, 0.06);
|
||||||
border-color: rgba(255, 255, 255, 0.12);
|
border-color: rgba(255, 255, 255, 0.12);
|
||||||
transform: translateY(-1px);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-video-card.sidebar-card-swapping {
|
.sidebar-video-card.sidebar-card-swapping {
|
||||||
@@ -3134,10 +3133,6 @@ body.sidebar-right-hidden #sidebar-drag-zone::after {
|
|||||||
-webkit-user-select: none;
|
-webkit-user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-video-card:hover .sidebar-video-thumb {
|
|
||||||
transform: scale(1.04);
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-video-badges {
|
.sidebar-video-badges {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: 6px;
|
bottom: 6px;
|
||||||
@@ -9983,12 +9978,6 @@ div.logwrap>p {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.admin-icon {
|
|
||||||
margin-right: 6px;
|
|
||||||
color: #ffeb3b;
|
|
||||||
font-size: 0.9em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.topf0ckers {
|
.topf0ckers {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 20px 0;
|
padding: 20px 0;
|
||||||
@@ -11836,10 +11825,12 @@ video.autoplay-gif {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: 5px;
|
gap: 5px;
|
||||||
border-radius: 3px;
|
border-radius: 0;
|
||||||
border: 1px solid var(--nav-border-color, rgba(255, 255, 255, 0.15));
|
border: 1px solid var(--nav-border-color, rgba(255, 255, 255, 0.15));
|
||||||
background: var(--navigation-links-bg, rgba(255, 255, 255, 0.08));
|
background: var(--navigation-links-bg, rgba(255, 255, 255, 0.08));
|
||||||
color: var(--white, #fff);
|
color: var(--white, #fff);
|
||||||
|
box-shadow: none;
|
||||||
|
text-shadow: none;
|
||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
@@ -11895,20 +11886,29 @@ video.autoplay-gif {
|
|||||||
opacity: 0.9;
|
opacity: 0.9;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Cancel buttons */
|
/* Cancel buttons: regular theme buttons (style guide), accent on hover like the others */
|
||||||
.input-actions .cancel-reply,
|
.input-actions .cancel-reply,
|
||||||
.input-actions .cancel-edit-btn {
|
.input-actions .cancel-edit-btn {
|
||||||
background: #555;
|
|
||||||
color: #fff;
|
|
||||||
border: 1px solid #555;
|
|
||||||
padding: 0 10px;
|
padding: 0 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.input-actions .cancel-reply:hover,
|
/* Comment / DM input buttons: style guide (square, flat, theme colours, no glow) */
|
||||||
.input-actions .cancel-edit-btn:hover {
|
.input-actions,
|
||||||
background: #666;
|
.input-actions * {
|
||||||
border-color: #666;
|
border-radius: 0 !important;
|
||||||
color: #fff;
|
}
|
||||||
|
|
||||||
|
.input-actions button:active {
|
||||||
|
opacity: 0.75;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-actions button:focus-visible {
|
||||||
|
outline: 2px solid var(--accent);
|
||||||
|
outline-offset: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.emoji-picker {
|
||||||
|
box-shadow: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Light / Paper Theme */
|
/* Light / Paper Theme */
|
||||||
@@ -15138,6 +15138,94 @@ html[theme='paper'] .badge.badge-untagged {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── #tags box (tags, rating badge, add-tag button): style guide (square, flat, theme colours, no glow) ──
|
||||||
|
Same rules as the .gapRight action row. Rating badges keep their rating colours. */
|
||||||
|
span#tags,
|
||||||
|
span#tags *,
|
||||||
|
span#tags *::before,
|
||||||
|
span#tags *::after {
|
||||||
|
border-radius: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
span#tags :is(.badge, .rating-tag, .rating-label, .tag-btn, a, i) {
|
||||||
|
box-shadow: none !important;
|
||||||
|
text-shadow: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The "new tag" highlight fades the background only: flat, no glow */
|
||||||
|
span#tags .new-tag-glow {
|
||||||
|
box-shadow: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* One height for everything in the row: tags, rating badge, add-tag button and the tag input. Each used
|
||||||
|
to get its height from its own padding/borders (25.4 / 26.4 / 21 / 20px); now they share --tag-h
|
||||||
|
(the rating badge's height: 1.4em line + its label border) and centre their content inside it */
|
||||||
|
span#tags {
|
||||||
|
--tag-h: calc(1.4em + 4px);
|
||||||
|
}
|
||||||
|
|
||||||
|
span#tags .tags-inner > .badge,
|
||||||
|
span#tags .tag-btn,
|
||||||
|
span#tags .tag-ac-wrapper {
|
||||||
|
box-sizing: border-box;
|
||||||
|
height: var(--tag-h);
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
padding-top: 0 !important;
|
||||||
|
padding-bottom: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
span#tags .tag-controls {
|
||||||
|
padding-top: 2.5px;
|
||||||
|
padding-bottom: 2.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
span#tags .tag-ac-wrapper form {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
span#tags .tag-ac-input {
|
||||||
|
box-sizing: border-box;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
span#tags .tag-btn {
|
||||||
|
min-width: 26px;
|
||||||
|
background: var(--navigation-links-bg, rgba(255, 255, 255, 0.08));
|
||||||
|
border: 1px solid var(--nav-border-color, rgba(255, 255, 255, 0.15));
|
||||||
|
color: var(--white, #fff) !important;
|
||||||
|
transition: background-color 0.15s ease, border-color 0.15s ease, color 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
span#tags .tag-btn:hover,
|
||||||
|
span#tags .tag-btn.is-active {
|
||||||
|
background: var(--nav-link-hover-bg, rgba(255, 255, 255, 0.15));
|
||||||
|
border-color: var(--accent);
|
||||||
|
color: var(--accent) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Rating-coloured tag buttons keep their rating colour (hover still shows the accent border) */
|
||||||
|
span#tags .tag-btn.is-sfw:not(:hover) {
|
||||||
|
color: #28a745 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
span#tags .tag-btn.is-nsfw:not(:hover) {
|
||||||
|
color: #dc3545 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
span#tags :is(.tag-btn, .rating-tag.can-cycle, .badge a):hover {
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
span#tags :is(.tag-btn, .rating-tag.can-cycle):active {
|
||||||
|
opacity: 0.75;
|
||||||
|
}
|
||||||
|
|
||||||
|
span#tags :is(.tag-btn, .rating-tag, .badge a):focus-visible {
|
||||||
|
outline: 2px solid var(--accent);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
.rules {
|
.rules {
|
||||||
@@ -17844,16 +17932,141 @@ i.iconset#a_oc {
|
|||||||
padding: 5px;
|
padding: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Profile stat chips: style guide (square, flat, theme colours, no glow). !important beats the
|
||||||
|
per-theme shadow rules (amoled/light/paper) that would bring the shadow back */
|
||||||
.profile_head_user_stats > * {
|
.profile_head_user_stats > * {
|
||||||
background: var(--bg);
|
/* Same 30px height as #edit-profile-btn (border included), text centred */
|
||||||
padding: 0px 5px 0px 5px;
|
box-sizing: border-box;
|
||||||
border-radius: 5px;
|
height: 30px;
|
||||||
box-shadow: 1px 1px 1px black;
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 4px;
|
||||||
|
line-height: 1;
|
||||||
|
white-space: nowrap;
|
||||||
|
background: var(--bg);
|
||||||
|
border: 1px solid var(--nav-border-color, rgba(255, 255, 255, 0.15));
|
||||||
|
padding: 0 8px;
|
||||||
|
border-radius: 0 !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
text-shadow: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.profile_head_user_stats a {
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Normal cursor on the chips; pointer only where something happens: the joined date (hover tooltip)
|
||||||
|
and the comment / tag / hall count links */
|
||||||
|
.profile_head_user_stats > * {
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile_head_user_stats > .stat-joined,
|
||||||
|
.profile_head_user_stats a {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Edit-profile toggle and edit-banner button on the profile banner: square, flat, theme colours, one
|
||||||
|
size (30x30); accent on hover and while editing. The banner button is positioned by its wrapper. */
|
||||||
|
#edit-profile-btn {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 5px;
|
||||||
|
left: 5px;
|
||||||
|
z-index: 20;
|
||||||
|
}
|
||||||
|
|
||||||
|
#edit-profile-btn,
|
||||||
|
#inline-banner-edit-btn {
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
padding: 0;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1;
|
||||||
|
background: var(--dropdown-bg, #232323);
|
||||||
|
border: 1px solid var(--nav-border-color, rgba(255, 255, 255, 0.15));
|
||||||
|
color: var(--white, #fff);
|
||||||
|
border-radius: 0 !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
text-shadow: none !important;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.15s ease, border-color 0.15s ease, color 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
#edit-profile-btn:hover,
|
||||||
|
#edit-profile-btn.is-active,
|
||||||
|
#inline-banner-edit-btn:hover {
|
||||||
|
background: var(--nav-link-hover-bg, #333);
|
||||||
|
border-color: var(--accent);
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
#edit-profile-btn:active,
|
||||||
|
#inline-banner-edit-btn:active {
|
||||||
|
opacity: 0.75;
|
||||||
|
}
|
||||||
|
|
||||||
|
#edit-profile-btn:focus-visible,
|
||||||
|
#inline-banner-edit-btn:focus-visible {
|
||||||
|
outline: 2px solid var(--accent);
|
||||||
|
outline-offset: 1px;
|
||||||
|
}
|
||||||
|
/* Profile content (uploads / favourites panels): style guide (square, flat, theme colours, no glow).
|
||||||
|
Each section is a panel with a title bar; the thumbnails inside keep their own grid styling. */
|
||||||
|
.user_content_wrapper {
|
||||||
|
gap: 8px;
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user_content_wrapper > .user-uploads,
|
||||||
|
.user_content_wrapper > .favs {
|
||||||
|
min-width: 0;
|
||||||
|
background: var(--dropdown-bg, #232323);
|
||||||
|
border: 1px solid var(--nav-border-color, rgba(255, 255, 255, 0.15));
|
||||||
|
border-radius: 0 !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
.uploads-header,
|
.uploads-header,
|
||||||
.favs-header {
|
.favs-header {
|
||||||
background: var(--black);
|
display: flex;
|
||||||
padding: 5px;
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 8px;
|
||||||
|
background: var(--bg, #111);
|
||||||
|
border-bottom: 1px solid var(--nav-border-color, rgba(255, 255, 255, 0.15));
|
||||||
|
padding: 6px 8px;
|
||||||
|
font-weight: bold;
|
||||||
|
border-radius: 0 !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
text-shadow: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.uploads-header a,
|
||||||
|
.favs-header a {
|
||||||
|
color: var(--accent);
|
||||||
|
font-weight: normal;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.uploads-header a:hover,
|
||||||
|
.favs-header a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user_content_wrapper div.user-uploads div.posts,
|
||||||
|
.user_content_wrapper div.favs div.posts {
|
||||||
|
background: transparent;
|
||||||
|
padding: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user_content_wrapper .private-favs-msg {
|
||||||
|
padding: 12px 8px;
|
||||||
|
color: var(--text-muted, rgba(255, 255, 255, 0.6));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -22047,8 +22260,11 @@ div#flash,
|
|||||||
padding: 6px 12px;
|
padding: 6px 12px;
|
||||||
box-shadow: none !important;
|
box-shadow: none !important;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
|
/* Hidden = really hidden: no Tab stops or screen-reader access to its buttons. visibility flips only
|
||||||
|
after the slide/fade out has finished (0.2s delay), and immediately when showing (see .visible) */
|
||||||
|
visibility: hidden;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
transition: transform 0.2s ease, opacity 0.2s ease;
|
transition: transform 0.2s ease, opacity 0.2s ease, visibility 0s linear 0.2s;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
font-family: var(--font, monospace) !important;
|
font-family: var(--font, monospace) !important;
|
||||||
max-width: 100vw;
|
max-width: 100vw;
|
||||||
@@ -22057,7 +22273,9 @@ div#flash,
|
|||||||
#bulk-action-bar.visible {
|
#bulk-action-bar.visible {
|
||||||
transform: translateX(-50%) translateY(0);
|
transform: translateX(-50%) translateY(0);
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
|
visibility: visible;
|
||||||
pointer-events: auto;
|
pointer-events: auto;
|
||||||
|
transition: transform 0.2s ease, opacity 0.2s ease, visibility 0s linear 0s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bulk-count-box {
|
.bulk-count-box {
|
||||||
@@ -22085,79 +22303,79 @@ div#flash,
|
|||||||
}
|
}
|
||||||
|
|
||||||
.bulk-actions-group {
|
.bulk-actions-group {
|
||||||
display: flex;
|
/* Equal columns: every action button is exactly as wide as the widest one */
|
||||||
align-items: center;
|
display: grid;
|
||||||
|
grid-auto-flow: column;
|
||||||
|
grid-auto-columns: 1fr;
|
||||||
|
align-items: stretch;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
flex-wrap: nowrap;
|
/* Sized to its content so 1fr resolves to the widest button, not to a share of the bar */
|
||||||
|
width: max-content;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bulk-btn {
|
/* All bar buttons: style guide (square, flat, theme colours, no glow), one size, one look.
|
||||||
|
Delete / Add Tags / close used to have their own filled or red variants; they are plain now. */
|
||||||
|
.bulk-btn,
|
||||||
|
.bulk-btn-primary,
|
||||||
|
.bulk-btn-danger,
|
||||||
|
.bulk-btn-close {
|
||||||
|
box-sizing: border-box;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 5px;
|
justify-content: center;
|
||||||
padding: 5px 10px;
|
gap: 6px;
|
||||||
|
width: 100%;
|
||||||
|
height: 30px;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0 10px;
|
||||||
border-radius: 0 !important;
|
border-radius: 0 !important;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
|
font-weight: normal !important;
|
||||||
font-family: var(--font, monospace) !important;
|
font-family: var(--font, monospace) !important;
|
||||||
border: 1px solid var(--nav-border-color, #444);
|
line-height: 1;
|
||||||
background: var(--nav-bg, #2b2b2b);
|
border: 1px solid var(--nav-border-color, #444) !important;
|
||||||
color: var(--white, #fff);
|
background: var(--navigation-links-bg, var(--nav-bg, #2b2b2b)) !important;
|
||||||
|
color: var(--white, #fff) !important;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
box-shadow: none !important;
|
box-shadow: none !important;
|
||||||
transition: background 0.12s ease, color 0.12s ease, border-color 0.12s ease;
|
text-shadow: none !important;
|
||||||
|
filter: none !important;
|
||||||
|
transform: none !important;
|
||||||
|
transition: background-color 0.12s ease, color 0.12s ease, border-color 0.12s ease;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bulk-btn:hover {
|
.bulk-btn:hover,
|
||||||
background: var(--nav-link-hover-bg, #333);
|
.bulk-btn:focus-visible,
|
||||||
color: var(--accent, #9f0);
|
.bulk-dropdown-wrap:has(.bulk-dropdown-menu.show) > .bulk-btn {
|
||||||
border-color: var(--accent, #9f0);
|
background: var(--nav-link-hover-bg, #333) !important;
|
||||||
transform: none !important;
|
color: var(--accent, #9f0) !important;
|
||||||
|
border-color: var(--accent, #9f0) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bulk-btn:focus-visible {
|
||||||
|
outline: 2px solid var(--accent, #9f0);
|
||||||
|
outline-offset: 1px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bulk-btn:active {
|
.bulk-btn:active {
|
||||||
transform: none !important;
|
opacity: 0.75;
|
||||||
}
|
|
||||||
|
|
||||||
.bulk-btn-primary {
|
|
||||||
background: var(--accent, #9f0) !important;
|
|
||||||
color: var(--black, #000) !important;
|
|
||||||
border: 1px solid var(--accent, #9f0) !important;
|
|
||||||
font-weight: bold !important;
|
|
||||||
box-shadow: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bulk-btn-primary:hover {
|
|
||||||
filter: brightness(1.15);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Close (x) sits outside the equal-width group: same height and look, square */
|
||||||
.bulk-btn-close {
|
.bulk-btn-close {
|
||||||
padding: 5px 8px;
|
width: 30px;
|
||||||
background: var(--nav-bg, #2b2b2b);
|
flex: 0 0 30px;
|
||||||
color: var(--white, #fff);
|
padding: 0;
|
||||||
border: 1px solid var(--nav-border-color, #444);
|
|
||||||
margin-right: 4px;
|
margin-right: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bulk-btn-close:hover {
|
.bulk-btn .bulk-chevron {
|
||||||
background: #ff4444 !important;
|
font-size: 10px;
|
||||||
color: #fff !important;
|
|
||||||
border-color: #ff4444 !important;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.bulk-btn-danger {
|
.bulk-dropdown-wrap {
|
||||||
background: transparent !important;
|
display: flex;
|
||||||
color: #ff5555 !important;
|
|
||||||
border: 1px solid #ff5555 !important;
|
|
||||||
font-weight: bold !important;
|
|
||||||
box-shadow: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bulk-btn-danger:hover {
|
|
||||||
background: #ff4444 !important;
|
|
||||||
color: #fff !important;
|
|
||||||
border-color: #ff4444 !important;
|
|
||||||
filter: none !important;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Bulk rating dropdown wrapper */
|
/* Bulk rating dropdown wrapper */
|
||||||
@@ -22463,13 +22681,14 @@ div#flash,
|
|||||||
gap: 4px;
|
gap: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Phones: icon-only buttons, still all the same size */
|
||||||
.bulk-btn {
|
.bulk-btn {
|
||||||
padding: 5px 7px;
|
padding: 0 8px;
|
||||||
font-size: 11.5px;
|
|
||||||
gap: 3px;
|
gap: 3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bulk-btn .btn-text-full {
|
.bulk-btn > span,
|
||||||
|
.bulk-btn .bulk-chevron {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -22561,53 +22780,6 @@ div#flash,
|
|||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.album-counter-pill {
|
|
||||||
position: absolute;
|
|
||||||
top: 14px;
|
|
||||||
left: 50%;
|
|
||||||
transform: translateX(-50%);
|
|
||||||
pointer-events: none;
|
|
||||||
opacity: 0;
|
|
||||||
background: rgba(16, 16, 20, 0.75);
|
|
||||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
|
||||||
backdrop-filter: blur(12px);
|
|
||||||
-webkit-backdrop-filter: blur(12px);
|
|
||||||
color: #fff;
|
|
||||||
padding: 5px 14px;
|
|
||||||
border-radius: 20px;
|
|
||||||
font-size: 0.85rem;
|
|
||||||
font-weight: 600;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 7px;
|
|
||||||
box-shadow: 0 4px 14px rgba(0, 0, 0, 0.4);
|
|
||||||
letter-spacing: 0.03em;
|
|
||||||
user-select: none;
|
|
||||||
transition: opacity 0.25s cubic-bezier(0.4, 0, 0.2, 1), transform 0.25s cubic-bezier(0.4, 0, 0.2, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (hover: hover) {
|
|
||||||
.album-gallery-container:hover .album-counter-pill,
|
|
||||||
.album-gallery-container:focus-within .album-counter-pill {
|
|
||||||
opacity: 1;
|
|
||||||
pointer-events: auto;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.album-gallery-container.v0ck_hover .album-counter-pill,
|
|
||||||
.album-gallery-container.strip-peek .album-counter-pill,
|
|
||||||
.album-gallery-container:has(.v0ck.v0ck_hover) .album-counter-pill,
|
|
||||||
.album-counter-pill.is-visible,
|
|
||||||
.album-counter-pill.strip-peek {
|
|
||||||
opacity: 1;
|
|
||||||
pointer-events: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.album-counter-pill i {
|
|
||||||
color: var(--accent);
|
|
||||||
font-size: 0.95rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.album-thumbnails-strip {
|
.album-thumbnails-strip {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: 12px;
|
bottom: 12px;
|
||||||
@@ -22748,6 +22920,13 @@ div#flash,
|
|||||||
|
|
||||||
.album-gallery-container.is-video-active .album-thumbnails-strip,
|
.album-gallery-container.is-video-active .album-thumbnails-strip,
|
||||||
.album-gallery-container.is-audio-active .album-thumbnails-strip {
|
.album-gallery-container.is-audio-active .album-thumbnails-strip {
|
||||||
|
/* Follows the player controls: lifted above them only while they are shown (.v0ck_hover), back
|
||||||
|
down with them when they auto-hide, on the same 0.3s timing as the controls' slide */
|
||||||
|
transition: opacity 0.25s cubic-bezier(0.4, 0, 0.2, 1), transform 0.25s cubic-bezier(0.4, 0, 0.2, 1), bottom 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.album-gallery-container.is-video-active:has(.v0ck.v0ck_hover) .album-thumbnails-strip,
|
||||||
|
.album-gallery-container.is-audio-active:has(.v0ck.v0ck_hover) .album-thumbnails-strip {
|
||||||
bottom: 58px;
|
bottom: 58px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -22762,8 +22941,8 @@ div#flash,
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.album-gallery-container.is-video-active .album-thumbnails-strip,
|
.album-gallery-container.is-video-active:has(.v0ck.v0ck_hover) .album-thumbnails-strip,
|
||||||
.album-gallery-container.is-audio-active .album-thumbnails-strip {
|
.album-gallery-container.is-audio-active:has(.v0ck.v0ck_hover) .album-thumbnails-strip {
|
||||||
bottom: 48px;
|
bottom: 48px;
|
||||||
}
|
}
|
||||||
.album-gallery-container.is-youtube-active .album-thumbnails-strip {
|
.album-gallery-container.is-youtube-active .album-thumbnails-strip {
|
||||||
@@ -22855,11 +23034,6 @@ div#flash,
|
|||||||
.album-controls-overlay {
|
.album-controls-overlay {
|
||||||
padding: 0 8px;
|
padding: 0 8px;
|
||||||
}
|
}
|
||||||
.album-counter-pill {
|
|
||||||
top: 10px;
|
|
||||||
padding: 3px 10px;
|
|
||||||
font-size: 0.78rem;
|
|
||||||
}
|
|
||||||
.album-thumb-item {
|
.album-thumb-item {
|
||||||
width: 36px;
|
width: 36px;
|
||||||
height: 36px;
|
height: 36px;
|
||||||
@@ -23203,3 +23377,137 @@ html.range-tap-lock input[type="range"].range-unlocked {
|
|||||||
a#nav-upload-link {
|
a#nav-upload-link {
|
||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
/* Background toggle (l key / player button): the fade out matches the 1.5s fade in instead of the quick
|
||||||
|
0.22s navigation fade-out. bg-toggle-fade is set by toggleBackground() while the background is off. */
|
||||||
|
#bg.fader-out.bg-toggle-fade,
|
||||||
|
body.onara-modal-open #bg.bg-toggle-fade:is(.fader-in, .fader-out) {
|
||||||
|
transition: opacity 1.5s cubic-bezier(0.4, 0, 0.2, 1), transform 0.6s cubic-bezier(0.45, 0, 0.2, 1) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Album thumbnail strip: style guide (square, flat, theme colours, no glow) ──
|
||||||
|
Same look as the bulk bar / tuner: theme panel background, nav border, accent for state. No blur,
|
||||||
|
no shadows, no hover/active scaling (the active thumb is marked by its accent border instead).
|
||||||
|
Positioning, show/hide and the mobile sizes are left to the rules above. */
|
||||||
|
.album-thumbnails-strip,
|
||||||
|
.album-thumb-item,
|
||||||
|
.album-thumb-mime-badge {
|
||||||
|
border-radius: 0 !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
backdrop-filter: none !important;
|
||||||
|
-webkit-backdrop-filter: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.album-thumbnails-strip {
|
||||||
|
background: var(--dropdown-bg, #232323);
|
||||||
|
border: 1px solid var(--nav-border-color, rgba(255, 255, 255, 0.15));
|
||||||
|
padding: 4px;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.album-thumb-item {
|
||||||
|
border: 2px solid var(--nav-border-color, rgba(255, 255, 255, 0.15));
|
||||||
|
opacity: 0.55;
|
||||||
|
transition: opacity 0.15s ease, border-color 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.album-thumb-item:hover,
|
||||||
|
.album-thumb-item:focus-visible {
|
||||||
|
opacity: 0.9;
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.album-thumb-item.active {
|
||||||
|
opacity: 1;
|
||||||
|
border-color: var(--accent, #9f0);
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.album-thumb-item:focus-visible {
|
||||||
|
outline: 2px solid var(--accent, #9f0);
|
||||||
|
outline-offset: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.album-thumb-mime-badge {
|
||||||
|
background: var(--bg, #111);
|
||||||
|
color: var(--white, #fff);
|
||||||
|
border: 1px solid var(--nav-border-color, rgba(255, 255, 255, 0.15));
|
||||||
|
bottom: 0;
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dragging the album strip: grabbing cursor everywhere until the mouse is released (the thumbnails'
|
||||||
|
own pointer cursor would otherwise win, and leaving the strip would drop back to the page cursor) */
|
||||||
|
html.album-strip-dragging,
|
||||||
|
html.album-strip-dragging * {
|
||||||
|
cursor: grabbing !important;
|
||||||
|
user-select: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Legacy comment slide (comments.js slideLegacyComments): while it runs, the container's height comes
|
||||||
|
from --comments-slide-h (animated) and it clips its content, so the comments disappear exactly at its
|
||||||
|
top edge, right under the .blahlol box. !important + the extra class outrank layouts that pin the
|
||||||
|
container (Onara: height: auto !important; overflow: visible !important). Registered so the height
|
||||||
|
animates smoothly instead of jumping. */
|
||||||
|
@property --comments-slide-h {
|
||||||
|
syntax: '<length>';
|
||||||
|
inherits: false;
|
||||||
|
initial-value: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#comments-container.is-comments-sliding,
|
||||||
|
#onara-modal #comments-container.is-comments-sliding {
|
||||||
|
height: var(--comments-slide-h) !important;
|
||||||
|
min-height: 0 !important;
|
||||||
|
max-height: none !important;
|
||||||
|
overflow: hidden !important;
|
||||||
|
flex: 0 0 auto !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Legacy .blahlol meta row (infobox off): ID / user / OC badge on the left, time (+ expiry) on the right */
|
||||||
|
.blahlol-meta-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 6px 12px;
|
||||||
|
padding: 2px 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* .blahlol hands down a 32px line-height (the time link has its own 28px): use the tags' 1.4 so the
|
||||||
|
row is as compact as the tag row below it */
|
||||||
|
.blahlol-meta-row,
|
||||||
|
.blahlol-meta-row * {
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blahlol-meta-row > .blahlol-id-badge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 4px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blahlol-meta-row > .blahlol-meta-right {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Meta row: the time (smaller font) gets exactly the same box height as the user badge instead of a
|
||||||
|
line box from its own font size; the text is centred inside it */
|
||||||
|
.blahlol-meta-row > .blahlol-id-badge,
|
||||||
|
.blahlol-meta-row > .blahlol-meta-right {
|
||||||
|
box-sizing: border-box;
|
||||||
|
height: 1.4em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blahlol-meta-right > .badge,
|
||||||
|
.blahlol-meta-right .timestamp-link,
|
||||||
|
.blahlol-meta-right .timeago {
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|||||||
+33
-12
@@ -4223,17 +4223,20 @@ class CommentSystem {
|
|||||||
if (startH === null) startH = el.getBoundingClientRect().height;
|
if (startH === null) startH = el.getBoundingClientRect().height;
|
||||||
}
|
}
|
||||||
|
|
||||||
el.style.height = 'auto';
|
|
||||||
const fullH = el.scrollHeight;
|
const fullH = el.scrollHeight;
|
||||||
const endH = hide ? 0 : fullH;
|
const endH = hide ? 0 : fullH;
|
||||||
el.style.overflow = 'hidden';
|
// Height + clipping go through a class and a registered custom property (see f0ckm.css): layouts
|
||||||
el.style.height = endH + 'px';
|
// like the Onara modal pin the container with `height: auto !important; overflow: visible
|
||||||
|
// !important`, which beats both an animated height and an inline overflow. The class rule is
|
||||||
|
// more specific and !important, and its height reads the variable the animation drives.
|
||||||
|
el.style.setProperty('--comments-slide-h', endH + 'px');
|
||||||
|
el.classList.add('is-comments-sliding');
|
||||||
|
|
||||||
const frames = (h0, h1) => [{ transform: 'translateY(' + (h0 - fullH) + 'px)' }, { transform: 'translateY(' + (h1 - fullH) + 'px)' }];
|
const frames = (h0, h1) => [{ transform: 'translateY(' + (h0 - fullH) + 'px)' }, { transform: 'translateY(' + (h1 - fullH) + 'px)' }];
|
||||||
// A turnaround covers only part of the distance, so it takes proportionally less time (same speed)
|
// A turnaround covers only part of the distance, so it takes proportionally less time (same speed)
|
||||||
const share = fullH > 0 ? Math.abs(endH - startH) / fullH : 1;
|
const share = fullH > 0 ? Math.abs(endH - startH) / fullH : 1;
|
||||||
const opts = { duration: Math.round(DURATION * Math.max(0.3, Math.min(1, share))), easing: EASING };
|
const opts = { duration: Math.round(DURATION * Math.max(0.3, Math.min(1, share))), easing: EASING };
|
||||||
const anims = [el.animate([{ height: startH + 'px' }, { height: endH + 'px' }], opts)];
|
const anims = [el.animate([{ '--comments-slide-h': startH + 'px' }, { '--comments-slide-h': endH + 'px' }], opts)];
|
||||||
kids.forEach(k => anims.push(k.animate(frames(startH, endH), opts)));
|
kids.forEach(k => anims.push(k.animate(frames(startH, endH), opts)));
|
||||||
|
|
||||||
const slide = { dir: hide ? 'hide' : 'show', anims, el };
|
const slide = { dir: hide ? 'hide' : 'show', anims, el };
|
||||||
@@ -4241,8 +4244,8 @@ class CommentSystem {
|
|||||||
anims[0].onfinish = () => {
|
anims[0].onfinish = () => {
|
||||||
if (this._legacySlide !== slide) return;
|
if (this._legacySlide !== slide) return;
|
||||||
this._legacySlide = null;
|
this._legacySlide = null;
|
||||||
el.style.removeProperty('overflow');
|
el.classList.remove('is-comments-sliding');
|
||||||
el.style.removeProperty('height');
|
el.style.removeProperty('--comments-slide-h');
|
||||||
if (!hide) {
|
if (!hide) {
|
||||||
document.body.classList.remove('comments-sliding');
|
document.body.classList.remove('comments-sliding');
|
||||||
return;
|
return;
|
||||||
@@ -5005,6 +5008,28 @@ class CommentSystem {
|
|||||||
}, { once: true });
|
}, { once: true });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Bring the whole picker into view with some room below it. Measured once the enter animation has
|
||||||
|
// finished (mid-animation it is shifted up and slightly squashed, so it would look smaller than it
|
||||||
|
// is) and scrolled on whatever actually scrolls here (Onara modal, index container or the window).
|
||||||
|
const revealPicker = () => {
|
||||||
|
if (!picker || picker.style.display === 'none') return;
|
||||||
|
let done = false;
|
||||||
|
const run = () => {
|
||||||
|
if (done || !picker || picker.style.display === 'none') return;
|
||||||
|
done = true;
|
||||||
|
const MARGIN = 24;
|
||||||
|
const sc = window._f0ckScrollerFor ? window._f0ckScrollerFor(picker) : null;
|
||||||
|
const view = sc ? sc.getBoundingClientRect() : { top: 0, bottom: window.innerHeight };
|
||||||
|
const r = picker.getBoundingClientRect();
|
||||||
|
let delta = r.bottom + MARGIN - view.bottom;
|
||||||
|
// Never push the picker's own top out of view (very short viewports)
|
||||||
|
delta = Math.min(delta, r.top - view.top - 8);
|
||||||
|
if (delta > 0) (sc || window).scrollBy({ top: delta, behavior: 'smooth' });
|
||||||
|
};
|
||||||
|
picker.addEventListener('animationend', run, { once: true });
|
||||||
|
setTimeout(run, 300);
|
||||||
|
};
|
||||||
|
|
||||||
trigger.addEventListener('click', (e) => {
|
trigger.addEventListener('click', (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
@@ -5028,9 +5053,7 @@ class CommentSystem {
|
|||||||
picker.offsetHeight; // reflow
|
picker.offsetHeight; // reflow
|
||||||
picker.style.animation = '';
|
picker.style.animation = '';
|
||||||
trigger.classList.add('is-active');
|
trigger.classList.add('is-active');
|
||||||
requestAnimationFrame(() => requestAnimationFrame(() =>
|
revealPicker();
|
||||||
picker.scrollIntoView({ behavior: 'smooth', block: 'nearest' })
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -5093,9 +5116,7 @@ class CommentSystem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
container.appendChild(picker);
|
container.appendChild(picker);
|
||||||
requestAnimationFrame(() => requestAnimationFrame(() =>
|
revealPicker();
|
||||||
picker.scrollIntoView({ behavior: 'smooth', block: 'nearest' })
|
|
||||||
));
|
|
||||||
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
+202
-53
@@ -809,6 +809,36 @@ window.cancelAnimFrame = (function () {
|
|||||||
|
|
||||||
let video;
|
let video;
|
||||||
let isNavigating = false;
|
let isNavigating = false;
|
||||||
|
// Next/prev presses that arrive while an item is still loading: +1 per next, -1 per prev. Each
|
||||||
|
// finished load immediately takes the next step (runNavQueue), so mashing next flies through items
|
||||||
|
// instead of dropping every press after the first. Capped so holding the key can't run forever.
|
||||||
|
let navQueue = 0;
|
||||||
|
const NAV_QUEUE_MAX = 25;
|
||||||
|
// Step inside the current item's album if there is a slide left in that direction. Returns true when
|
||||||
|
// it stepped (the post navigation must then not happen).
|
||||||
|
const albumNavStep = (direction) => {
|
||||||
|
const gal = window._currentActiveAlbumGallery;
|
||||||
|
if (!gal || typeof gal.getIndex !== 'function' || (gal.isAlive && !gal.isAlive())) return false;
|
||||||
|
const idx = gal.getIndex();
|
||||||
|
const count = gal.getCount();
|
||||||
|
if (!(count > 1)) return false;
|
||||||
|
if (direction === 'next' && idx < count - 1) { gal.next(); return true; }
|
||||||
|
if (direction === 'prev' && idx > 0) { gal.prev(); return true; }
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
const runNavQueue = () => {
|
||||||
|
if (!navQueue || isNavigating) return;
|
||||||
|
const dir = navQueue > 0 ? 'next' : 'prev';
|
||||||
|
const el = dir === 'next'
|
||||||
|
? document.querySelector(".steuerung .nav-next:not([href='#']), .nav-next:not([href='#']), #next:not([href='#'])")
|
||||||
|
: document.querySelector(".steuerung .nav-prev:not([href='#']), .nav-prev:not([href='#']), #prev:not([href='#'])");
|
||||||
|
if (!el || !el.href || el.href.endsWith('#')) {
|
||||||
|
navQueue = 0; // reached the end of the list
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
navQueue += dir === 'next' ? -1 : 1;
|
||||||
|
el.click();
|
||||||
|
};
|
||||||
const main = document.getElementById('main');
|
const main = document.getElementById('main');
|
||||||
let posts = document.querySelector('.posts');
|
let posts = document.querySelector('.posts');
|
||||||
const navbar = document.querySelector("nav.navbar");
|
const navbar = document.querySelector("nav.navbar");
|
||||||
@@ -3149,6 +3179,10 @@ window.cancelAnimFrame = (function () {
|
|||||||
background = !background;
|
background = !background;
|
||||||
window.background = background;
|
window.background = background;
|
||||||
localStorage.setItem('background', background ? 'true' : 'false');
|
localStorage.setItem('background', background ? 'true' : 'false');
|
||||||
|
// Toggling (l key / player button): fade out exactly as slowly as it fades in. Plain fader-out is
|
||||||
|
// the quick navigation fade, so the toggle marks the canvas while the background is off.
|
||||||
|
const bgEl = document.getElementById('bg');
|
||||||
|
if (bgEl) bgEl.classList.toggle('bg-toggle-fade', !background);
|
||||||
window.initBackground();
|
window.initBackground();
|
||||||
|
|
||||||
// Update videoplayer toggle buttons if they exist
|
// Update videoplayer toggle buttons if they exist
|
||||||
@@ -3597,8 +3631,6 @@ window.cancelAnimFrame = (function () {
|
|||||||
const linkEl = container.querySelector('#elfe, .album-stage-link');
|
const linkEl = container.querySelector('#elfe, .album-stage-link');
|
||||||
const prevBtn = container.querySelector('.album-btn-prev');
|
const prevBtn = container.querySelector('.album-btn-prev');
|
||||||
const nextBtn = container.querySelector('.album-btn-next');
|
const nextBtn = container.querySelector('.album-btn-next');
|
||||||
const currentIdxEl = container.querySelector('.album-current-idx');
|
|
||||||
const totalCountEl = container.querySelector('.album-total-count');
|
|
||||||
const thumbItems = container.querySelectorAll('.album-thumb-item');
|
const thumbItems = container.querySelectorAll('.album-thumb-item');
|
||||||
const stripEl = container.querySelector('.album-thumbnails-strip');
|
const stripEl = container.querySelector('.album-thumbnails-strip');
|
||||||
|
|
||||||
@@ -3613,7 +3645,6 @@ window.cancelAnimFrame = (function () {
|
|||||||
}, duration);
|
}, duration);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (totalCountEl) totalCountEl.textContent = albumData.length;
|
|
||||||
|
|
||||||
let albumVideoV0ck = null;
|
let albumVideoV0ck = null;
|
||||||
const initAlbumVideoV0ck = () => {
|
const initAlbumVideoV0ck = () => {
|
||||||
@@ -3953,10 +3984,6 @@ window.cancelAnimFrame = (function () {
|
|||||||
linkEl.href = newSrc;
|
linkEl.href = newSrc;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (currentIdxEl) {
|
|
||||||
currentIdxEl.textContent = currentIndex + 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
thumbItems.forEach((btn, idx) => {
|
thumbItems.forEach((btn, idx) => {
|
||||||
const isActive = idx === currentIndex;
|
const isActive = idx === currentIndex;
|
||||||
btn.classList.toggle('active', isActive);
|
btn.classList.toggle('active', isActive);
|
||||||
@@ -4152,8 +4179,19 @@ window.cancelAnimFrame = (function () {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Arrived here by stepping backwards (prev from the following post): start on the LAST slide, so
|
||||||
|
// going back through albums mirrors going forward. Only when no specific slide was requested.
|
||||||
|
let enterFromEnd = false;
|
||||||
|
if (window._albumEnterFromEnd) {
|
||||||
|
window._albumEnterFromEnd = false;
|
||||||
|
if (!initialHash) {
|
||||||
|
currentIndex = albumData.length - 1;
|
||||||
|
enterFromEnd = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Ensure proper initial media display (especially if initial item is video or subf0ck hash was requested)
|
// Ensure proper initial media display (especially if initial item is video or subf0ck hash was requested)
|
||||||
showImage(currentIndex, 'none', !!initialHash);
|
showImage(currentIndex, 'none', !!initialHash || enterFromEnd);
|
||||||
|
|
||||||
if (prevBtn) {
|
if (prevBtn) {
|
||||||
prevBtn.addEventListener('click', (e) => {
|
prevBtn.addEventListener('click', (e) => {
|
||||||
@@ -4182,6 +4220,10 @@ window.cancelAnimFrame = (function () {
|
|||||||
let lastX = 0;
|
let lastX = 0;
|
||||||
let lastTime = 0;
|
let lastTime = 0;
|
||||||
|
|
||||||
|
// Thumbnails are images inside buttons: without this the browser starts a native image drag on
|
||||||
|
// them (Firefox ignores -webkit-user-drag), which swallows the mousemoves the strip scrolls with
|
||||||
|
stripEl.addEventListener('dragstart', (e) => e.preventDefault());
|
||||||
|
|
||||||
stripEl.addEventListener('mousedown', (e) => {
|
stripEl.addEventListener('mousedown', (e) => {
|
||||||
if (e.button !== 0) return;
|
if (e.button !== 0) return;
|
||||||
isDown = true;
|
isDown = true;
|
||||||
@@ -4201,6 +4243,8 @@ window.cancelAnimFrame = (function () {
|
|||||||
if (Math.abs(walk) > 6) {
|
if (Math.abs(walk) > 6) {
|
||||||
hasDraggedStrip = true;
|
hasDraggedStrip = true;
|
||||||
stripEl.classList.add('is-dragging');
|
stripEl.classList.add('is-dragging');
|
||||||
|
// Grabbing cursor for the whole drag, also over thumbnails and outside the strip
|
||||||
|
document.documentElement.classList.add('album-strip-dragging');
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
stripEl.scrollLeft = startScrollLeft - walk;
|
stripEl.scrollLeft = startScrollLeft - walk;
|
||||||
|
|
||||||
@@ -4215,6 +4259,7 @@ window.cancelAnimFrame = (function () {
|
|||||||
if (!isDown) return;
|
if (!isDown) return;
|
||||||
isDown = false;
|
isDown = false;
|
||||||
stripEl.classList.remove('is-dragging');
|
stripEl.classList.remove('is-dragging');
|
||||||
|
document.documentElement.classList.remove('album-strip-dragging');
|
||||||
showStripPeek(6000);
|
showStripPeek(6000);
|
||||||
|
|
||||||
if (hasDraggedStrip && Math.abs(velX) > 0.15) {
|
if (hasDraggedStrip && Math.abs(velX) > 0.15) {
|
||||||
@@ -4393,6 +4438,10 @@ window.cancelAnimFrame = (function () {
|
|||||||
window._currentActiveAlbumGallery = {
|
window._currentActiveAlbumGallery = {
|
||||||
prev: () => showImage(currentIndex - 1, 'prev'),
|
prev: () => showImage(currentIndex - 1, 'prev'),
|
||||||
next: () => showImage(currentIndex + 1, 'next'),
|
next: () => showImage(currentIndex + 1, 'next'),
|
||||||
|
// Used by next/prev post navigation to step through the slides first (no wrap-around there)
|
||||||
|
getIndex: () => currentIndex,
|
||||||
|
getCount: () => albumData.length,
|
||||||
|
isAlive: () => container.isConnected,
|
||||||
showBySlug: (subKey) => {
|
showBySlug: (subKey) => {
|
||||||
if (!subKey || !Array.isArray(albumData)) return false;
|
if (!subKey || !Array.isArray(albumData)) return false;
|
||||||
const foundIdx = albumData.findIndex((item) =>
|
const foundIdx = albumData.findIndex((item) =>
|
||||||
@@ -4437,7 +4486,6 @@ window.cancelAnimFrame = (function () {
|
|||||||
th.setAttribute('data-index', newIdx);
|
th.setAttribute('data-index', newIdx);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (totalCountEl) totalCountEl.textContent = albumData.length;
|
|
||||||
|
|
||||||
if (currentIndex >= albumData.length) currentIndex = albumData.length - 1;
|
if (currentIndex >= albumData.length) currentIndex = albumData.length - 1;
|
||||||
showImage(currentIndex, 'none', true);
|
showImage(currentIndex, 'none', true);
|
||||||
@@ -5502,6 +5550,21 @@ window.cancelAnimFrame = (function () {
|
|||||||
const startTime = performance.now();
|
const startTime = performance.now();
|
||||||
const duration = 350; // ms
|
const duration = 350; // ms
|
||||||
|
|
||||||
|
// Render the new background (scaled + blurred) ONCE into an offscreen canvas at canvas size.
|
||||||
|
// Each crossfade frame then only blends two small bitmaps instead of re-scaling and re-blurring
|
||||||
|
// the source (which may be a full-resolution album image) ~20 times.
|
||||||
|
let prerendered = null;
|
||||||
|
try {
|
||||||
|
prerendered = document.createElement('canvas');
|
||||||
|
prerendered.width = targetW;
|
||||||
|
prerendered.height = targetH;
|
||||||
|
const pctx = prerendered.getContext('2d');
|
||||||
|
pctx.filter = getCanvasFilter();
|
||||||
|
drawCoverImage(pctx, sourceImg);
|
||||||
|
} catch (e) {
|
||||||
|
prerendered = null;
|
||||||
|
}
|
||||||
|
|
||||||
const step = (now) => {
|
const step = (now) => {
|
||||||
const elapsed = now - startTime;
|
const elapsed = now - startTime;
|
||||||
const progress = Math.min(1.0, elapsed / duration);
|
const progress = Math.min(1.0, elapsed / duration);
|
||||||
@@ -5520,9 +5583,13 @@ window.cancelAnimFrame = (function () {
|
|||||||
// 2. Fade in new background smoothly over previous
|
// 2. Fade in new background smoothly over previous
|
||||||
context.save();
|
context.save();
|
||||||
context.globalAlpha = ease;
|
context.globalAlpha = ease;
|
||||||
context.filter = getCanvasFilter();
|
|
||||||
try {
|
try {
|
||||||
drawCoverImage(context, sourceImg);
|
if (prerendered) {
|
||||||
|
context.drawImage(prerendered, 0, 0, w, h);
|
||||||
|
} else {
|
||||||
|
context.filter = getCanvasFilter();
|
||||||
|
drawCoverImage(context, sourceImg);
|
||||||
|
}
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
context.restore();
|
context.restore();
|
||||||
|
|
||||||
@@ -5607,6 +5674,9 @@ window.cancelAnimFrame = (function () {
|
|||||||
let videoFadeStartTime = prevSnapshot ? performance.now() : 0;
|
let videoFadeStartTime = prevSnapshot ? performance.now() : 0;
|
||||||
let _bgLastTs = 0;
|
let _bgLastTs = 0;
|
||||||
const animationLoop = (ts) => {
|
const animationLoop = (ts) => {
|
||||||
|
// Started directly (not from rAF) there is no timestamp: without one the fps throttle below compares
|
||||||
|
// against NaN and never throttles again for the rest of the loop
|
||||||
|
if (typeof ts !== 'number') ts = performance.now();
|
||||||
// Increment the per-frame ID so getEffectiveBackgroundTuning() cache invalidates correctly
|
// Increment the per-frame ID so getEffectiveBackgroundTuning() cache invalidates correctly
|
||||||
window._rafFrameId = ((window._rafFrameId || 0) + 1) & 0xFFFFFF;
|
window._rafFrameId = ((window._rafFrameId || 0) + 1) & 0xFFFFFF;
|
||||||
|
|
||||||
@@ -6169,21 +6239,25 @@ window.cancelAnimFrame = (function () {
|
|||||||
window.isCurrentlyInOnara = isCurrentlyInOnara;
|
window.isCurrentlyInOnara = isCurrentlyInOnara;
|
||||||
|
|
||||||
let _isAudioItemCache = { result: false, ts: 0 };
|
let _isAudioItemCache = { result: false, ts: 0 };
|
||||||
|
// The album template always contains a hidden <audio>; it only counts while an audio slide is shown
|
||||||
|
const isInactiveAlbumAudio = (el) => {
|
||||||
|
if (!el || el.id !== 'f0ck-album-audio') return false;
|
||||||
|
const albumContainer = el.closest('.album-gallery-container');
|
||||||
|
return !albumContainer || !albumContainer.classList.contains('is-audio-active');
|
||||||
|
};
|
||||||
|
|
||||||
const isCurrentItemAudio = () => {
|
const isCurrentItemAudio = () => {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
if (now - _isAudioItemCache.ts < 250) return _isAudioItemCache.result;
|
if (now - _isAudioItemCache.ts < 250) return _isAudioItemCache.result;
|
||||||
|
// Albums always contain a hidden <audio>: it only counts while an audio slide is shown, otherwise
|
||||||
|
// video/image albums would get the audio background profile (different fps cap and tuning)
|
||||||
|
const isActiveAudio = (el) => !!el && el.isConnected && !isInactiveAlbumAudio(el);
|
||||||
const mount = document.getElementById('onara-item-mount');
|
const mount = document.getElementById('onara-item-mount');
|
||||||
if (mount) {
|
if (mount && Array.from(mount.querySelectorAll('audio')).some(isActiveAudio)) {
|
||||||
const a = mount.querySelector('#f0ck-album-audio, audio#my-video, audio');
|
_isAudioItemCache = { result: true, ts: now };
|
||||||
if (a && a.isConnected) { _isAudioItemCache = { result: true, ts: now }; return true; }
|
return true;
|
||||||
}
|
|
||||||
const aud = document.querySelector('#f0ck-album-audio, audio#my-video, audio');
|
|
||||||
let result = false;
|
|
||||||
if (aud && aud.isConnected) {
|
|
||||||
const albumAudioWrap = document.getElementById('f0ck-album-audio-wrapper');
|
|
||||||
if (albumAudioWrap && albumAudioWrap.style.display !== 'none') result = true;
|
|
||||||
if (aud.tagName === 'AUDIO') result = true;
|
|
||||||
}
|
}
|
||||||
|
const result = Array.from(document.querySelectorAll('#f0ck-album-audio, audio#my-video, audio')).some(isActiveAudio);
|
||||||
_isAudioItemCache = { result, ts: now };
|
_isAudioItemCache = { result, ts: now };
|
||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
@@ -8860,13 +8934,6 @@ window.cancelAnimFrame = (function () {
|
|||||||
if (bgCanvas) bgCanvas.style.filter = '';
|
if (bgCanvas) bgCanvas.style.filter = '';
|
||||||
};
|
};
|
||||||
|
|
||||||
// The album template always contains a hidden <audio>; it only counts while an audio slide is shown
|
|
||||||
const isInactiveAlbumAudio = (el) => {
|
|
||||||
if (!el || el.id !== 'f0ck-album-audio') return false;
|
|
||||||
const albumContainer = el.closest('.album-gallery-container');
|
|
||||||
return !albumContainer || !albumContainer.classList.contains('is-audio-active');
|
|
||||||
};
|
|
||||||
|
|
||||||
window.initVisualizer = (targetAudio) => {
|
window.initVisualizer = (targetAudio) => {
|
||||||
let audioElement = targetAudio;
|
let audioElement = targetAudio;
|
||||||
if (!audioElement || !audioElement.isConnected) {
|
if (!audioElement || !audioElement.isConnected) {
|
||||||
@@ -12553,7 +12620,11 @@ window.cancelAnimFrame = (function () {
|
|||||||
|
|
||||||
const loadItemAjax = async (url, inheritContext = true, options = {}) => {
|
const loadItemAjax = async (url, inheritContext = true, options = {}) => {
|
||||||
|
|
||||||
|
// Anything other than a next/prev step (random, thumbnail, back...) ends a queued burst
|
||||||
|
if (!options.direction) navQueue = 0;
|
||||||
if (isNavigating) return;
|
if (isNavigating) return;
|
||||||
|
// Stepping back (prev) into an album opens it on its last slide (read by initAlbumGallery)
|
||||||
|
window._albumEnterFromEnd = options.direction === 'prev';
|
||||||
cancelPrefetch();
|
cancelPrefetch();
|
||||||
|
|
||||||
// Immediately restore scrollability and hide modals
|
// Immediately restore scrollability and hide modals
|
||||||
@@ -13384,9 +13455,12 @@ window.cancelAnimFrame = (function () {
|
|||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("AJAX load failed:", err);
|
console.error("AJAX load failed:", err);
|
||||||
|
navQueue = 0;
|
||||||
} finally {
|
} finally {
|
||||||
isNavigating = false;
|
isNavigating = false;
|
||||||
if (navbar) navbar.classList.remove("pbwork");
|
if (navbar) navbar.classList.remove("pbwork");
|
||||||
|
// Queued next/prev presses: take the next step right away, from the freshly loaded item's links
|
||||||
|
if (navQueue) setTimeout(runNavQueue, 0);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -13735,6 +13809,20 @@ window.cancelAnimFrame = (function () {
|
|||||||
const isGridPage = !!document.querySelector('.index-layout-wrapper, .index-container, .posts');
|
const isGridPage = !!document.querySelector('.index-layout-wrapper, .index-container, .posts');
|
||||||
const useOnara = isOnaraActive();
|
const useOnara = isOnaraActive();
|
||||||
|
|
||||||
|
// Still loading the previous step: remember the press, it runs as soon as that item is in
|
||||||
|
if (direction && isNavigating) {
|
||||||
|
navQueue = Math.max(-NAV_QUEUE_MAX, Math.min(NAV_QUEUE_MAX, navQueue + (direction === 'next' ? 1 : -1)));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Album on this item: next/prev go through its slides one by one; only past the last (or before
|
||||||
|
// the first) slide do they move on to the next (previous) post
|
||||||
|
if (direction && albumNavStep(direction)) {
|
||||||
|
// A queued burst keeps going through the remaining slides / posts
|
||||||
|
if (navQueue) setTimeout(runNavQueue, 120);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (direction) {
|
if (direction) {
|
||||||
const outEls = document.querySelectorAll('.content, ._204863, .item_title');
|
const outEls = document.querySelectorAll('.content, ._204863, .item_title');
|
||||||
outEls.forEach(el => {
|
outEls.forEach(el => {
|
||||||
@@ -14849,15 +14937,7 @@ window.cancelAnimFrame = (function () {
|
|||||||
// <keybindings>
|
// <keybindings>
|
||||||
const clickOnElementBinding = selector => () => (elem = document.querySelector(selector)) ? elem.click() : null;
|
const clickOnElementBinding = selector => () => (elem = document.querySelector(selector)) ? elem.click() : null;
|
||||||
const clickOnNavBinding = (directionOrSelector) => () => {
|
const clickOnNavBinding = (directionOrSelector) => () => {
|
||||||
if (window._currentActiveAlbumGallery && window._currentActiveAlbumGallery.isHovered) {
|
// Albums: the next/prev link click handler steps through the slides first (see albumNavStep)
|
||||||
if (directionOrSelector === 'prev' || directionOrSelector === '#prev') {
|
|
||||||
window._currentActiveAlbumGallery.prev();
|
|
||||||
return;
|
|
||||||
} else if (directionOrSelector === 'next' || directionOrSelector === '#next') {
|
|
||||||
window._currentActiveAlbumGallery.next();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let el;
|
let el;
|
||||||
if (directionOrSelector === 'prev' || directionOrSelector === '#prev') {
|
if (directionOrSelector === 'prev' || directionOrSelector === '#prev') {
|
||||||
el = document.querySelector(".steuerung .nav-prev:not([href='#']), .nav-prev:not([href='#']), #prev:not([href='#'])") || document.querySelector('.nav-prev') || document.getElementById('prev');
|
el = document.querySelector(".steuerung .nav-prev:not([href='#']), .nav-prev:not([href='#']), #prev:not([href='#'])") || document.querySelector('.nav-prev') || document.getElementById('prev');
|
||||||
@@ -24078,6 +24158,9 @@ document.addEventListener('keydown', (e) => {
|
|||||||
percent: Math.round(percent),
|
percent: Math.round(percent),
|
||||||
type
|
type
|
||||||
};
|
};
|
||||||
|
// sendBeacon can't set headers: logged-in users need the CSRF token in the body or get a 403
|
||||||
|
const csrf = window.f0ckSession?.csrf_token;
|
||||||
|
if (csrf) payload.csrf_token = csrf;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (navigator.sendBeacon) {
|
if (navigator.sendBeacon) {
|
||||||
@@ -24086,7 +24169,7 @@ document.addEventListener('keydown', (e) => {
|
|||||||
} else {
|
} else {
|
||||||
fetch('/api/v2/track/interaction', {
|
fetch('/api/v2/track/interaction', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json', ...(csrf ? { 'X-CSRF-Token': csrf } : {}) },
|
||||||
body: JSON.stringify(payload),
|
body: JSON.stringify(payload),
|
||||||
keepalive: true
|
keepalive: true
|
||||||
}).catch(() => {});
|
}).catch(() => {});
|
||||||
@@ -24112,6 +24195,9 @@ document.addEventListener('keydown', (e) => {
|
|||||||
|
|
||||||
// Dispatch tracking to backend
|
// Dispatch tracking to backend
|
||||||
const payload = { tag: tags.join(',') };
|
const payload = { tag: tags.join(',') };
|
||||||
|
// sendBeacon can't set headers: logged-in users need the CSRF token in the body or get a 403
|
||||||
|
const csrf = window.f0ckSession?.csrf_token;
|
||||||
|
if (csrf) payload.csrf_token = csrf;
|
||||||
try {
|
try {
|
||||||
if (navigator.sendBeacon) {
|
if (navigator.sendBeacon) {
|
||||||
const blob = new Blob([JSON.stringify(payload)], { type: 'application/json' });
|
const blob = new Blob([JSON.stringify(payload)], { type: 'application/json' });
|
||||||
@@ -24119,7 +24205,7 @@ document.addEventListener('keydown', (e) => {
|
|||||||
} else {
|
} else {
|
||||||
fetch('/api/v2/track/search', {
|
fetch('/api/v2/track/search', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json', ...(csrf ? { 'X-CSRF-Token': csrf } : {}) },
|
||||||
body: JSON.stringify(payload),
|
body: JSON.stringify(payload),
|
||||||
keepalive: true
|
keepalive: true
|
||||||
}).catch(() => {});
|
}).catch(() => {});
|
||||||
@@ -24265,24 +24351,18 @@ window.BulkSelection = (() => {
|
|||||||
<i class="fa-solid fa-xmark"></i>
|
<i class="fa-solid fa-xmark"></i>
|
||||||
</button>
|
</button>
|
||||||
<div class="bulk-count-box">
|
<div class="bulk-count-box">
|
||||||
<i class="fa-solid fa-check-double" style="color: var(--accent);"></i>
|
|
||||||
<span class="bulk-count-num">0</span>
|
<span class="bulk-count-num">0</span>
|
||||||
<span class="bulk-count-label">selected</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="bulk-actions-group">
|
<div class="bulk-actions-group">
|
||||||
<button type="button" class="bulk-btn bulk-btn-primary" id="bulk-btn-add-tags" title="Add tags in bulk">
|
<button type="button" class="bulk-btn bulk-btn-primary" id="bulk-btn-add-tags" title="Add tags in bulk">
|
||||||
<i class="fa-solid fa-tag"></i>
|
<i class="fa-solid fa-tag"></i>
|
||||||
<span>Add Tag<span class="btn-text-full">s</span></span>
|
<span>Add Tag<span class="btn-text-full">s</span></span>
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="bulk-btn" id="bulk-btn-remove-tags" title="Remove tags in bulk">
|
|
||||||
<i class="fa-solid fa-tags"></i>
|
|
||||||
<span>Remove Tag<span class="btn-text-full">s</span></span>
|
|
||||||
</button>
|
|
||||||
<div class="bulk-dropdown-wrap">
|
<div class="bulk-dropdown-wrap">
|
||||||
<button type="button" class="bulk-btn" id="bulk-btn-rate" title="Change rating in bulk">
|
<button type="button" class="bulk-btn" id="bulk-btn-rate" title="Change rating in bulk">
|
||||||
<i class="fa-solid fa-shield-halved"></i>
|
<i class="fa-solid fa-shield-halved"></i>
|
||||||
<span>Rating</span>
|
<span>Rating</span>
|
||||||
<i class="fa-solid fa-chevron-up" style="font-size: 10px; margin-left: 2px;"></i>
|
<i class="fa-solid fa-chevron-up bulk-chevron"></i>
|
||||||
</button>
|
</button>
|
||||||
<div class="bulk-dropdown-menu" id="bulk-rating-menu">
|
<div class="bulk-dropdown-menu" id="bulk-rating-menu">
|
||||||
<button type="button" class="bulk-menu-item rating-sfw" data-rating="sfw">
|
<button type="button" class="bulk-menu-item rating-sfw" data-rating="sfw">
|
||||||
@@ -24296,6 +24376,27 @@ window.BulkSelection = (() => {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="bulk-dropdown-wrap">
|
||||||
|
<button type="button" class="bulk-btn" id="bulk-btn-visibility" title="Change visibility in bulk">
|
||||||
|
<i class="fa-solid fa-eye"></i>
|
||||||
|
<span>Visibility</span>
|
||||||
|
<i class="fa-solid fa-chevron-up bulk-chevron"></i>
|
||||||
|
</button>
|
||||||
|
<div class="bulk-dropdown-menu" id="bulk-visibility-menu">
|
||||||
|
<button type="button" class="bulk-menu-item" data-visibility="0">
|
||||||
|
<i class="fa-solid fa-globe"></i> Public
|
||||||
|
</button>
|
||||||
|
<button type="button" class="bulk-menu-item" data-visibility="1">
|
||||||
|
<i class="fa-solid fa-link"></i> Unlisted
|
||||||
|
</button>
|
||||||
|
<button type="button" class="bulk-menu-item" data-visibility="2">
|
||||||
|
<i class="fa-solid fa-lock"></i> Private
|
||||||
|
</button>
|
||||||
|
<button type="button" class="bulk-menu-item" data-visibility="3">
|
||||||
|
<i class="fa-solid fa-ban"></i> Unavailable
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<button type="button" class="bulk-btn bulk-btn-danger" id="bulk-btn-delete" title="Delete selected items">
|
<button type="button" class="bulk-btn bulk-btn-danger" id="bulk-btn-delete" title="Delete selected items">
|
||||||
<i class="fa-solid fa-trash-can"></i>
|
<i class="fa-solid fa-trash-can"></i>
|
||||||
<span class="btn-text-full">Delete</span>
|
<span class="btn-text-full">Delete</span>
|
||||||
@@ -24337,14 +24438,15 @@ window.BulkSelection = (() => {
|
|||||||
|
|
||||||
// Wire up Action Bar events
|
// Wire up Action Bar events
|
||||||
document.getElementById('bulk-btn-add-tags')?.addEventListener('click', () => openTagModal('add'));
|
document.getElementById('bulk-btn-add-tags')?.addEventListener('click', () => openTagModal('add'));
|
||||||
document.getElementById('bulk-btn-remove-tags')?.addEventListener('click', () => openTagModal('remove'));
|
|
||||||
|
|
||||||
// Rating menu toggle
|
// Rating menu toggle
|
||||||
const rateBtn = document.getElementById('bulk-btn-rate');
|
const rateBtn = document.getElementById('bulk-btn-rate');
|
||||||
const rateMenu = document.getElementById('bulk-rating-menu');
|
const rateMenu = document.getElementById('bulk-rating-menu');
|
||||||
rateBtn?.addEventListener('click', (e) => {
|
rateBtn?.addEventListener('click', (e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
rateMenu?.classList.toggle('show');
|
const open = !rateMenu?.classList.contains('show');
|
||||||
|
closeBulkMenus();
|
||||||
|
if (open) rateMenu?.classList.add('show');
|
||||||
});
|
});
|
||||||
|
|
||||||
// Rating items click
|
// Rating items click
|
||||||
@@ -24357,6 +24459,24 @@ window.BulkSelection = (() => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Visibility menu toggle + items
|
||||||
|
const visBtn = document.getElementById('bulk-btn-visibility');
|
||||||
|
const visMenu = document.getElementById('bulk-visibility-menu');
|
||||||
|
visBtn?.addEventListener('click', (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
const open = !visMenu?.classList.contains('show');
|
||||||
|
closeBulkMenus();
|
||||||
|
if (open) visMenu?.classList.add('show');
|
||||||
|
});
|
||||||
|
visMenu?.querySelectorAll('.bulk-menu-item').forEach(btn => {
|
||||||
|
btn.addEventListener('click', async (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
visMenu.classList.remove('show');
|
||||||
|
const vis = parseInt(btn.dataset.visibility, 10);
|
||||||
|
if (!isNaN(vis)) await executeBulkVisibility(vis, btn.textContent.trim());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
document.getElementById('bulk-btn-select-all')?.addEventListener('click', () => selectAllLoaded());
|
document.getElementById('bulk-btn-select-all')?.addEventListener('click', () => selectAllLoaded());
|
||||||
document.getElementById('bulk-btn-close')?.addEventListener('click', () => exitSelectionMode());
|
document.getElementById('bulk-btn-close')?.addEventListener('click', () => exitSelectionMode());
|
||||||
document.getElementById('bulk-btn-delete')?.addEventListener('click', () => executeBulkDelete());
|
document.getElementById('bulk-btn-delete')?.addEventListener('click', () => executeBulkDelete());
|
||||||
@@ -24394,16 +24514,14 @@ window.BulkSelection = (() => {
|
|||||||
if (bar) {
|
if (bar) {
|
||||||
bar.classList.remove('visible');
|
bar.classList.remove('visible');
|
||||||
}
|
}
|
||||||
document.getElementById('bulk-rating-menu')?.classList.remove('show');
|
closeBulkMenus();
|
||||||
closeTagModal();
|
closeTagModal();
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateToolbar() {
|
function updateToolbar() {
|
||||||
const count = selectedIds.size;
|
const count = selectedIds.size;
|
||||||
const countNum = document.querySelector('#bulk-action-bar .bulk-count-num');
|
const countNum = document.querySelector('#bulk-action-bar .bulk-count-num');
|
||||||
const countLabel = document.querySelector('#bulk-action-bar .bulk-count-label');
|
|
||||||
if (countNum) countNum.textContent = count;
|
if (countNum) countNum.textContent = count;
|
||||||
if (countLabel) countLabel.textContent = count === 1 ? 'item' : 'items';
|
|
||||||
|
|
||||||
if (count > 0 && !isSelectionMode) {
|
if (count > 0 && !isSelectionMode) {
|
||||||
enterSelectionMode();
|
enterSelectionMode();
|
||||||
@@ -24493,7 +24611,7 @@ window.BulkSelection = (() => {
|
|||||||
|
|
||||||
renderTagChips();
|
renderTagChips();
|
||||||
if (input) input.value = '';
|
if (input) input.value = '';
|
||||||
document.getElementById('bulk-rating-menu')?.classList.remove('show');
|
closeBulkMenus();
|
||||||
|
|
||||||
modal?.classList.add('visible');
|
modal?.classList.add('visible');
|
||||||
setTimeout(() => input?.focus(), 80);
|
setTimeout(() => input?.focus(), 80);
|
||||||
@@ -24696,6 +24814,37 @@ window.BulkSelection = (() => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function closeBulkMenus() {
|
||||||
|
document.querySelectorAll('#bulk-action-bar .bulk-dropdown-menu.show').forEach(m => m.classList.remove('show'));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function executeBulkVisibility(visibility, label) {
|
||||||
|
const itemIds = Array.from(selectedIds);
|
||||||
|
if (itemIds.length === 0) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/v2/items/bulk-visibility', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-CSRF-Token': getCsrfToken()
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ item_ids: itemIds, visibility })
|
||||||
|
});
|
||||||
|
const data = await res.json();
|
||||||
|
if (data.success) {
|
||||||
|
const n = data.updated || 0;
|
||||||
|
flash(n ? `Visibility set to ${label} for ${n} item${n === 1 ? '' : 's'}` : (data.msg || 'Nothing to change'));
|
||||||
|
exitSelectionMode();
|
||||||
|
} else {
|
||||||
|
flash(data.msg || 'Failed to update visibility', 'error');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[BULK_VISIBILITY_ERROR]', err);
|
||||||
|
flash('Failed to communicate with server', 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function executeBulkRating(rating) {
|
async function executeBulkRating(rating) {
|
||||||
const itemIds = Array.from(selectedIds);
|
const itemIds = Array.from(selectedIds);
|
||||||
if (itemIds.length === 0) return;
|
if (itemIds.length === 0) return;
|
||||||
@@ -24918,7 +25067,7 @@ window.BulkSelection = (() => {
|
|||||||
|
|
||||||
// Close dropdown if clicking outside
|
// Close dropdown if clicking outside
|
||||||
if (!e.target.closest('.bulk-dropdown-wrap')) {
|
if (!e.target.closest('.bulk-dropdown-wrap')) {
|
||||||
document.getElementById('bulk-rating-menu')?.classList.remove('show');
|
closeBulkMenus();
|
||||||
}
|
}
|
||||||
|
|
||||||
const thumb = e.target.closest('.posts > a.thumb, .posts > a.lazy-thumb');
|
const thumb = e.target.closest('.posts > a.thumb, .posts > a.lazy-thumb');
|
||||||
|
|||||||
@@ -2200,6 +2200,68 @@ export default router => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Bulk visibility for the mod/admin selection bar: same levels and side effects as /item/visibility
|
||||||
|
group.post(/\/items\/bulk-visibility$/, lib.loggedin, async (req, res) => {
|
||||||
|
const isModOrAdmin = !!(req.session?.admin || req.session?.is_moderator);
|
||||||
|
if (!isModOrAdmin) {
|
||||||
|
return res.json({ success: false, msg: 'Bulk selection is only available to administrators and moderators' }, 403);
|
||||||
|
}
|
||||||
|
if (cfg.enable_private_uploads === false && !req.session.admin) {
|
||||||
|
return res.json({ success: false, msg: 'Private uploads feature disabled' }, 403);
|
||||||
|
}
|
||||||
|
|
||||||
|
const visibility = parseInt(req.body?.visibility ?? req.post?.visibility, 10);
|
||||||
|
if (isNaN(visibility) || ![0, 1, 2, 3].includes(visibility)) {
|
||||||
|
return res.json({ success: false, msg: 'Invalid visibility' }, 400);
|
||||||
|
}
|
||||||
|
const rawIds = req.body?.item_ids || req.post?.item_ids;
|
||||||
|
if (!Array.isArray(rawIds) || rawIds.length === 0) {
|
||||||
|
return res.json({ success: false, msg: 'No items selected' }, 400);
|
||||||
|
}
|
||||||
|
const itemIds = [...new Set(rawIds.map(id => +id).filter(id => Number.isInteger(id) && id > 0))].slice(0, 500);
|
||||||
|
if (itemIds.length === 0) {
|
||||||
|
return res.json({ success: false, msg: 'Invalid item IDs' }, 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const items = await db`
|
||||||
|
SELECT id, dest, username, COALESCE(visibility, 0) AS visibility
|
||||||
|
FROM items
|
||||||
|
WHERE id = ANY(${itemIds}::int[]) AND active = true AND is_deleted = false
|
||||||
|
`;
|
||||||
|
const changed = items.filter(it => it.visibility !== visibility);
|
||||||
|
if (changed.length === 0) {
|
||||||
|
return res.json({ success: true, updated: 0, visibility, msg: 'Nothing to change' });
|
||||||
|
}
|
||||||
|
|
||||||
|
await db`UPDATE items SET visibility = ${visibility} WHERE id = ANY(${changed.map(it => it.id)}::int[])`;
|
||||||
|
|
||||||
|
for (const it of changed) {
|
||||||
|
if (visibility === 2) {
|
||||||
|
addPrivateItem(it.id, it.dest, it.username);
|
||||||
|
removeUnavailableItem(it.id, it.dest);
|
||||||
|
} else if (visibility === 3) {
|
||||||
|
addUnavailableItem(it.id, it.dest, it.username);
|
||||||
|
removePrivateItem(it.id, it.dest);
|
||||||
|
} else {
|
||||||
|
removePrivateItem(it.id, it.dest);
|
||||||
|
removeUnavailableItem(it.id, it.dest);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
f0cklib.clearCountCache();
|
||||||
|
await audit.log(req.session.id, 'bulk_set_visibility', 'items', null, {
|
||||||
|
item_ids: changed.map(it => it.id),
|
||||||
|
visibility
|
||||||
|
}).catch(() => {});
|
||||||
|
|
||||||
|
return res.json({ success: true, updated: changed.length, item_ids: changed.map(it => it.id), visibility });
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[BULK VISIBILITY]', err);
|
||||||
|
return res.json({ success: false, msg: 'Failed to update visibility' }, 500);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
group.post(/\/items\/(?<id>[0-9]+)\/rethumb$/, lib.registeredUser, async (req, res) => {
|
group.post(/\/items\/(?<id>[0-9]+)\/rethumb$/, lib.registeredUser, async (req, res) => {
|
||||||
const itemid = +req.params.id;
|
const itemid = +req.params.id;
|
||||||
if (!itemid) return res.json({ success: false, msg: 'No itemid provided' }, 400);
|
if (!itemid) return res.json({ success: false, msg: 'No itemid provided' }, 400);
|
||||||
|
|||||||
@@ -368,10 +368,10 @@
|
|||||||
<!-- Role Filter -->
|
<!-- Role Filter -->
|
||||||
<select id="role-filter" class="admin-select-filter {{ role ? 'active' : '' }}" {{ onlyLegacy ? 'disabled' : '' }} title="Filter by user role">
|
<select id="role-filter" class="admin-select-filter {{ role ? 'active' : '' }}" {{ onlyLegacy ? 'disabled' : '' }} title="Filter by user role">
|
||||||
<option value="">All Roles</option>
|
<option value="">All Roles</option>
|
||||||
<option value="staff" {{ role === 'staff' ? 'selected' : '' }}>🛡️ Staff (All)</option>
|
<option value="staff" {{ role === 'staff' ? 'selected' : '' }}>Staff (All)</option>
|
||||||
<option value="admin" {{ role === 'admin' ? 'selected' : '' }}>⚡ Admins</option>
|
<option value="admin" {{ role === 'admin' ? 'selected' : '' }}>Admins</option>
|
||||||
<option value="mod" {{ role === 'mod' ? 'selected' : '' }}>🗡️ Moderators</option>
|
<option value="mod" {{ role === 'mod' ? 'selected' : '' }}>Moderators</option>
|
||||||
<option value="user" {{ role === 'user' ? 'selected' : '' }}>👤 Regular Users</option>
|
<option value="user" {{ role === 'user' ? 'selected' : '' }}>Regular Users</option>
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<!-- Legacy Only Toggle -->
|
<!-- Legacy Only Toggle -->
|
||||||
|
|||||||
@@ -178,16 +178,19 @@
|
|||||||
</div>
|
</div>
|
||||||
@endif
|
@endif
|
||||||
|
|
||||||
<span class="badge badge-dark">
|
{{-- Meta row: user / OC on the left, time (and expiry) on the right --}}
|
||||||
|
<div class="blahlol-meta-row" @if(user_alternative_infobox)style="display:none"@endif>
|
||||||
<a href="/{{ (enable_item_slugs && item.slug) ? item.slug : item.id }}" class="id-link" data-item-id="{{ item.id }}" @if(user_alternative_infobox)style="display:none"@endif>{{ (enable_item_slugs && item.slug) ? item.slug : item.id }}</a>
|
<div class="badge badge-dark blahlol-id-badge">
|
||||||
@if(!user_alternative_infobox) — [@if(!is_anonymized)<a id="a_username" data-username="{{ item.username || '' }}" @if(session && !session.is_anon) data-author-id="{{ item.author_id || '' }}" @endif href="/user/{{ (item.username || '').toLowerCase() }}" @if(session && !session.is_anon && item.author_id) tooltip="ID: {{ item.author_id }}" @endif @if(item.author_color) style="color: {{ item.author_color }}" @endif>{!! item.author_display_name || item.username || 'unknown' !!}</a>@else<span>anonymous</span>@endif] @endif
|
@if(!user_alternative_infobox)@if(!is_anonymized)<a id="a_username" data-username="{{ item.username || '' }}" @if(session && !session.is_anon) data-author-id="{{ item.author_id || '' }}" @endif href="/user/{{ (item.username || '').toLowerCase() }}" @if(session && !session.is_anon && item.author_id) tooltip="ID: {{ item.author_id }}" @endif @if(item.author_color) style="color: {{ item.author_color }}" @endif>{!! item.author_display_name || item.username || 'unknown' !!}</a>@else<span>anonymous</span>@endif @endif
|
||||||
|
@if(!user_alternative_infobox)
|
||||||
|
<span id="oc-badge-container">@if(item.is_oc) — <span class="oc-badge" tooltip="Original Content">OC</span>@endif</span>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
@if(!user_alternative_infobox)
|
@if(!user_alternative_infobox)
|
||||||
<span id="oc-badge-container">@if(item.is_oc) — <span class="oc-badge" tooltip="Original Content">OC</span>@endif</span>
|
<div class="blahlol-meta-right"><span class="badge badge-dark"><a href="/{{ (enable_item_slugs && item.slug) ? item.slug : item.id }}" class="timestamp-link"><time class="timeago" tooltip="{{ item.timestamp.timefull }}">{{item.timestamp.timeago }}</time></a></span>@if(item.expires_at) <span class="badge badge-warning" style="background: rgba(255, 170, 0, 0.2); color: #ffaa00; border: 1px solid rgba(255, 170, 0, 0.4);" tooltip="Self-destructs {{ item.expires_in }}" flow="up"><i class="fa-solid fa-clock"></i> Expiring ({{ item.expires_in }})</span>@endif</div>
|
||||||
@endif
|
@endif
|
||||||
</span>
|
</div>
|
||||||
@if(!user_alternative_infobox) — <span class="badge badge-dark"><a href="/{{ (enable_item_slugs && item.slug) ? item.slug : item.id }}" class="timestamp-link"><time class="timeago" tooltip="{{ item.timestamp.timefull }}">{{item.timestamp.timeago }}</time></a></span>@if(item.expires_at) — <span class="badge badge-warning" style="background: rgba(255, 170, 0, 0.2); color: #ffaa00; border: 1px solid rgba(255, 170, 0, 0.4);" tooltip="Self-destructs {{ item.expires_in }}" flow="up"><i class="fa-solid fa-clock"></i> Expiring ({{ item.expires_in }})</span>@endif @endif
|
@if(!user_alternative_infobox && item.is_chan) <span class="badge badge-dark chan-post-badge" style="background: rgba(74, 222, 128, 0.15); border: 1px solid rgba(74, 222, 128, 0.3); display: inline-flex; align-items: center; gap: 6px;">
|
||||||
@if(!user_alternative_infobox && item.is_chan) — <span class="badge badge-dark chan-post-badge" style="background: rgba(74, 222, 128, 0.15); border: 1px solid rgba(74, 222, 128, 0.3); display: inline-flex; align-items: center; gap: 6px;">
|
|
||||||
<a href="/4/{{ item.external_board }}/{{ item.external_tid }}" style="color: #4ade80; text-decoration: none; font-weight: 600;" title="View thread overview"><i class="fa-solid fa-clover"></i> /{{ item.external_board }}/{{ item.external_tid }}</a>
|
<a href="/4/{{ item.external_board }}/{{ item.external_tid }}" style="color: #4ade80; text-decoration: none; font-weight: 600;" title="View thread overview"><i class="fa-solid fa-clover"></i> /{{ item.external_board }}/{{ item.external_tid }}</a>
|
||||||
@if(item.local_id)
|
@if(item.local_id)
|
||||||
<a href="/{{ item.local_path || item.local_id }}" class="chan-rehost-action-btn rehosted" title="Already rehosted on f0ckm (/{{ item.local_path || item.local_id }})" style="display:inline-flex;align-items:center;justify-content:center;gap:5px;padding:3px 10px;border-radius:4px;font-size:0.75rem;background:rgba(74,222,128,0.2);color:#4ade80;border:1px solid rgba(74,222,128,0.4);text-decoration:none;font-weight:600;white-space:nowrap;flex-shrink:0;"><i class="fa-solid fa-check"></i> /{{ item.local_path || item.local_id }}</a>
|
<a href="/{{ item.local_path || item.local_id }}" class="chan-rehost-action-btn rehosted" title="Already rehosted on f0ckm (/{{ item.local_path || item.local_id }})" style="display:inline-flex;align-items:center;justify-content:center;gap:5px;padding:3px 10px;border-radius:4px;font-size:0.75rem;background:rgba(74,222,128,0.2);color:#4ade80;border:1px solid rgba(74,222,128,0.4);text-decoration:none;font-weight:600;white-space:nowrap;flex-shrink:0;"><i class="fa-solid fa-check"></i> /{{ item.local_path || item.local_id }}</a>
|
||||||
|
|||||||
@@ -142,7 +142,6 @@
|
|||||||
@endif
|
@endif
|
||||||
</td>
|
</td>
|
||||||
<td class="user-cell">
|
<td class="user-cell">
|
||||||
@if(list[i].admin)<span class="admin-icon" title="Administrator">⚡</span>@endif
|
|
||||||
<a href="/user/{{ list[i].user }}">{!! list[i].display_name || list[i].user !!}</a>
|
<a href="/user/{{ list[i].user }}">{!! list[i].display_name || list[i].user !!}</a>
|
||||||
</td>
|
</td>
|
||||||
<td class="count-cell">{{ list[i].count }}</td>
|
<td class="count-cell">{{ list[i].count }}</td>
|
||||||
|
|||||||
@@ -28,15 +28,6 @@
|
|||||||
<button type="button" class="album-btn album-btn-prev" title="{{ t('album.prev') || 'Previous' }}" aria-label="Previous">
|
<button type="button" class="album-btn album-btn-prev" title="{{ t('album.prev') || 'Previous' }}" aria-label="Previous">
|
||||||
<i class="fa-solid fa-chevron-left"></i>
|
<i class="fa-solid fa-chevron-left"></i>
|
||||||
</button>
|
</button>
|
||||||
<div class="album-counter-pill" title="Subf0cks">
|
|
||||||
<i class="fa-solid fa-layer-group"></i>
|
|
||||||
<span class="album-current-idx">1</span> / <span class="album-total-count">{{ item.album.length }}</span>
|
|
||||||
@if(can_manage_item)
|
|
||||||
<button type="button" class="album-sub-delete-btn" id="a_delete_sub" title="Delete current slide from album" aria-label="Delete slide" style="background: none; border: none; color: #ff6b6b; margin-left: 8px; cursor: pointer; pointer-events: auto; padding: 0 2px;">
|
|
||||||
<i class="fa-solid fa-trash-can"></i>
|
|
||||||
</button>
|
|
||||||
@endif
|
|
||||||
</div>
|
|
||||||
<button type="button" class="album-btn album-btn-next" title="{{ t('album.next') || 'Next' }}" aria-label="Next">
|
<button type="button" class="album-btn album-btn-next" title="{{ t('album.next') || 'Next' }}" aria-label="Next">
|
||||||
<i class="fa-solid fa-chevron-right"></i>
|
<i class="fa-solid fa-chevron-right"></i>
|
||||||
</button>
|
</button>
|
||||||
@@ -50,7 +41,7 @@
|
|||||||
<i class="fa-solid fa-music"></i>
|
<i class="fa-solid fa-music"></i>
|
||||||
</div>
|
</div>
|
||||||
@else
|
@else
|
||||||
<img src="{{ img.thumb || img.src }}" loading="lazy" alt="Subf0ck {{ img.slug || img.id }}" />
|
<img src="{{ img.thumb || img.src }}" loading="lazy" draggable="false" alt="Subf0ck {{ img.slug || img.id }}" />
|
||||||
@endif
|
@endif
|
||||||
@if(img.is_video)
|
@if(img.is_video)
|
||||||
<span class="album-thumb-mime-badge"><i class="fa-solid fa-play"></i></span>
|
<span class="album-thumb-mime-badge"><i class="fa-solid fa-play"></i></span>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<div class="profile_head" id="live-profile-preview-box" data-banner-user="{{ user.user || '' }}" style="position: relative; @if(user.banner_file && user_banner_enabled) --author-banner: url('/a/{{ user.banner_file }}'); --author-banner-position: {{ user.banner_position === 'center' ? 'center top' : (user.banner_position || 'center top') }}; --author-banner-size: {{ user.banner_size || 'cover' }}; @endif">
|
<div class="profile_head" id="live-profile-preview-box" data-banner-user="{{ user.user || '' }}" style="position: relative; @if(user.banner_file && user_banner_enabled) --author-banner: url('/a/{{ user.banner_file }}'); --author-banner-position: {{ user.banner_position === 'center' ? 'center top' : (user.banner_position || 'center top') }}; --author-banner-size: {{ user.banner_size || 'cover' }}; @endif">
|
||||||
@if(session && session.id === user.user_id && !session.is_anon && user_banner_enabled)
|
@if(session && session.id === user.user_id && !session.is_anon && user_banner_enabled)
|
||||||
<div id="inline-banner-edit-wrapper" style="display: none; position:absolute; top: 5px; right: 5px; z-index: 10;">
|
<div id="inline-banner-edit-wrapper" style="display: none; position:absolute; top: 5px; right: 5px; z-index: 10;">
|
||||||
<button id="inline-banner-edit-btn" onclick="const menu = document.getElementById('banner-edit-menu'); menu.style.display = menu.style.display === 'none' ? 'block' : 'none';" style="background: rgba(0,0,0,0.6); border: 1px solid rgba(255,255,255,0.2); border-radius: 4px; padding: 4px 8px; color: #fff; cursor: pointer; font-size: 0.8em;" title="{{ t('profile.edit_banner', 'Edit Banner') }}">
|
<button id="inline-banner-edit-btn" onclick="const menu = document.getElementById('banner-edit-menu'); menu.style.display = menu.style.display === 'none' ? 'block' : 'none';" type="button" title="{{ t('profile.edit_banner', 'Edit Banner') }}">
|
||||||
<i class="fa-solid fa-panorama"></i>
|
<i class="fa-solid fa-panorama"></i>
|
||||||
</button>
|
</button>
|
||||||
<div id="banner-edit-menu" style="display: none; position: absolute; right: 0; top: 100%; margin-top: 5px; background: #222; border: 1px solid #444; border-radius: 4px; padding: 5px; min-width: 120px; box-shadow: 0 2px 10px rgba(0,0,0,0.5);">
|
<div id="banner-edit-menu" style="display: none; position: absolute; right: 0; top: 100%; margin-top: 5px; background: #222; border: 1px solid #444; border-radius: 4px; padding: 5px; min-width: 120px; box-shadow: 0 2px 10px rgba(0,0,0,0.5);">
|
||||||
@@ -41,14 +41,14 @@
|
|||||||
@endif
|
@endif
|
||||||
</div>
|
</div>
|
||||||
@if(session && session.id === user.user_id && !session.is_anon)
|
@if(session && session.id === user.user_id && !session.is_anon)
|
||||||
<button id="edit-profile-btn" class="btn btn-sm btn-outline-primary" onclick="const box = document.getElementById('live-profile-preview-box'); box.classList.toggle('edit-mode-active'); if(box.classList.contains('edit-mode-active')){ this.innerHTML='<i class="fa-solid fa-check"></i>'; this.style.color='#51cf66'; this.style.borderColor='#51cf66'; } else { this.innerHTML='<i class="fa-solid fa-pen"></i>'; this.style.color='var(--accent)'; this.style.borderColor='var(--accent)'; }" style="position: absolute; bottom: 5px; left: 5px; padding: 3px 8px; font-size: 0.85em; border: 1px solid var(--accent); color: var(--accent); background: rgba(0,0,0,0.8); cursor: pointer; border-radius: 4px; z-index: 20; box-shadow: 0 0 5px rgba(0,0,0,0.5);" title="{{ t('profile.edit_btn', 'Edit Profile') }}"><i class="fa-solid fa-pen"></i></button>
|
<button id="edit-profile-btn" type="button" class="btn btn-sm" onclick="const box = document.getElementById('live-profile-preview-box'); const on = box.classList.toggle('edit-mode-active'); this.classList.toggle('is-active', on); this.innerHTML = on ? '<i class="fa-solid fa-check"></i>' : '<i class="fa-solid fa-pen"></i>';" title="{{ t('profile.edit_btn', 'Edit Profile') }}"><i class="fa-solid fa-pen"></i></button>
|
||||||
@endif
|
@endif
|
||||||
</div>
|
</div>
|
||||||
<div class="layersoffear">
|
<div class="layersoffear">
|
||||||
<div class="profile_head_username">
|
<div class="profile_head_username">
|
||||||
<div style="display: flex; justify-content: space-between; align-items: flex-start; gap: 10px;">
|
<div style="display: flex; justify-content: space-between; align-items: flex-start; gap: 10px;">
|
||||||
<div style="flex: 1; min-width: 0; word-break: break-word; line-height: 1.2; align-self: flex-start; margin-top: 0; display: flex; align-items: center; gap: 5px;" id="profile-name-container">
|
<div style="flex: 1; min-width: 0; word-break: break-word; line-height: 1.2; align-self: flex-start; margin-top: 0; display: flex; align-items: center; gap: 5px;" id="profile-name-container">
|
||||||
<span id="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(session && 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 && 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(session && 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 && !session.is_anon)
|
@if(session && session.id === user.user_id && !session.is_anon)
|
||||||
<button id="inline-name-edit-btn" style="background: transparent; border:none; color: var(--text-muted); cursor: pointer; font-size: 0.7em; padding: 0;" title="Edit Name & Color">
|
<button id="inline-name-edit-btn" style="background: transparent; border:none; color: var(--text-muted); cursor: pointer; font-size: 0.7em; padding: 0;" title="Edit Name & Color">
|
||||||
<i class="fa-solid fa-pen"></i>
|
<i class="fa-solid fa-pen"></i>
|
||||||
@@ -108,7 +108,7 @@
|
|||||||
<div class="stat-comments">{{ t('profile.stat_comments') }} <a href="/user/{!! user.user !!}/comments">{{ count.comments }}</a></div>
|
<div class="stat-comments">{{ t('profile.stat_comments') }} <a href="/user/{!! user.user !!}/comments">{{ count.comments }}</a></div>
|
||||||
@endif
|
@endif
|
||||||
<div class="stat-tags">{{ t('profile.stat_tags') }} <a href="/user/{!! user.user !!}/tags">{{ count.tags }}</a></div>
|
<div class="stat-tags">{{ t('profile.stat_tags') }} <a href="/user/{!! user.user !!}/tags">{{ count.tags }}</a></div>
|
||||||
@if(!user.is_ghost)
|
@if(!user.is_ghost && userhalls_enabled)
|
||||||
<div class="stat-halls">{{ t('profile.stat_halls') }} <a href="/user/{!! user.user !!}/halls">{{ count.halls }}</a></div>
|
<div class="stat-halls">{{ t('profile.stat_halls') }} <a href="/user/{!! user.user !!}/halls">{{ count.halls }}</a></div>
|
||||||
@endif
|
@endif
|
||||||
@endif
|
@endif
|
||||||
|
|||||||
Reference in New Issue
Block a user