dev #27

Merged
RHPZAdmin merged 3 commits from dev into master 2026-07-01 09:51:49 +00:00
21 changed files with 403 additions and 38 deletions

View File

@@ -64,6 +64,19 @@ class XenForoGuard implements Guard
if ($this->hasUser())
return $this->user;
$user = $this->getFromSession();
if( $user )
return $user;
$user = $this->getFromCookie();
if( $user )
return $user;
return null;
}
private function getFromSession(): ?XenForoUser
{
$sessionId = $this->request->cookie('xf_session');
if(!$sessionId)
return null;
@@ -92,6 +105,64 @@ class XenForoGuard implements Guard
return $this->user = new XenForoUser($xfUser);
}
private function isCorrectCookieKey(string $key, $record): bool
{
$known = $record->remember_key;
if( !$known )
return false;
$check = hash('sha256', $key, true);
return hash_equals($known, $check);
}
private function getFromCookie(): ?XenForoUser
{
$cookie = $this->request->cookie('xf_user');
if(!$cookie)
return null;
$parts = explode(',', $cookie);
if( count( $parts ) !== 2 )
return null;
[$userId, $key] = $parts;
$userId = (int) $userId;
if( !$userId || !$key )
return null;
$remembers = \DB::connection('xenforo')
->table('user_remember')
->where('user_id', $userId)
->get();
if( !$remembers )
return null;
$valid = false;
foreach( $remembers as $remember )
{
if( $this->isCorrectCookieKey($key, $remember) && $remember->expiry_date >= time() ){
$valid = true;
break;
}
}
if( !$valid )
return null;
$xfUser = \DB::connection('xenforo')
->table('user')
->where('user_id', $userId)
->first();
if(!$xfUser)
return null;
return $this->user = new XenForoUser($xfUser);
}
/**
* Unused.
*

View File

@@ -125,4 +125,18 @@ class EntryHelpers {
session(["downloaded_file_{$entryFile->file_uuid}" => 1]);
return true;
}
public static function stripBbCode(?string $text): ?string
{
if ($text === null) return null;
$text = preg_replace('/\[quote\b[^\]]*\](.*?)\[\/quote\]/is', '', $text);
return preg_replace('/\[\/?\w+[^\]]*\]/i', '', $text);
}
public static function stripMarkdown(?string $text): ?string
{
if ($text === null) return null;
$html = Str::markdown($text);
return html_entity_decode(strip_tags($html), ENT_QUOTES, 'UTF-8');
}
}

View File

@@ -2,6 +2,7 @@
namespace App\Services;
use App\Helpers\EntryHelpers;
use App\Models\Entry;
use App\Models\EntryReview;
use App\Models\News;
@@ -65,7 +66,7 @@ class ActivityService
'user_id' => $entry->user_id,
'badge' => EntryCard::ENTRY_TYPES_BADGE[$entry->type],
'badge_class' => $entry->type,
'excerpt' => $entry->description ? \Str::limit(strip_tags($entry->description), 80) : null,
'excerpt' => $entry->description ? \Str::limit(EntryHelpers::stripMarkdown(strip_tags($entry->description)), 80) : null,
'meta' => $entry->getRealPlatform()?->name
];
}
@@ -82,7 +83,7 @@ class ActivityService
'user_id' => $news->user_id,
'badge' => 'News',
'badge_class' => 'news',
'excerpt' => $news->description ? \Str::limit(strip_tags($news->description), 80) : null,
'excerpt' => $news->description ? \Str::limit(EntryHelpers::stripMarkdown(strip_tags($news->description)), 80) : null,
'meta' => $news->category?->name
];
}
@@ -99,7 +100,7 @@ class ActivityService
'user_id' => $message->user_id,
'badge' => 'Post',
'badge_class' => 'message',
'excerpt' => $message->message ? \Str::limit(strip_tags($message->message), 80) : null,
'excerpt' => $message->message ? \Str::limit(EntryHelpers::stripBbCode(strip_tags($message->message)), 80) : null,
'meta' => null
];
@@ -117,7 +118,7 @@ class ActivityService
'user_id' => $thread->user_id,
'badge' => 'Thread',
'badge_class' => 'thread',
'excerpt' => $thread->message ? \Str::limit(strip_tags($thread->message), 80) : null,
'excerpt' => $thread->message ? \Str::limit(EntryHelpers::stripBbCode(strip_tags($thread->message)), 80) : null,
'meta' => null
];
@@ -152,7 +153,7 @@ class ActivityService
'user_id' => $review->user_id,
'badge' => 'Review',
'badge_class' => 'review',
'excerpt' => $review->description ? \Str::limit(strip_tags($review->description), 80) : null,
'excerpt' => $review->description ? \Str::limit(EntryHelpers::stripMarkdown(strip_tags($review->description)), 80) : null,
'meta' => $review->entry()->exists() ? ( $review->entry->complete_title ?? $review->entry->title ) : null,
];
}

View File

@@ -30,6 +30,7 @@
border: 1px solid var(--border);
display: flex;
min-width: 0;
max-width: 100%;
flex-direction: column;
transition: transform 0.2s, border-color 0.2s;
cursor: pointer;
@@ -72,6 +73,7 @@
.entry-card-info {
padding: 15px;
flex-grow: 1;
min-width: 0;
display: flex;
flex-direction: column;
}
@@ -82,13 +84,16 @@
font-size: 1.1rem;
margin-bottom: 5px;
line-height: 1.3;
word-wrap: break-word;
overflow-wrap: anywhere;
word-break: break-word;
}
.entry-card-author {
color: var(--rhpz-orange);
font-size: 0.85rem;
margin-bottom: 10px;
overflow-wrap: anywhere;
word-break: break-word;
}
.entry-card-meta {
@@ -148,6 +153,9 @@
.entry-card-meta {
font-size: 0.75rem;
flex-direction: column;
align-items: flex-start;
gap: 6px;
}
}

View File

@@ -43,6 +43,45 @@
background-color: rgba(129, 199, 132, 0.1);
}
.back-to-top {
display: none;
}
@media (max-width: 768px) {
.back-to-top {
display: none;
align-items: center;
justify-content: center;
gap: 8px;
position: fixed;
right: 14px;
bottom: 14px;
z-index: 1000;
padding: 10px 14px;
border-radius: 999px;
background-color: var(--bg2);
border: 1px solid var(--border);
color: var(--text);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.2);
opacity: 0;
pointer-events: none;
transform: translateY(10px);
transition: opacity 0.2s ease, transform 0.2s ease;
}
.back-to-top.visible {
display: inline-flex;
opacity: 1;
pointer-events: auto;
transform: translateY(0);
}
.back-to-top span {
font-size: 0.78rem;
font-weight: 600;
}
}
/* BLOCK */
.block {

View File

@@ -227,8 +227,10 @@
display: flex;
justify-content: center;
align-items: center;
flex-wrap: wrap;
gap: 4px;
margin-top: 20px;
width: 100%;
.btn {
min-width: 36px;
@@ -255,18 +257,20 @@
}
@media (max-width: 900px) {
.database-layout {
flex-direction: column;
}
.database-wrapper {
flex-direction: column;
width: 100%;
}
.database-filters {
width: 100%;
display: grid;
grid-template-columns: repeat(2,1fr);
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.database-results {
width: 100%;
min-width: 0;
}
.database-filter-group:last-child {
@@ -286,13 +290,32 @@
flex-wrap: wrap;
}
.filter-bar {
flex-direction: column;
align-items: stretch;
}
.filter-bar .filter-bar-search {
max-width: none;
flex: initial;
width: 100%;
}
.filter-bar .btn {
width: 100%;
justify-content: center;
}
.database-wrapper {
flex-direction: column;
gap: 15px;
width: 100%;
}
.database-filters {
width: 100%;
width: min(100%, 420px);
max-width: 420px;
margin: 0 auto;
grid-template-columns: 1fr;
order: -1;
margin-bottom: 10px;
@@ -303,7 +326,7 @@
}
.grid-entries {
grid-template-columns: repeat(3, 1fr);
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 15px;
}
}
@@ -311,6 +334,20 @@
@media (max-width: 600px) {
.database-search {
flex-direction: column;
align-items: stretch;
}
.filter-bar {
padding: 12px;
}
.filter-bar .filter-bar-search {
max-width: none;
}
.filter-bar .btn {
width: 100%;
justify-content: center;
}
.database-filters {
@@ -318,7 +355,7 @@
}
.grid-entries {
grid-template-columns: repeat(2, 1fr);
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 12px;
}

View File

@@ -393,6 +393,37 @@
min-width: 20px;
text-align: center;
}
.gallery-mobile-controls {
display: none;
}
@media (max-width: 1024px) {
.gallery-mobile-controls {
display: flex;
justify-content: flex-end;
gap: 6px;
margin-top: 6px;
}
.gallery-move-btn {
display: inline-flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
border: 1px solid var(--border);
background-color: var(--bg2);
color: var(--text);
cursor: pointer;
padding: 0;
}
.gallery-move-btn:disabled {
opacity: 0.4;
cursor: not-allowed;
}
}
.authors-list {
display: grid;
grid-template-columns: repeat(4, 1fr);
@@ -510,13 +541,30 @@
}
@media (max-width: 600px) {
.upload-item-actions {
.upload-item {
flex-direction: column;
align-items: stretch;
}
.upload-item-info {
width: 100%;
flex: 1 1 auto;
min-width: 0;
}
.upload-item-actions {
flex-direction: row;
justify-content: flex-end;
align-items: center;
gap: 8px;
width: 100%;
margin-top: 8px;
flex-basis: 100%;
}
.upload-item-actions .btn {
width: 100%;
width: auto;
flex: 0 0 auto;
}
}
.file-state-icon { width: 18px; height: 18px; }
@@ -673,10 +721,18 @@
.grid-hashes {
grid-template-columns: 1fr;
}
.grid-credits {
grid-template-columns: 1fr;
}
.grid-first {
display: none;
}
.hash-first {
display: none;
}
.author-search-selected {
display: none;
}
}
@media (max-width: 600px) {
@@ -706,4 +762,20 @@
.form-error-text {
font-size: 0.8rem;
}
.form-type-of-checkboxes {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 1rem;
label {
box-sizing: border-box;
margin: 0;
}
input[type="checkbox"] {
transform: scale(1.5);
margin-right: 1.33rem;
}
}
}

View File

@@ -30,8 +30,38 @@
.grid-entries {
display: grid;
grid-template-columns: repeat(6,1fr);
grid-template-columns: repeat(6, minmax(0, 1fr));
gap: 20px;
margin-bottom: 20px;
}
@media (max-width: 1400px) {
.grid-entries {
grid-template-columns: repeat(5, minmax(0, 1fr));
}
}
@media (max-width: 1200px) {
.grid-entries {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
}
@media (max-width: 900px) {
.grid-entries {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
}
@media (max-width: 640px) {
.grid-entries {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
@media (max-width: 420px) {
.grid-entries {
grid-template-columns: 1fr;
}
}

View File

@@ -223,7 +223,7 @@
.activity-tl-card-description {
font-size: 0.8rem;
color: var(--text2);
white-space: nowrap;
word-break: break-word;
text-overflow: ellipsis;
line-height: 1.3;
}
@@ -265,13 +265,15 @@
@media (max-width: 600px) {
.activity-tl-header { flex-direction: column; align-items: flex-start; }
.activity-tl-thumb { display: none; }
.activity-tl-left { display: none; }
.activity-tl-card-description { display: none; }
.activity-day-sep { padding-left: 44px; }
.activity-tl-left { width: 44px; }
.activity-tl-date {
font-size: 0.75rem;
}
.activity-tl-content-title {
font-size: 0.9rem;
}
@@ -279,7 +281,7 @@
@media (max-width: 768px) {
.activity-timeline {
padding-left: 50px;
padding-left: 0px;
}
.activity-tl-left {

View File

@@ -101,6 +101,7 @@
line-height: 1.6;
color: var(--text);
margin-bottom: 30px;
word-break: break-word;
}
.entry-gallery {

View File

@@ -7,6 +7,7 @@
flex-shrink: 0;
transition: transform 0.3s ease;
z-index: 100;
overflow: hidden;
.menu-header {
padding: 10px;
@@ -44,9 +45,12 @@
}
.menu-navigation {
flex-grow: 1;
flex: 1 1 auto;
min-height: 0;
padding: 10px 0;
overflow-y: auto;
-webkit-overflow-scrolling: touch;
overscroll-behavior: contain;
.menu-group {
margin-bottom: 20px;
@@ -96,7 +100,9 @@
}
.menu-user {
padding: 15px 20px;
flex-shrink: 0;
margin-top: auto;
padding: 15px 20px calc(15px + env(safe-area-inset-bottom)) 20px;
border-top: 1px solid var(--border);
display: flex;
align-items: center;
@@ -131,6 +137,7 @@
&.username {
font-size: 0.9rem;
font-weight: 600;
text-decoration: none !important;
}
&.user_role {
font-size: 0.75rem;

View File

@@ -8,7 +8,8 @@
position: fixed;
left: 0;
top: 60px;
height: calc(100vh - 60px);
height: calc(100dvh - 60px);
max-height: calc(100dvh - 60px);
transform: translateX(-100%);
transition: transform 0.3s ease-in-out;
z-index: 999;

View File

@@ -131,15 +131,38 @@ export function GalleryManager() {
this.dragSrcI = index;
},
moveImageUp(index){
if( index <= 0 )
return;
const moved = this.images.splice(index, 1)[0];
this.images.splice(index - 1, 0, moved);
},
moveImageDown(index){
if( index >= this.images.length - 1 )
return;
const moved = this.images.splice(index, 1)[0];
this.images.splice(index + 1, 0, moved);
},
reorderImages(from, to){
if( from === null || to === null || from === to )
return;
const moved = this.images.splice(from, 1)[0];
this.images.splice(to, 0, moved);
this.dragSrcI = to;
},
dragOver(e, index){
e.preventDefault();
if( this.dragSrcI === null || this.dragSrcI === index )
return;
const moved = this.images.splice(this.dragSrcI, 1)[0];
this.images.splice(index, 0, moved);
this.dragSrcI = index;
this.reorderImages(this.dragSrcI, index);
},
dragEnd(){

View File

@@ -8,6 +8,7 @@ import notifications from "./notifications.js";
import conversations from "./conversations.js";
import settings from "./settings.js";
import { initMobileMenu } from "./mobile-menu.js";
import { initMobileBackToTop } from "./mobile-back-to-top.js"
/**
* Get config defined in meta.blade.php
@@ -47,3 +48,4 @@ Alpine.store('settings', settings() );
// Mobile Menu
document.addEventListener('DOMContentLoaded', initMobileMenu);
document.addEventListener( 'DOMContentLoaded', initMobileBackToTop );

View File

@@ -72,6 +72,7 @@ export default function hovercard(){
Alpine.nextTick(() => {
const card = document.querySelector('.hovercard');
if (card) window.refreshIcons(card);
this.updatePosition(this.anchorEl);
});
} catch( error ){
@@ -89,13 +90,26 @@ export default function hovercard(){
const RECT = anchorEl.getBoundingClientRect();
const SCROLL_X = window.scrollX;
const SCROLL_Y = window.scrollY;
const VIEWPORT_WIDTH = window.innerWidth;
const VIEWPORT_HEIGHT = window.innerHeight;
let x = RECT.left + SCROLL_X;
const CARD = document.querySelector('.hovercard');
const WIDTH = CARD?.offsetWidth || 280;
const HEIGHT = CARD?.offsetHeight || 320;
let x = RECT.right + SCROLL_X + 8;
let y = RECT.bottom + SCROLL_Y + 8;
const WIDTH = 280;
if( x + WIDTH > window.innerWidth ){
x = window.innerWidth - WIDTH - 16;
if( x + WIDTH > VIEWPORT_WIDTH - 8 && RECT.left + SCROLL_X - WIDTH - 8 >= 8 ){
x = RECT.left + SCROLL_X - WIDTH - 8;
} else {
x = Math.max(8, Math.min(x, VIEWPORT_WIDTH - WIDTH - 8));
}
if( y + HEIGHT > VIEWPORT_HEIGHT + SCROLL_Y - 8 && RECT.top + SCROLL_Y - HEIGHT - 8 >= 8 ){
y = RECT.top + SCROLL_Y - HEIGHT - 8;
} else {
y = Math.max(8, Math.min(y, VIEWPORT_HEIGHT + SCROLL_Y - HEIGHT - 8));
}
this.x = x;

View File

@@ -0,0 +1,18 @@
export function initMobileBackToTop(){
const backToTopButton = document.querySelector('.back-to-top');
const content = document.getElementById('content');
if (!backToTopButton || !content) {
return;
}
const toggleBackToTop = () => {
const shouldShow = window.innerWidth <= 768 && content.scrollTop > 320;
backToTopButton.classList.toggle('visible', shouldShow);
};
toggleBackToTop();
content.addEventListener('scroll', toggleBackToTop, { passive: true });
window.addEventListener('resize', toggleBackToTop, { passive: true });
}

View File

@@ -15,7 +15,15 @@
</div>
<div class="form-gallery form-group level" style="flex:4;">
<template x-for="(image,i) in images" :key="image.key">
<div class="gallery-item" :class="{ 'gallery-item--dragging': dragSrcI === i }" draggable="true" @dragstart="dragStart(i)" @dragover="dragOver($event, i)" @dragend="dragEnd()">
<div
class="gallery-item"
:class="{ 'gallery-item--dragging': dragSrcI === i }"
draggable="true"
:data-gallery-index="i"
@dragstart="dragStart(i)"
@dragover="dragOver($event, i)"
@dragend="dragEnd()"
>
<div class="form-image-preview-wrap">
<div class="gallery-drag-handle" title="Drag to reorder">
<i data-lucide="grip-vertical" size="14"></i>
@@ -28,6 +36,14 @@
X
</button>
</div>
<div class="gallery-mobile-controls" aria-label="Reorder screenshot">
<button type="button" class="gallery-move-btn" @click="moveImageUp(i)" :disabled="i === 0" title="Move up">
<i data-lucide="chevron-up" size="14"></i>
</button>
<button type="button" class="gallery-move-btn" @click="moveImageDown(i)" :disabled="i === images.length - 1" title="Move down">
<i data-lucide="chevron-down" size="14"></i>
</button>
</div>
</div>
</template>
</div>

View File

@@ -2,9 +2,9 @@
<x-form-field-title name="Staff/Credits" />
<template x-if="credits.length > 0">
<div class="form-group grid-credits">
<div><x-form-field-title name="Name" /></div>
<div><x-form-field-title name="Description" /></div>
<div><x-form-field-title name="Actions" /></div>
<div class="grid-first"><x-form-field-title name="Name" /></div>
<div class="grid-first"><x-form-field-title name="Description" /></div>
<div class="grid-first"><x-form-field-title name="Actions" /></div>
<template x-for="(credit,i) in credits" :key="i">
<div style="display:contents">
<div>

View File

@@ -54,7 +54,7 @@
</div>
<div class="comment-body">
{!! $comment['message'] !!}
{!! \App\Helpers\EntryHelpers::stripBbCode($comment['message']) !!}
</div>
</div>
</div>

View File

@@ -33,6 +33,11 @@
</div>
<button type="button" class="back-to-top" aria-label="Back to top" onclick="document.getElementById('content')?.scrollTo({ top: 0, behavior: 'smooth' });">
<i data-lucide="arrow-up"></i>
<span>Top</span>
</button>
@include('components.hovercard')
@livewireScripts
@stack('scripts')

View File

@@ -38,7 +38,11 @@
</div>
<div class="menu-user-info">
<span class="username">
{{ $VISITOR->username ?? "Guest" }}
@if( $VISITOR->guest() )
Guest
@else
<x-xf-username-link :user-id="$VISITOR->user_id" />
@endif
</span>
<span class="user_role">
<a href="{{ $VISITOR->guest() ? xfRoute('login') : xfRoute('logout') . '?t=' . xfCsrfToken() }}">