A lot of things
This commit is contained in:
@@ -3,6 +3,7 @@
|
|||||||
namespace App\Console\Commands;
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
use App\Models\Entry;
|
use App\Models\Entry;
|
||||||
|
use App\Models\News;
|
||||||
use Illuminate\Console\Attributes\Description;
|
use Illuminate\Console\Attributes\Description;
|
||||||
use Illuminate\Console\Attributes\Signature;
|
use Illuminate\Console\Attributes\Signature;
|
||||||
use Illuminate\Console\Command;
|
use Illuminate\Console\Command;
|
||||||
@@ -20,7 +21,11 @@ class DeleteRejectedEntries extends Command
|
|||||||
$count = Entry::where('state', 'rejected')
|
$count = Entry::where('state', 'rejected')
|
||||||
->where('rejected_at', '<', now()->subDays($days))
|
->where('rejected_at', '<', now()->subDays($days))
|
||||||
->delete();
|
->delete();
|
||||||
|
$count += News::where('state', 'rejected')
|
||||||
|
->where('rejected_at', '<', now()->subDays($days))
|
||||||
|
->delete();
|
||||||
|
|
||||||
$this->info("Deleted {$count} entries");
|
$this->info("Deleted {$count} entries/news");
|
||||||
|
return self::SUCCESS;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
31
app/Console/Commands/PurgeFeaturedEntries.php
Normal file
31
app/Console/Commands/PurgeFeaturedEntries.php
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use Illuminate\Console\Attributes\Description;
|
||||||
|
use Illuminate\Console\Attributes\Signature;
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
use App\Models\Entry;
|
||||||
|
|
||||||
|
#[Signature('entries:purge-featured {--days=15}')]
|
||||||
|
#[Description('Remove Featured from entries higher than X days')]
|
||||||
|
class PurgeFeaturedEntries extends Command
|
||||||
|
{
|
||||||
|
public function handle()
|
||||||
|
{
|
||||||
|
$days = $this->option('days');
|
||||||
|
|
||||||
|
$cutoff = now()->subDays($days);
|
||||||
|
|
||||||
|
$count = Entry::query()
|
||||||
|
->where('featured', true)
|
||||||
|
->where('featured_at', '<=', $cutoff)
|
||||||
|
->update([
|
||||||
|
'featured' => false,
|
||||||
|
'featured_at' => null,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->info("$count entr" . ($count > 1 ? 'ies' : 'y') . " unfeatured.");
|
||||||
|
return self::SUCCESS;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,8 @@
|
|||||||
namespace App\Helpers;
|
namespace App\Helpers;
|
||||||
|
|
||||||
use App\Models\Entry;
|
use App\Models\Entry;
|
||||||
|
use App\Models\EntryFile;
|
||||||
|
use App\Models\News;
|
||||||
use App\Services\XenforoApiService;
|
use App\Services\XenforoApiService;
|
||||||
use Illuminate\Support\Facades\Cache;
|
use Illuminate\Support\Facades\Cache;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
@@ -62,13 +64,16 @@ class EntryHelpers {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function getLatestComments(Entry $entry, int $limit = 20): array {
|
public static function getLatestComments(Entry|News $entry, int $limit = 20): array {
|
||||||
|
|
||||||
if( !$entry->comments_thread_id ){
|
if( !$entry->comments_thread_id ){
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
$cacheKey = "entry_comments_{$entry->id}";
|
if( is_a( $entry, News::class ) )
|
||||||
|
$cacheKey = "news_comments_{$entry->id}";
|
||||||
|
else
|
||||||
|
$cacheKey = "entry_comments_{$entry->id}";
|
||||||
return Cache::remember($cacheKey, now()->addDays(1), function () use ($entry, $limit) {
|
return Cache::remember($cacheKey, now()->addDays(1), function () use ($entry, $limit) {
|
||||||
|
|
||||||
$service = app(XenforoApiService::class);
|
$service = app(XenforoApiService::class);
|
||||||
@@ -93,6 +98,25 @@ class EntryHelpers {
|
|||||||
|
|
||||||
public static function enableOnlinePatcherBasedOnExtension(string $filename): bool
|
public static function enableOnlinePatcherBasedOnExtension(string $filename): bool
|
||||||
{
|
{
|
||||||
return Str::endsWith(Str::lower($filename), ['.ips', '.bps', '.xdelta', '.zip' ]);
|
return Str::endsWith(Str::lower($filename), ['.ips', '.bps', '.xdelta', '.ups', '.aps', '.ppf', '.zip' ]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getYoutubeVideoId(string $url): ?string
|
||||||
|
{
|
||||||
|
$pattern = '%(?:https?://)?(?:www\.|m\.)?(?:youtu\.be/|youtube(?:-nocookie)?\.com/(?:watch\?.*v=|embed/|v/|shorts/|live/))([\w-]{11})%i';
|
||||||
|
|
||||||
|
preg_match($pattern, $url, $matches);
|
||||||
|
return $matches[1] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function fileAlreadyDownloaded(EntryFile $entryFile): bool
|
||||||
|
{
|
||||||
|
return session("downloaded_file_{$entryFile->file_uuid}", null ) !== null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function markFileAsDownloaded(EntryFile $entryFile): bool
|
||||||
|
{
|
||||||
|
session(["downloaded_file_{$entryFile->file_uuid}" => 1]);
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
57
app/Helpers/PlayOnlineHelpers.php
Normal file
57
app/Helpers/PlayOnlineHelpers.php
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Helpers;
|
||||||
|
|
||||||
|
class PlayOnlineHelpers
|
||||||
|
{
|
||||||
|
public static function getCoreLists(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'nes',
|
||||||
|
'fceumm',
|
||||||
|
'nestopia',
|
||||||
|
'snes',
|
||||||
|
'snes9x',
|
||||||
|
'bsnes',
|
||||||
|
'gb',
|
||||||
|
'gambatte',
|
||||||
|
'gba',
|
||||||
|
'mgba',
|
||||||
|
'nds',
|
||||||
|
'melonds',
|
||||||
|
'desmume2015',
|
||||||
|
'desmume',
|
||||||
|
'a5200',
|
||||||
|
'mame2003',
|
||||||
|
'mame2003_plus',
|
||||||
|
'fbneo',
|
||||||
|
'psx',
|
||||||
|
'pcsx_rearmed',
|
||||||
|
'mednafen_psx_hw',
|
||||||
|
'segaSaturn',
|
||||||
|
'yabuase',
|
||||||
|
'segaMD',
|
||||||
|
'segaGG',
|
||||||
|
'segaCD',
|
||||||
|
'genesis_plus_gx',
|
||||||
|
'n64',
|
||||||
|
'mupen64plus_next',
|
||||||
|
'parallel-n64',
|
||||||
|
'atari7800',
|
||||||
|
'prosystem',
|
||||||
|
'atari2600',
|
||||||
|
'stella2014',
|
||||||
|
'sega32x',
|
||||||
|
'picodrive',
|
||||||
|
'segaMS',
|
||||||
|
'smsplus',
|
||||||
|
'c64',
|
||||||
|
'vice_x64sc',
|
||||||
|
'same_cdi',
|
||||||
|
'psp',
|
||||||
|
'ppsspp',
|
||||||
|
'3ds',
|
||||||
|
'azahar'
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ namespace App\Helpers;
|
|||||||
|
|
||||||
use App\Auth\XenForoUser;
|
use App\Auth\XenForoUser;
|
||||||
use App\Models\Entry;
|
use App\Models\Entry;
|
||||||
|
use App\Models\News;
|
||||||
use App\Services\XenforoApiService;
|
use App\Services\XenforoApiService;
|
||||||
|
|
||||||
class XenForoHelpers {
|
class XenForoHelpers {
|
||||||
@@ -44,7 +45,7 @@ class XenForoHelpers {
|
|||||||
$service->updateEntriesCount( $count, $userId );
|
$service->updateEntriesCount( $count, $userId );
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function entryApproved( Entry $entry ): void
|
public static function entryApproved( Entry|News $entry ): void
|
||||||
{
|
{
|
||||||
// 1. Update XF Entry count.
|
// 1. Update XF Entry count.
|
||||||
self::updateEntriesCount( $entry->user_id );
|
self::updateEntriesCount( $entry->user_id );
|
||||||
@@ -58,6 +59,29 @@ class XenForoHelpers {
|
|||||||
$title = "Entry approved : {$entry->title}";
|
$title = "Entry approved : {$entry->title}";
|
||||||
$message = "Your entry {$entry->title} has been approved by {$moderator}.";
|
$message = "Your entry {$entry->title} has been approved by {$moderator}.";
|
||||||
|
|
||||||
|
$service = app(XenForoApiService::class);
|
||||||
|
|
||||||
|
$service->createConversation([ $entry->user_id ], $title, $message, false, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function entryRejected( Entry|News $entry ): void
|
||||||
|
{
|
||||||
|
// 1. Update XF Entry count.
|
||||||
|
self::updateEntriesCount( $entry->user_id );
|
||||||
|
|
||||||
|
// 2. Send a private message
|
||||||
|
/*
|
||||||
|
if( \Auth::user()->user_id === $entry->user_id ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
$moderator = \Auth::user()->username;
|
||||||
|
$title = "Entry rejected : {$entry->title}";
|
||||||
|
$message = "Your entry {$entry->title} has been rejected by {$moderator}.\nReason: {$entry->staff_comment}\n\nYou have 7 days to edit your entry before it is permanently deleted.";
|
||||||
|
|
||||||
|
$service = app(XenForoApiService::class);
|
||||||
|
|
||||||
$service->createConversation([ $entry->user_id ], $title, $message, false, false);
|
$service->createConversation([ $entry->user_id ], $title, $message, false, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,8 +3,10 @@
|
|||||||
namespace App\Http\Controllers;
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
use App\Helpers\XenForoHelpers;
|
use App\Helpers\XenForoHelpers;
|
||||||
|
use App\Services\ActivityService;
|
||||||
use App\Services\XenforoApiService;
|
use App\Services\XenforoApiService;
|
||||||
use App\Services\XenforoService;
|
use App\Services\XenforoService;
|
||||||
|
use Illuminate\Http\JsonResponse;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\Cache;
|
use Illuminate\Support\Facades\Cache;
|
||||||
|
|
||||||
@@ -63,4 +65,24 @@ class DynamicLoadController extends Controller
|
|||||||
|
|
||||||
return response()->json( $data );
|
return response()->json( $data );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function activityFeed(Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
$availableFilters = ['entries', 'news', 'messages', 'threads', 'clubs'];
|
||||||
|
|
||||||
|
$requested = $request->query('filters')
|
||||||
|
? explode(',', $request->query('filters'))
|
||||||
|
: [];
|
||||||
|
|
||||||
|
$activeFilters = !empty($requested)
|
||||||
|
? array_intersect($requested, $availableFilters)
|
||||||
|
: $availableFilters;
|
||||||
|
|
||||||
|
$service = app(ActivityService::class);
|
||||||
|
$items = $service->getActivities(array_values($activeFilters));
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'html' => view('activity.timeline', compact('items'))->render(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ namespace App\Http\Controllers;
|
|||||||
|
|
||||||
use App\Helpers\EntryHelpers;
|
use App\Helpers\EntryHelpers;
|
||||||
use App\Models\Entry;
|
use App\Models\Entry;
|
||||||
|
use App\Models\News;
|
||||||
use Illuminate\Support\Facades\Gate;
|
use Illuminate\Support\Facades\Gate;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\View\View;
|
use Illuminate\View\View;
|
||||||
@@ -11,7 +12,7 @@ use Illuminate\View\View;
|
|||||||
class EntryController extends Controller
|
class EntryController extends Controller
|
||||||
{
|
{
|
||||||
|
|
||||||
private const SECTION_TYPES = ['translations', 'romhacks', 'homebrew', 'utilities', 'documents', 'lua-scripts', 'tutorials'];
|
private const SECTION_TYPES = ['translations', 'romhacks', 'homebrew', 'utilities', 'documents', 'lua-scripts'];
|
||||||
|
|
||||||
public function index(): View
|
public function index(): View
|
||||||
{
|
{
|
||||||
@@ -31,7 +32,8 @@ class EntryController extends Controller
|
|||||||
if ($entry->type !== $section)
|
if ($entry->type !== $section)
|
||||||
abort(404);
|
abort(404);
|
||||||
|
|
||||||
Gate::authorize('viewAny', $entry);
|
if( !\Auth::guest() )
|
||||||
|
Gate::authorize('viewAny', $entry);
|
||||||
|
|
||||||
// Permissions.
|
// Permissions.
|
||||||
$entryPolicy = match ($entry->state) {
|
$entryPolicy = match ($entry->state) {
|
||||||
@@ -61,7 +63,12 @@ class EntryController extends Controller
|
|||||||
->orderBy('updated_at', 'desc')
|
->orderBy('updated_at', 'desc')
|
||||||
->paginate(20);
|
->paginate(20);
|
||||||
|
|
||||||
return view('entries.drafts', compact('drafts'));
|
$newsDrafts = News::where('user_id', \Auth::user()->user_id )
|
||||||
|
->where('state', 'draft')
|
||||||
|
->orderBy('updated_at', 'desc')
|
||||||
|
->paginate(20);
|
||||||
|
|
||||||
|
return view('entries.drafts', compact('drafts', 'newsDrafts'));
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
23
app/Http/Controllers/EntryFeaturedRequestController.php
Normal file
23
app/Http/Controllers/EntryFeaturedRequestController.php
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Models\Entry;
|
||||||
|
use App\Services\XenforoApiService;
|
||||||
|
use App\Services\XenforoService;
|
||||||
|
use Illuminate\Http\JsonResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
class EntryFeaturedRequestController extends Controller
|
||||||
|
{
|
||||||
|
|
||||||
|
public function featuredRequest(Request $request, Entry $entry)
|
||||||
|
{
|
||||||
|
$service = app(XenforoApiService::class);
|
||||||
|
$response = $service->featuredRequest($entry);
|
||||||
|
return response()->json([
|
||||||
|
'success' => $response,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ namespace App\Http\Controllers;
|
|||||||
|
|
||||||
use App\Helpers\EntryHelpers;
|
use App\Helpers\EntryHelpers;
|
||||||
use App\Models\EntryFile;
|
use App\Models\EntryFile;
|
||||||
|
use App\Models\LogXfUser;
|
||||||
use App\Services\FileServersService;
|
use App\Services\FileServersService;
|
||||||
use Illuminate\Http\Client\ConnectionException;
|
use Illuminate\Http\Client\ConnectionException;
|
||||||
use Illuminate\Http\JsonResponse;
|
use Illuminate\Http\JsonResponse;
|
||||||
@@ -45,7 +46,7 @@ class FileServerController extends Controller {
|
|||||||
return response()->json($data);
|
return response()->json($data);
|
||||||
}
|
}
|
||||||
|
|
||||||
\Cache::put("uploaded_file_{$fileUuid}", [
|
$fileData = [
|
||||||
'uuid' => $fileUuid,
|
'uuid' => $fileUuid,
|
||||||
'type' => $type,
|
'type' => $type,
|
||||||
'filename' => $filename,
|
'filename' => $filename,
|
||||||
@@ -54,7 +55,16 @@ class FileServerController extends Controller {
|
|||||||
'favorite_server' => $data['favorite_server'],
|
'favorite_server' => $data['favorite_server'],
|
||||||
'favorite_at' => time(),
|
'favorite_at' => time(),
|
||||||
'state' => 'public',
|
'state' => 'public',
|
||||||
], now()->addHours(2) );
|
];
|
||||||
|
|
||||||
|
activity('entry-file')
|
||||||
|
->causedBy(LogXfUser::find(\Auth::user()->getAuthIdentifier()))
|
||||||
|
->withProperties($fileData)
|
||||||
|
->event('file_upload')
|
||||||
|
->log("File uploaded")
|
||||||
|
;
|
||||||
|
|
||||||
|
\Cache::put("uploaded_file_{$fileUuid}", $fileData, now()->addHours(2) );
|
||||||
|
|
||||||
$data['finished'] = true;
|
$data['finished'] = true;
|
||||||
return response()->json($data);
|
return response()->json($data);
|
||||||
@@ -67,7 +77,11 @@ class FileServerController extends Controller {
|
|||||||
abort(404);
|
abort(404);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: DL Count.
|
if( !EntryHelpers::fileAlreadyDownloaded($file) ) {
|
||||||
|
EntryHelpers::markFileAsDownloaded($file);
|
||||||
|
$file->increaseDownloadCount();
|
||||||
|
}
|
||||||
|
|
||||||
return redirect( $this->fs->getDownloadFileUrl( $file) );
|
return redirect( $this->fs->getDownloadFileUrl( $file) );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,14 +2,42 @@
|
|||||||
|
|
||||||
namespace App\Http\Controllers;
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Models\Entry;
|
||||||
|
use App\Services\ActivityService;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\View\View;
|
use Illuminate\View\View;
|
||||||
|
use App\Models\News;
|
||||||
|
|
||||||
class HomeController extends Controller
|
class HomeController extends Controller
|
||||||
{
|
{
|
||||||
|
|
||||||
public function index(): View {
|
public function __construct( private ActivityService $service ) {}
|
||||||
return view('home');
|
|
||||||
|
public function index( Request $request ): View {
|
||||||
|
|
||||||
|
$filters = [ 'entries', 'news', 'messages', 'threads', 'clubs' ];
|
||||||
|
|
||||||
|
$cookie = $request->cookie('activity_filters');
|
||||||
|
$activeFilters = $cookie ? array_intersect( json_decode( $cookie, true ) ?? [], $filters ) : $filters;
|
||||||
|
|
||||||
|
if( empty( $activeFilters ) ) {
|
||||||
|
$activeFilters = $filters;
|
||||||
|
}
|
||||||
|
|
||||||
|
$items = $this->service->getActivities( array_values( $activeFilters ) );
|
||||||
|
|
||||||
|
$viewFilters = [
|
||||||
|
'entries' => ['label' => 'Entries', 'icon' => 'database'],
|
||||||
|
'news' => ['label' => 'News', 'icon' => 'newspaper'],
|
||||||
|
'messages' => ['label' => 'Posts', 'icon' => 'message-square'],
|
||||||
|
'threads' => ['label' => 'Threads', 'icon' => 'messages-square'],
|
||||||
|
'clubs' => ['label' => 'Clubs', 'icon' => 'balloon'],
|
||||||
|
];
|
||||||
|
|
||||||
|
$latestNews = News::published()->latest('created_at')->limit(5)->get();
|
||||||
|
$featuredEntries = Entry::published()->where('featured', true)->latest('featured_at')->get();
|
||||||
|
|
||||||
|
return view('home', compact('items', 'activeFilters', 'viewFilters', 'latestNews', 'featuredEntries'));
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -75,4 +75,9 @@ class ModCPController extends Controller
|
|||||||
$entry->forceDelete();
|
$entry->forceDelete();
|
||||||
return back()->with('success', "Entry permanently deleted");
|
return back()->with('success', "Entry permanently deleted");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function logs()
|
||||||
|
{
|
||||||
|
return view('modcp.logs');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
124
app/Http/Controllers/NewsController.php
Normal file
124
app/Http/Controllers/NewsController.php
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Exceptions\SubmissionException;
|
||||||
|
use App\Helpers\EntryHelpers;
|
||||||
|
use App\Http\Requests\StoreDraftRequest;
|
||||||
|
use App\Http\Requests\StoreEntryRequest;
|
||||||
|
use App\Http\Requests\StoreNewsDraftRequest;
|
||||||
|
use App\Http\Requests\StoreNewsRequest;
|
||||||
|
use App\Jobs\DeleteXenForoCommentsThread;
|
||||||
|
use App\Models\News;
|
||||||
|
use App\Services\NewsService;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Gate;
|
||||||
|
|
||||||
|
class NewsController extends Controller
|
||||||
|
{
|
||||||
|
|
||||||
|
public function index()
|
||||||
|
{
|
||||||
|
return view('news.index');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function show(Request $request, News $news)
|
||||||
|
{
|
||||||
|
if( !\Auth::guest() )
|
||||||
|
Gate::authorize('viewAny', $news);
|
||||||
|
|
||||||
|
// Permissions.
|
||||||
|
$entryPolicy = match ($news->state) {
|
||||||
|
'pending' => 'viewPending',
|
||||||
|
'draft' => 'viewDraft',
|
||||||
|
'rejected' => 'viewRejected',
|
||||||
|
'hidden' => 'viewHidden',
|
||||||
|
'locked' => 'viewLocked',
|
||||||
|
'published' => null,
|
||||||
|
'default' => null
|
||||||
|
};
|
||||||
|
|
||||||
|
if ($entryPolicy)
|
||||||
|
Gate::authorize($entryPolicy, $news);
|
||||||
|
|
||||||
|
$comments = EntryHelpers::getLatestComments($news);
|
||||||
|
|
||||||
|
return view('news.show', compact('news', 'comments'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function create(Request $request)
|
||||||
|
{
|
||||||
|
$data = [
|
||||||
|
'news' => new News(),
|
||||||
|
'isEdit' => false,
|
||||||
|
'oldCategory' => old('category') ? [ old('category') ] : []
|
||||||
|
];
|
||||||
|
|
||||||
|
return view ('news.create', $data);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function edit(Request $request, News $news)
|
||||||
|
{
|
||||||
|
$data = [
|
||||||
|
'news' => $news,
|
||||||
|
'isEdit' => true,
|
||||||
|
'oldCategory' => old('category', $news->category_id) ? [ old('category', $news->category_id) ] : []
|
||||||
|
];
|
||||||
|
|
||||||
|
return view ('news.edit', $data);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function store(Request $request)
|
||||||
|
{
|
||||||
|
$request = $request->input('submit-state') === 'draft' ? app(StoreNewsDraftRequest::class) : app(StoreNewsRequest::class);
|
||||||
|
$request->validateResolved();
|
||||||
|
|
||||||
|
$service = app(NewsService::class);
|
||||||
|
|
||||||
|
try {
|
||||||
|
$entry = $service->storeNews($request);
|
||||||
|
|
||||||
|
return match ($entry->state) {
|
||||||
|
'published' => redirect()->route('news.show', ['news' => $entry->slug])->with('success', "Your entry has been published."),
|
||||||
|
'pending' => redirect()->route('home')->with('success', "Your entry has been submitted and is pending review."),
|
||||||
|
default => redirect()->route('home')->with('success', "Your entry has been saved as a draft.")
|
||||||
|
};
|
||||||
|
} catch ( SubmissionException $e ) {
|
||||||
|
return back()->withInput()->withErrors(['error' => $e->getMessage()]);
|
||||||
|
} catch ( \Exception $e ) {
|
||||||
|
return back()->withInput()->withErrors(['error' => 'Unknown error: '.$e->getMessage()]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function update(Request $request, News $news)
|
||||||
|
{
|
||||||
|
$request = $request->input('submit-state') === 'draft' ? app(StoreNewsDraftRequest::class) : app(StoreNewsRequest::class);
|
||||||
|
$request->validateResolved();
|
||||||
|
|
||||||
|
$service = app(NewsService::class);
|
||||||
|
|
||||||
|
try {
|
||||||
|
$news = $service->editNews($request, $news);
|
||||||
|
|
||||||
|
return match ($news->state) {
|
||||||
|
'published' => redirect()->route('news.show', ['news' => $news->slug])->with('success', "Your entry has been published."),
|
||||||
|
'pending' => redirect()->route('home')->with('success', "Your entry has been submitted and is pending review."),
|
||||||
|
default => redirect()->route('home')->with('success', "Your entry has been saved as a draft.")
|
||||||
|
};
|
||||||
|
} catch ( SubmissionException $e ) {
|
||||||
|
return back()->withInput()->withErrors(['error' => $e->getMessage()]);
|
||||||
|
} catch ( \Exception $e ) {
|
||||||
|
return back()->withInput()->withErrors(['error' => 'Unknown error: '.$e->getMessage()]);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public function destroy(Request $request, News $news)
|
||||||
|
{
|
||||||
|
if( $news->comments_thread_id )
|
||||||
|
DeleteXenForoCommentsThread::dispatch( $news->comments_thread_id );
|
||||||
|
|
||||||
|
$news->delete();
|
||||||
|
return redirect( route('news.index') )->with('success', "Entry successfully deleted.");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ namespace App\Http\Controllers;
|
|||||||
|
|
||||||
use App\Helpers\XenForoHelpers;
|
use App\Helpers\XenForoHelpers;
|
||||||
use App\Models\Entry;
|
use App\Models\Entry;
|
||||||
|
use App\Models\News;
|
||||||
use App\Services\XenforoService;
|
use App\Services\XenforoService;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
class QueueController extends Controller
|
class QueueController extends Controller
|
||||||
@@ -14,7 +15,25 @@ class QueueController extends Controller
|
|||||||
->with(['authors', 'game.platform'])
|
->with(['authors', 'game.platform'])
|
||||||
->orderByRaw("CASE WHEN state = 'pending' THEN 1 ELSE 0 END")
|
->orderByRaw("CASE WHEN state = 'pending' THEN 1 ELSE 0 END")
|
||||||
->orderBy('created_at', 'asc')
|
->orderBy('created_at', 'asc')
|
||||||
->get();
|
->get()
|
||||||
|
->map(fn($item) => $item->setAttribute('queue_type', 'entry'));
|
||||||
|
|
||||||
|
$news = News::inQueue()
|
||||||
|
->orderByRaw("CASE WHEN state = 'pending' THEN 1 ELSE 0 END")
|
||||||
|
->orderBy('created_at', 'asc')
|
||||||
|
->get()
|
||||||
|
->map(fn($item) => $item->setAttribute('queue_type', 'news'));
|
||||||
|
|
||||||
|
$entries = $entries->concat($news)->sort(function($a, $b) {
|
||||||
|
$aPending = $a->state === 'pending' ? 0 : 1;
|
||||||
|
$bPending = $b->state === 'pending' ? 0 : 1;
|
||||||
|
|
||||||
|
if($aPending !== $bPending) {
|
||||||
|
return $aPending <=> $bPending;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $a->created_at <=> $b->created_at;
|
||||||
|
})->values();
|
||||||
|
|
||||||
return view('queue.index', compact('entries'));
|
return view('queue.index', compact('entries'));
|
||||||
}
|
}
|
||||||
@@ -25,24 +44,53 @@ class QueueController extends Controller
|
|||||||
|
|
||||||
$entry->update(['staff_comment' => $request->input('comment')]);
|
$entry->update(['staff_comment' => $request->input('comment')]);
|
||||||
|
|
||||||
|
return back()->with('success', 'Comment supdated');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function updateComment_news(Request $request, News $news)
|
||||||
|
{
|
||||||
|
$request->validate(['comment' => 'nullable|string|max:2000']);
|
||||||
|
|
||||||
|
$news->update(['staff_comment' => $request->input('comment')]);
|
||||||
|
|
||||||
return back()->with('success', 'Comment updated');
|
return back()->with('success', 'Comment updated');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function approve(Request $request, Entry $entry)
|
public function approve(Request $request, Entry $entry)
|
||||||
{
|
{
|
||||||
// $entry->update(['state' => 'published']);
|
$entry->update(['state' => 'published', 'created_at' => now()]);
|
||||||
|
|
||||||
XenForoHelpers::entryApproved($entry);
|
XenForoHelpers::entryApproved($entry);
|
||||||
|
|
||||||
return back()->with('success', 'Entry approved');
|
return back()->with('success', 'Entry approved');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function approve_news(Request $request, News $news)
|
||||||
|
{
|
||||||
|
$news->update(['state' => 'published', 'created_at' => now()]);
|
||||||
|
|
||||||
|
XenForoHelpers::entryApproved($news);
|
||||||
|
|
||||||
|
return back()->with('success', 'Entry approved');
|
||||||
|
}
|
||||||
|
|
||||||
public function reject(Request $request, Entry $entry)
|
public function reject(Request $request, Entry $entry)
|
||||||
{
|
{
|
||||||
$request->validate(['reason' => 'nullable|string|max:2000']);
|
$request->validate(['reason' => 'nullable|string|max:2000']);
|
||||||
|
|
||||||
$entry->update(['state' => 'rejected', 'staff_comment' => $request->input('reason'), 'rejected_at' => now() ]);
|
$entry->update(['state' => 'rejected', 'staff_comment' => $request->input('reason'), 'rejected_at' => now() ]);
|
||||||
|
|
||||||
|
XenForoHelpers::entryRejected($entry);
|
||||||
|
return back()->with('success', 'Entry rejected');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function reject_news(Request $request, News $news)
|
||||||
|
{
|
||||||
|
$request->validate(['reason' => 'nullable|string|max:2000']);
|
||||||
|
|
||||||
|
$news->update(['state' => 'rejected', 'staff_comment' => $request->input('reason'), 'rejected_at' => now() ]);
|
||||||
|
|
||||||
|
XenForoHelpers::entryRejected($news);
|
||||||
return back()->with('success', 'Entry rejected');
|
return back()->with('success', 'Entry rejected');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,4 +14,13 @@ class RedirectController extends Controller
|
|||||||
|
|
||||||
return redirect()->route('entries.show', ['section' => $entry->type, 'entry' => $entry])->with('success', "Your report has been sent.");
|
return redirect()->route('entries.show', ['section' => $entry->type, 'entry' => $entry])->with('success', "Your report has been sent.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function newsReportRedirect( Request $request )
|
||||||
|
{
|
||||||
|
$id = $request->input('id');
|
||||||
|
$entry = News::findOrFail($id);
|
||||||
|
|
||||||
|
return redirect()->route('news.show', ['news' => $entry])->with('success', "Your report has been sent.");
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,6 +33,62 @@ class SubmissionController extends Controller
|
|||||||
|
|
||||||
public function __construct(private SubmissionsService $services){}
|
public function __construct(private SubmissionsService $services){}
|
||||||
|
|
||||||
|
public function index(Request $request)
|
||||||
|
{
|
||||||
|
|
||||||
|
$entryTypes = [
|
||||||
|
'romhack' => [
|
||||||
|
'slug' => 'romhacks',
|
||||||
|
'label' => 'Romhack',
|
||||||
|
'icon' => 'gamepad-2',
|
||||||
|
'color' => '#4a6fc2',
|
||||||
|
'bg' => '#d5e1fc1a',
|
||||||
|
'border' => '#d5e1fc40',
|
||||||
|
],
|
||||||
|
'translation' => [
|
||||||
|
'slug' => 'translations',
|
||||||
|
'label' => 'Translation',
|
||||||
|
'icon' => 'languages',
|
||||||
|
'color' => '#4a8a2a',
|
||||||
|
'bg' => '#e7f4d91a',
|
||||||
|
'border' => '#e7f4d940',
|
||||||
|
],
|
||||||
|
'homebrew' => [
|
||||||
|
'slug' => 'homebrew',
|
||||||
|
'label' => 'Homebrew',
|
||||||
|
'icon' => 'cpu',
|
||||||
|
'color' => '#c23060',
|
||||||
|
'bg' => '#ffeaf01a',
|
||||||
|
'border' => '#ffeaf040',
|
||||||
|
],
|
||||||
|
'utility' => [
|
||||||
|
'slug' => 'utilities',
|
||||||
|
'label' => 'Utility',
|
||||||
|
'icon' => 'wrench',
|
||||||
|
'color' => '#8a6600',
|
||||||
|
'bg' => '#fff8d51a',
|
||||||
|
'border' => '#fff8d540',
|
||||||
|
],
|
||||||
|
'document' => [
|
||||||
|
'slug' => 'documents',
|
||||||
|
'label' => 'Document',
|
||||||
|
'icon' => 'file-text',
|
||||||
|
'color' => '#7a35c2',
|
||||||
|
'bg' => '#f3eaff1a',
|
||||||
|
'border' => '#f3eaff40',
|
||||||
|
],
|
||||||
|
'lua-script' => [
|
||||||
|
'slug' => 'lua-script',
|
||||||
|
'label' => 'Lua script',
|
||||||
|
'icon' => 'terminal',
|
||||||
|
'color' => '#a04515',
|
||||||
|
'bg' => '#eed6c51a',
|
||||||
|
'border' => '#eed6c540',
|
||||||
|
] ];
|
||||||
|
|
||||||
|
return view('submissions.index', compact('entryTypes'));
|
||||||
|
}
|
||||||
|
|
||||||
public function create(Request $request, string $section)
|
public function create(Request $request, string $section)
|
||||||
{
|
{
|
||||||
$data = [
|
$data = [
|
||||||
@@ -108,7 +164,7 @@ class SubmissionController extends Controller
|
|||||||
if( $entry->type !== $section )
|
if( $entry->type !== $section )
|
||||||
abort(404);
|
abort(404);
|
||||||
|
|
||||||
if( $entry->comments_thread_id)
|
if( $entry->comments_thread_id )
|
||||||
DeleteXenForoCommentsThread::dispatch( $entry->comments_thread_id );
|
DeleteXenForoCommentsThread::dispatch( $entry->comments_thread_id );
|
||||||
|
|
||||||
$entry->delete();
|
$entry->delete();
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace App\Http\Controllers;
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Models\Entry;
|
||||||
use App\Models\EntryFile;
|
use App\Models\EntryFile;
|
||||||
use App\Services\FileServersService;
|
use App\Services\FileServersService;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
@@ -15,9 +16,29 @@ class ToolsController extends Controller
|
|||||||
return view('tools.patcher');
|
return view('tools.patcher');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function directPatch( Request $request, int $entry_id, EntryFile $file )
|
public function directPatch( Request $request, int $entryId, EntryFile $file )
|
||||||
{
|
{
|
||||||
if( $file->entry_id != $entry_id ) {
|
if( $file->entry_id != $entryId ) {
|
||||||
|
abort(404);
|
||||||
|
}
|
||||||
|
|
||||||
|
$service = app(FileServersService::class);
|
||||||
|
|
||||||
|
|
||||||
|
$patches = [
|
||||||
|
'file' => $service->getDownloadFileUrl( $file ),
|
||||||
|
'name' => $file->entry->title,
|
||||||
|
'outputName' => $file->filename
|
||||||
|
];
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
return view('tools.patcher', compact('patches'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function play( Request $request, int $entryId, EntryFile $file )
|
||||||
|
{
|
||||||
|
if( $file->entry_id != $entryId ) {
|
||||||
abort(404);
|
abort(404);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -28,7 +49,11 @@ class ToolsController extends Controller
|
|||||||
'name' => $file->entry->title,
|
'name' => $file->entry->title,
|
||||||
'outputName' => $file->filename
|
'outputName' => $file->filename
|
||||||
];
|
];
|
||||||
|
$emuConfig = [
|
||||||
|
'core' => $file->playOnlineSetting?->core,
|
||||||
|
'threads' => $file->playOnlineSetting?->threads,
|
||||||
|
];
|
||||||
|
|
||||||
return view('tools.patcher', compact('patches'));
|
return view('tools.play', compact('patches', 'emuConfig'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -149,6 +149,9 @@ class StoreEntryRequest extends FormRequest
|
|||||||
$rules['files_metadata'] = 'array|nullable';
|
$rules['files_metadata'] = 'array|nullable';
|
||||||
$rules['files_metadata.*.online_patcher'] = 'nullable|boolean';
|
$rules['files_metadata.*.online_patcher'] = 'nullable|boolean';
|
||||||
$rules['files_metadata.*.secondary_online_patcher'] = 'nullable|boolean|required_with:files_metadata.*.online_patcher';
|
$rules['files_metadata.*.secondary_online_patcher'] = 'nullable|boolean|required_with:files_metadata.*.online_patcher';
|
||||||
|
$rules['files_metadata.*.play_online'] = 'nullable|boolean';
|
||||||
|
$rules['files_metadata.*.play_online_core'] = 'nullable|string';
|
||||||
|
$rules['files_metadata.*.play_online_threads'] = 'nullable|boolean';
|
||||||
}
|
}
|
||||||
|
|
||||||
if( $isEdit && $this->user()->can('moderate', $this->route('entry') ) ){
|
if( $isEdit && $this->user()->can('moderate', $this->route('entry') ) ){
|
||||||
|
|||||||
26
app/Http/Requests/StoreNewsDraftRequest.php
Normal file
26
app/Http/Requests/StoreNewsDraftRequest.php
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests;
|
||||||
|
|
||||||
|
class StoreNewsDraftRequest extends StoreNewsRequest
|
||||||
|
{
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
$rules = parent::rules();
|
||||||
|
$rules['submit-state'] = 'required|string|in:draft';
|
||||||
|
|
||||||
|
$rules = array_map(function($rule){
|
||||||
|
if( is_array($rule) ){
|
||||||
|
return array_map( fn($r) => $r === 'required' ? 'nullable' : $r, $rule);
|
||||||
|
}
|
||||||
|
|
||||||
|
return preg_replace(
|
||||||
|
['/\brequired_without\S*/', '/required_with\S*/', '/\brequired\b/'],
|
||||||
|
['nullable', 'nullable', 'nullable'],
|
||||||
|
$rule
|
||||||
|
);
|
||||||
|
}, $rules );
|
||||||
|
|
||||||
|
return $rules;
|
||||||
|
}
|
||||||
|
}
|
||||||
78
app/Http/Requests/StoreNewsRequest.php
Normal file
78
app/Http/Requests/StoreNewsRequest.php
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests;
|
||||||
|
|
||||||
|
use App\Models\News;
|
||||||
|
use App\Rules\PublicFileExists;
|
||||||
|
use App\Rules\XfUserExists;
|
||||||
|
use Illuminate\Contracts\Validation\ValidationRule;
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
|
||||||
|
class StoreNewsRequest extends FormRequest
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Determine if the user is authorized to make this request.
|
||||||
|
*/
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
$news = $this->route('news');
|
||||||
|
if( $news )
|
||||||
|
return $this->user()->can('update', $news);
|
||||||
|
|
||||||
|
return $this->user()->can('create', News::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function prepareForValidation(): void
|
||||||
|
{
|
||||||
|
$this->merge([
|
||||||
|
'gallery' => $this->input('gallery') !== '' ? $this->input('gallery') : null,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the validation rules that apply to the request.
|
||||||
|
*
|
||||||
|
* @return array<string, ValidationRule|array<mixed>|string>
|
||||||
|
*/
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
$isEdit = (bool) $this->route('news');
|
||||||
|
|
||||||
|
$rules = [];
|
||||||
|
|
||||||
|
$rules['title'] = 'required|string|max:255';
|
||||||
|
$rules['category'] = 'required|integer|exists:categories,id';
|
||||||
|
|
||||||
|
$rules['description'] = 'required|string';
|
||||||
|
$rules['gallery'] = 'array|required|min:1';
|
||||||
|
$rules['gallery.*'] = [ 'string', new PublicFileExists ];
|
||||||
|
|
||||||
|
$rules['entry_id'] = 'nullable|integer|exists:entries,id';
|
||||||
|
$rules['release_site'] = 'nullable|url|max:500';
|
||||||
|
$rules['youtube_video'] = 'nullable|url|max:500';
|
||||||
|
|
||||||
|
if( $isEdit ){
|
||||||
|
$ss = 'draft,pending,published';
|
||||||
|
if( \Auth::user()->can('moderate', $this->route('news')) && \Auth::user()->can('view-hidden', $this->route('news')) )
|
||||||
|
$ss .= ',hidden';
|
||||||
|
if(\Auth::user()->can('moderate', $this->route('news')) && \Auth::user()->can('view-locked', $this->route('news')) )
|
||||||
|
$ss .= ',locked';
|
||||||
|
$rules['submit-state'] = 'required|in:' . $ss;
|
||||||
|
} else {
|
||||||
|
if( $this->user()->can('skip-queue', '\App\Models\News') ){
|
||||||
|
$rules['submit-state'] = 'required|string|in:draft,pending,published';
|
||||||
|
} else {
|
||||||
|
$rules['submit-state'] = 'required|string|in:draft,pending';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if( $isEdit && $this->user()->can('moderate', $this->route('news') ) ){
|
||||||
|
$rules['staff_comment'] = 'nullable|string';
|
||||||
|
$rules['owner_user_id'] = [ 'required', 'integer', new XfUserExists ];
|
||||||
|
$rules['comments_thread_id'] = 'nullable|integer';
|
||||||
|
}
|
||||||
|
|
||||||
|
return $rules;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
namespace App\Jobs;
|
namespace App\Jobs;
|
||||||
|
|
||||||
use App\Models\Entry;
|
use App\Models\Entry;
|
||||||
|
use App\Models\News;
|
||||||
use App\Services\XenforoApiService;
|
use App\Services\XenforoApiService;
|
||||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||||
use Illuminate\Foundation\Queue\Queueable;
|
use Illuminate\Foundation\Queue\Queueable;
|
||||||
@@ -18,7 +19,7 @@ class CreateXenForoCommentsThread implements ShouldQueue
|
|||||||
* Create a new job instance.
|
* Create a new job instance.
|
||||||
*/
|
*/
|
||||||
public function __construct(
|
public function __construct(
|
||||||
protected Entry $entry
|
protected Entry|News $entry
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
//
|
//
|
||||||
|
|||||||
36
app/Jobs/DeleteFile.php
Normal file
36
app/Jobs/DeleteFile.php
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Jobs;
|
||||||
|
|
||||||
|
use App\Models\EntryFile;
|
||||||
|
use App\Services\FileServersService;
|
||||||
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||||
|
use Illuminate\Foundation\Queue\Queueable;
|
||||||
|
|
||||||
|
class DeleteFile implements ShouldQueue
|
||||||
|
{
|
||||||
|
use Queueable;
|
||||||
|
|
||||||
|
public $tries = 3;
|
||||||
|
public $backoff = 10;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new job instance.
|
||||||
|
*/
|
||||||
|
public function __construct(
|
||||||
|
public string $filePath,
|
||||||
|
public string $fileName,
|
||||||
|
public int $userId
|
||||||
|
)
|
||||||
|
{
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the job.
|
||||||
|
*/
|
||||||
|
public function handle(FileServersService $service): void
|
||||||
|
{
|
||||||
|
$service->deleteFile($this->filePath, $this->fileName, $this->userId);
|
||||||
|
}
|
||||||
|
}
|
||||||
64
app/Livewire/ActivityLogs.php
Normal file
64
app/Livewire/ActivityLogs.php
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Livewire;
|
||||||
|
|
||||||
|
use Livewire\Attributes\Url;
|
||||||
|
use Livewire\Component;
|
||||||
|
use Livewire\WithPagination;
|
||||||
|
use Spatie\Activitylog\Models\Activity;
|
||||||
|
|
||||||
|
class ActivityLogs extends Component
|
||||||
|
{
|
||||||
|
use WithPagination;
|
||||||
|
|
||||||
|
#[Url(except: '')]
|
||||||
|
public string $search = '';
|
||||||
|
|
||||||
|
#[Url(except: '')]
|
||||||
|
public string $logName = '';
|
||||||
|
|
||||||
|
#[Url(except: '')]
|
||||||
|
public string $event = '';
|
||||||
|
|
||||||
|
#[Url(except: '')]
|
||||||
|
public string $dateFrom = '';
|
||||||
|
|
||||||
|
#[Url(except: '')]
|
||||||
|
public string $dateTo = '';
|
||||||
|
|
||||||
|
#[Url(except: '')]
|
||||||
|
public string $causerId = '';
|
||||||
|
|
||||||
|
public function updating(): void { $this->resetPage(); }
|
||||||
|
|
||||||
|
public function clearFilters(): void
|
||||||
|
{
|
||||||
|
$this->reset('search', 'logName', 'event', 'dateFrom', 'dateTo', 'causerId');
|
||||||
|
$this->resetPage();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function render()
|
||||||
|
{
|
||||||
|
$logs = Activity::query()
|
||||||
|
->with(['causer'])
|
||||||
|
->when($this->search, fn($q) => $q
|
||||||
|
->where('description', 'like', "%{$this->search}%")
|
||||||
|
->orWhere('subject_type', 'like', "%{$this->search}%")
|
||||||
|
->orWhere('log_name', 'like', "%{$this->search}%")
|
||||||
|
)
|
||||||
|
->when($this->logName, fn($q) => $q->where('log_name', $this->logName))
|
||||||
|
->when($this->event, fn($q) => $q->where('event', $this->event))
|
||||||
|
->when($this->causerId, fn($q) => $q->where('causer_id', $this->causerId))
|
||||||
|
->when($this->dateFrom, fn($q) => $q->whereDate('created_at', '>=', $this->dateFrom))
|
||||||
|
->when($this->dateTo, fn($q) => $q->whereDate('created_at', '<=', $this->dateTo))
|
||||||
|
->latest()
|
||||||
|
->paginate(50);
|
||||||
|
|
||||||
|
$logNames = Activity::distinct()->orderBy('log_name')->pluck('log_name')->filter()->values();
|
||||||
|
$events = Activity::distinct()->orderBy('event')->pluck('event')->filter()->values();
|
||||||
|
|
||||||
|
$hasFilters = $this->logName || $this->event || $this->dateFrom || $this->dateTo || $this->causerId;
|
||||||
|
|
||||||
|
return view('livewire.activity-logs', compact('logs', 'logNames', 'events', 'hasFilters'));
|
||||||
|
}
|
||||||
|
}
|
||||||
72
app/Livewire/EntrySelector.php
Normal file
72
app/Livewire/EntrySelector.php
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Livewire;
|
||||||
|
|
||||||
|
use App\Models\Entry;
|
||||||
|
use Livewire\Component;
|
||||||
|
|
||||||
|
class EntrySelector extends Component
|
||||||
|
{
|
||||||
|
public string $search = '';
|
||||||
|
public ?int $selectedEntryId = null;
|
||||||
|
|
||||||
|
public ?string $entryName = null;
|
||||||
|
|
||||||
|
public bool $dropdown = false;
|
||||||
|
|
||||||
|
public function mount( ?int $oldEntryId = null ): void
|
||||||
|
{
|
||||||
|
if( $oldEntryId ) {
|
||||||
|
$entry = Entry::find($oldEntryId);
|
||||||
|
if( $entry ) {
|
||||||
|
$this->selectedEntryId = $oldEntryId;
|
||||||
|
$this->entryName = $entry->complete_title ?? $entry->title;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function updatedSearch(): void
|
||||||
|
{
|
||||||
|
if( $this->selectedEntryId ) {
|
||||||
|
$this->selectedEntryId = null;
|
||||||
|
$this->entryName = null;
|
||||||
|
}
|
||||||
|
$this->dropdown = strlen($this->search) > 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function selectEntry( int $id ){
|
||||||
|
$entry = Entry::find($id);
|
||||||
|
if( $entry ) {
|
||||||
|
$this->selectedEntryId = $id;
|
||||||
|
$this->entryName = $entry->complete_title ?? $entry->title;
|
||||||
|
$this->search = $entry->complete_title ?? $entry->title;
|
||||||
|
$this->dropdown = false;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function clearEntry(): void
|
||||||
|
{
|
||||||
|
$this->selectedEntryId = null;
|
||||||
|
$this->entryName = null;
|
||||||
|
$this->search = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function render()
|
||||||
|
{
|
||||||
|
$entries = collect();
|
||||||
|
|
||||||
|
if( $this->dropdown && strlen($this->search) > 2 ) {
|
||||||
|
$entries = Entry::where('complete_title', 'like', '%' . $this->search . '%')
|
||||||
|
->orWhere('title', 'like', '%' . $this->search . '%')
|
||||||
|
->orderBy('complete_title', 'asc')
|
||||||
|
->orderBy('title', 'asc')
|
||||||
|
->limit(20)
|
||||||
|
->get();
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = [ 'entries' => $entries, 'required_chars' => 3 ];
|
||||||
|
|
||||||
|
return view('livewire.entry-selector', $data);
|
||||||
|
}
|
||||||
|
}
|
||||||
104
app/Livewire/NewsDatabase.php
Normal file
104
app/Livewire/NewsDatabase.php
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Livewire;
|
||||||
|
|
||||||
|
use App\Models\Category;
|
||||||
|
use App\Models\News;
|
||||||
|
use Livewire\Attributes\Url;
|
||||||
|
use Livewire\Component;
|
||||||
|
use Livewire\WithPagination;
|
||||||
|
|
||||||
|
class NewsDatabase extends Component
|
||||||
|
{
|
||||||
|
use WithPagination;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* News title search
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
#[Url(as: 's', except: '')]
|
||||||
|
public string $search = '';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Categories IDs filter.
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
#[Url(except:[])]
|
||||||
|
public array $categories = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sort by field.
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
#[Url(as: 'sort',except: 'created_at')]
|
||||||
|
public string $sortBy = 'created_at';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* asc/desc
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
#[Url(as: 'dir',except: 'desc')]
|
||||||
|
public string $sortDir = 'desc';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Translation of sort options key.
|
||||||
|
*/
|
||||||
|
public const array SORT_OPTIONS = [
|
||||||
|
'created_at' => 'Date added',
|
||||||
|
'title' => 'Title'
|
||||||
|
];
|
||||||
|
|
||||||
|
public const int PAGINATION = 30;
|
||||||
|
|
||||||
|
public function updatedSearch(): void { $this->resetPage(); $this->dispatch('filters-updated'); }
|
||||||
|
public function updatedCategories(): void { $this->resetPage(); $this->dispatch('filters-updated'); }
|
||||||
|
public function updatedCategoriesMode(): void { $this->resetPage(); $this->dispatch('filters-updated'); }
|
||||||
|
|
||||||
|
public function clearFilters(): void
|
||||||
|
{
|
||||||
|
$this->reset([
|
||||||
|
'search', 'categories',
|
||||||
|
]);
|
||||||
|
$this->resetPage();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setSort(string $field): void
|
||||||
|
{
|
||||||
|
if( $this->sortBy === $field ) {
|
||||||
|
$this->sortDir = $this->sortDir === 'asc' ? 'desc' : 'asc';
|
||||||
|
} else {
|
||||||
|
$this->sortBy = $field;
|
||||||
|
$this->sortDir = 'asc';
|
||||||
|
}
|
||||||
|
$this->resetPage();
|
||||||
|
$this->dispatch('filters-updated');
|
||||||
|
}
|
||||||
|
|
||||||
|
private function buildQuery()
|
||||||
|
{
|
||||||
|
$query = News::query()->published();
|
||||||
|
|
||||||
|
if( $this->search ){
|
||||||
|
$query->where(function($q){
|
||||||
|
$q->where('title', 'like', '%'.$this->search.'%');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if( $this->categories ) {
|
||||||
|
$query->whereIn('category_id', $this->categories);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $query->orderBy($this->sortBy, $this->sortDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function render()
|
||||||
|
{
|
||||||
|
return view('livewire.news-database', [
|
||||||
|
'news' => $this->buildQuery()->paginate(self::PAGINATION),
|
||||||
|
'allCategories' => Category::where(function ($query) {
|
||||||
|
$query->whereJsonContains('restricted_to', "news")
|
||||||
|
->orWhereNull('restricted_to');
|
||||||
|
})->orderBy('name')->get()
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
|
use App\Helpers\EntryHelpers;
|
||||||
use App\Traits\HasGallery;
|
use App\Traits\HasGallery;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
@@ -10,6 +11,8 @@ use Illuminate\Database\Eloquent\Builder;
|
|||||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||||
use Monolog\Level;
|
use Monolog\Level;
|
||||||
|
use Spatie\Activitylog\Models\Concerns\LogsActivity;
|
||||||
|
use Spatie\Activitylog\Support\LogOptions;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @property int $id
|
* @property int $id
|
||||||
@@ -22,6 +25,7 @@ use Monolog\Level;
|
|||||||
* @property string|null $staff_comment
|
* @property string|null $staff_comment
|
||||||
* @property \Illuminate\Support\Carbon|null $rejected_at
|
* @property \Illuminate\Support\Carbon|null $rejected_at
|
||||||
* @property bool $featured
|
* @property bool $featured
|
||||||
|
* @property \Illuminate\Support\Carbon|null $featured_at
|
||||||
* @property int|null $game_id
|
* @property int|null $game_id
|
||||||
* @property int|null $platform_id
|
* @property int|null $platform_id
|
||||||
* @property int|null $status_id
|
* @property int|null $status_id
|
||||||
@@ -69,6 +73,7 @@ use Monolog\Level;
|
|||||||
* @method static Builder<static>|Entry whereDeletedAt($value)
|
* @method static Builder<static>|Entry whereDeletedAt($value)
|
||||||
* @method static Builder<static>|Entry whereDescription($value)
|
* @method static Builder<static>|Entry whereDescription($value)
|
||||||
* @method static Builder<static>|Entry whereFeatured($value)
|
* @method static Builder<static>|Entry whereFeatured($value)
|
||||||
|
* @method static Builder<static>|Entry whereFeaturedAt($value)
|
||||||
* @method static Builder<static>|Entry whereGameId($value)
|
* @method static Builder<static>|Entry whereGameId($value)
|
||||||
* @method static Builder<static>|Entry whereId($value)
|
* @method static Builder<static>|Entry whereId($value)
|
||||||
* @method static Builder<static>|Entry whereLevelId($value)
|
* @method static Builder<static>|Entry whereLevelId($value)
|
||||||
@@ -90,12 +95,14 @@ use Monolog\Level;
|
|||||||
* @method static Builder<static>|Entry whereYoutubeLink($value)
|
* @method static Builder<static>|Entry whereYoutubeLink($value)
|
||||||
* @method static Builder<static>|Entry withTrashed(bool $withTrashed = true)
|
* @method static Builder<static>|Entry withTrashed(bool $withTrashed = true)
|
||||||
* @method static Builder<static>|Entry withoutTrashed()
|
* @method static Builder<static>|Entry withoutTrashed()
|
||||||
|
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Spatie\Activitylog\Models\Activity> $activitiesAsSubject
|
||||||
|
* @property-read int|null $activities_as_subject_count
|
||||||
* @mixin \Eloquent
|
* @mixin \Eloquent
|
||||||
*/
|
*/
|
||||||
class Entry extends Model
|
class Entry extends Model
|
||||||
{
|
{
|
||||||
|
|
||||||
use SoftDeletes, HasGallery;
|
use SoftDeletes, HasGallery, LogsActivity;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var string[]
|
* @var string[]
|
||||||
@@ -108,6 +115,7 @@ class Entry extends Model
|
|||||||
'main_image',
|
'main_image',
|
||||||
'state',
|
'state',
|
||||||
'featured',
|
'featured',
|
||||||
|
'featured_at',
|
||||||
'game_id',
|
'game_id',
|
||||||
'platform_id',
|
'platform_id',
|
||||||
'status_id',
|
'status_id',
|
||||||
@@ -121,7 +129,8 @@ class Entry extends Model
|
|||||||
'comments_thread_id',
|
'comments_thread_id',
|
||||||
'staff_comment',
|
'staff_comment',
|
||||||
'rejected_at',
|
'rejected_at',
|
||||||
'level_id'
|
'level_id',
|
||||||
|
'created_at'
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -131,6 +140,7 @@ class Entry extends Model
|
|||||||
'featured' => 'boolean',
|
'featured' => 'boolean',
|
||||||
'release_date' => 'date',
|
'release_date' => 'date',
|
||||||
'rejected_at' => 'datetime',
|
'rejected_at' => 'datetime',
|
||||||
|
'featured_at' => 'datetime',
|
||||||
];
|
];
|
||||||
|
|
||||||
public function scopePublished( Builder $query ): Builder {
|
public function scopePublished( Builder $query ): Builder {
|
||||||
@@ -197,7 +207,7 @@ class Entry extends Model
|
|||||||
return $this->hasMany(EntryHash::class);
|
return $this->hasMany(EntryHash::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function parseStaffCredits(): array {
|
public function parseStaffCredits(): ?array {
|
||||||
return json_decode( $this->staff_credits ?? "", true );
|
return json_decode( $this->staff_credits ?? "", true );
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -205,10 +215,17 @@ class Entry extends Model
|
|||||||
if( !$this->youtube_link )
|
if( !$this->youtube_link )
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
$pattern = '%(?:https?://)?(?:www\.|m\.)?(?:youtu\.be/|youtube(?:-nocookie)?\.com/(?:watch\?.*v=|embed/|v/|shorts/|live/))([\w-]{11})%i';
|
return EntryHelpers::getYoutubeVideoId( $this->youtube_link );
|
||||||
|
}
|
||||||
|
|
||||||
preg_match($pattern, $this->youtube_link, $matches);
|
public function getActivitylogOptions(): LogOptions
|
||||||
return $matches[1] ?? null;
|
{
|
||||||
|
return LogOptions::defaults()
|
||||||
|
->useLogName('entry')
|
||||||
|
->logAll()
|
||||||
|
->logOnlyDirty()
|
||||||
|
->dontLogEmptyChanges()
|
||||||
|
->setDescriptionForEvent(fn(string $eventName) => "Entry {$eventName}");
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ namespace App\Models;
|
|||||||
|
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\HasOne;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @property int $id
|
* @property int $id
|
||||||
@@ -36,6 +37,9 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
|||||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|EntryFile whereSecondaryOnlinePatcher($value)
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|EntryFile whereSecondaryOnlinePatcher($value)
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|EntryFile whereState($value)
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|EntryFile whereState($value)
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|EntryFile whereUpdatedAt($value)
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|EntryFile whereUpdatedAt($value)
|
||||||
|
* @property-read \App\Models\PlayOnlineSetting|null $playOnlineSetting
|
||||||
|
* @property int $download_count
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|EntryFile whereDownloadCount($value)
|
||||||
* @mixin \Eloquent
|
* @mixin \Eloquent
|
||||||
*/
|
*/
|
||||||
class EntryFile extends Model
|
class EntryFile extends Model
|
||||||
@@ -53,4 +57,15 @@ class EntryFile extends Model
|
|||||||
return $this->belongsTo(Entry::class);
|
return $this->belongsTo(Entry::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function playOnlineSetting(): HasOne
|
||||||
|
{
|
||||||
|
return $this->hasOne(PlayOnlineSetting::class,'file_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function increaseDownloadCount(): void
|
||||||
|
{
|
||||||
|
$this->download_count++;
|
||||||
|
$this->save();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,5 +4,24 @@ namespace App\Models;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @deprecated Use Gallery instead.
|
* @deprecated Use Gallery instead.
|
||||||
|
* @property int $id
|
||||||
|
* @property string $galleryable_type
|
||||||
|
* @property int $galleryable_id
|
||||||
|
* @property string $image
|
||||||
|
* @property \Illuminate\Support\Carbon|null $created_at
|
||||||
|
* @property \Illuminate\Support\Carbon|null $updated_at
|
||||||
|
* @property-read \Illuminate\Database\Eloquent\Model|\Eloquent $galleryable
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|EntryGallery newModelQuery()
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|EntryGallery newQuery()
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|EntryGallery query()
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|EntryGallery whereCreatedAt($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|EntryGallery whereGalleryableId($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|EntryGallery whereGalleryableType($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|EntryGallery whereId($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|EntryGallery whereImage($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|EntryGallery whereUpdatedAt($value)
|
||||||
|
* @property int $order
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|EntryGallery whereOrder($value)
|
||||||
|
* @mixin \Eloquent
|
||||||
*/
|
*/
|
||||||
class EntryGallery extends Gallery {}
|
class EntryGallery extends Gallery {}
|
||||||
|
|||||||
@@ -19,6 +19,13 @@ use Illuminate\Database\Eloquent\Relations\MorphTo;
|
|||||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Gallery whereId($value)
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|Gallery whereId($value)
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Gallery whereImage($value)
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|Gallery whereImage($value)
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Gallery whereUpdatedAt($value)
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|Gallery whereUpdatedAt($value)
|
||||||
|
* @property string $galleryable_type
|
||||||
|
* @property int $galleryable_id
|
||||||
|
* @property-read Model|\Eloquent $galleryable
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|Gallery whereGalleryableId($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|Gallery whereGalleryableType($value)
|
||||||
|
* @property int $order
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|Gallery whereOrder($value)
|
||||||
* @mixin \Eloquent
|
* @mixin \Eloquent
|
||||||
*/
|
*/
|
||||||
class Gallery extends Model
|
class Gallery extends Model
|
||||||
|
|||||||
129
app/Models/LogXfUser.php
Normal file
129
app/Models/LogXfUser.php
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used only for logging details on XenForo user.
|
||||||
|
*
|
||||||
|
* Read-Only, must not be used in other things than logging.
|
||||||
|
*
|
||||||
|
* @property int $user_id
|
||||||
|
* @property string $username
|
||||||
|
* @property int $username_date
|
||||||
|
* @property int $username_date_visible
|
||||||
|
* @property string $email
|
||||||
|
* @property string $custom_title
|
||||||
|
* @property int $language_id
|
||||||
|
* @property int $style_id 0 = use system default
|
||||||
|
* @property string $style_variation
|
||||||
|
* @property string $timezone Example: 'Europe/London'
|
||||||
|
* @property int $visible Show browsing activity to others
|
||||||
|
* @property int $activity_visible
|
||||||
|
* @property int $user_group_id
|
||||||
|
* @property string $secondary_group_ids
|
||||||
|
* @property int $display_style_group_id User group ID that provides user styling
|
||||||
|
* @property int $permission_combination_id
|
||||||
|
* @property int $message_count
|
||||||
|
* @property int $question_solution_count
|
||||||
|
* @property int $conversations_unread
|
||||||
|
* @property int $register_date
|
||||||
|
* @property int $last_activity
|
||||||
|
* @property int|null $last_summary_email_date
|
||||||
|
* @property int $trophy_points
|
||||||
|
* @property int $alerts_unviewed
|
||||||
|
* @property int $alerts_unread
|
||||||
|
* @property int $avatar_date
|
||||||
|
* @property int $avatar_width
|
||||||
|
* @property int $avatar_height
|
||||||
|
* @property int $avatar_highdpi
|
||||||
|
* @property int $avatar_optimized
|
||||||
|
* @property string $gravatar If specified, this is an email address corresponding to the user's 'Gravatar'
|
||||||
|
* @property string $user_state
|
||||||
|
* @property string $security_lock
|
||||||
|
* @property int $is_moderator
|
||||||
|
* @property int $is_admin
|
||||||
|
* @property int $is_banned
|
||||||
|
* @property int $reaction_score
|
||||||
|
* @property int $vote_score
|
||||||
|
* @property int $warning_points
|
||||||
|
* @property int $is_staff
|
||||||
|
* @property string $secret_key
|
||||||
|
* @property int $privacy_policy_accepted
|
||||||
|
* @property int $terms_accepted
|
||||||
|
* @property int $rhpz_entry_count
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|LogXfUser newModelQuery()
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|LogXfUser newQuery()
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|LogXfUser query()
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|LogXfUser whereActivityVisible($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|LogXfUser whereAlertsUnread($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|LogXfUser whereAlertsUnviewed($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|LogXfUser whereAvatarDate($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|LogXfUser whereAvatarHeight($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|LogXfUser whereAvatarHighdpi($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|LogXfUser whereAvatarOptimized($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|LogXfUser whereAvatarWidth($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|LogXfUser whereConversationsUnread($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|LogXfUser whereCustomTitle($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|LogXfUser whereDisplayStyleGroupId($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|LogXfUser whereEmail($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|LogXfUser whereGravatar($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|LogXfUser whereIsAdmin($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|LogXfUser whereIsBanned($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|LogXfUser whereIsModerator($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|LogXfUser whereIsStaff($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|LogXfUser whereLanguageId($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|LogXfUser whereLastActivity($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|LogXfUser whereLastSummaryEmailDate($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|LogXfUser whereMessageCount($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|LogXfUser wherePermissionCombinationId($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|LogXfUser wherePrivacyPolicyAccepted($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|LogXfUser whereQuestionSolutionCount($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|LogXfUser whereReactionScore($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|LogXfUser whereRegisterDate($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|LogXfUser whereRhpzEntryCount($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|LogXfUser whereSecondaryGroupIds($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|LogXfUser whereSecretKey($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|LogXfUser whereSecurityLock($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|LogXfUser whereStyleId($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|LogXfUser whereStyleVariation($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|LogXfUser whereTermsAccepted($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|LogXfUser whereTimezone($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|LogXfUser whereTrophyPoints($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|LogXfUser whereUserGroupId($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|LogXfUser whereUserId($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|LogXfUser whereUserState($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|LogXfUser whereUsername($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|LogXfUser whereUsernameDate($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|LogXfUser whereUsernameDateVisible($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|LogXfUser whereVisible($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|LogXfUser whereVoteScore($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|LogXfUser whereWarningPoints($value)
|
||||||
|
* @mixin \Eloquent
|
||||||
|
*/
|
||||||
|
class LogXfUser extends Model
|
||||||
|
{
|
||||||
|
|
||||||
|
protected $connection = 'xenforo';
|
||||||
|
protected $table = 'user';
|
||||||
|
protected $primaryKey = 'user_id';
|
||||||
|
public $timestamps = false;
|
||||||
|
public $incrementing = true;
|
||||||
|
|
||||||
|
public function save(array $options = [])
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function delete()
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function update(array $attributes = [], array $options = [])
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
118
app/Models/News.php
Normal file
118
app/Models/News.php
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use App\Helpers\EntryHelpers;
|
||||||
|
use App\Traits\HasGallery;
|
||||||
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @property int $id
|
||||||
|
* @property string $title
|
||||||
|
* @property string $slug
|
||||||
|
* @property int|null $category_id
|
||||||
|
* @property string $description
|
||||||
|
* @property string $state
|
||||||
|
* @property string|null $staff_comment
|
||||||
|
* @property \Illuminate\Support\Carbon|null $rejected_at
|
||||||
|
* @property int|null $entry_id
|
||||||
|
* @property string|null $relevant_link
|
||||||
|
* @property string|null $youtube_link
|
||||||
|
* @property int $user_id
|
||||||
|
* @property int|null $comments_thread_id
|
||||||
|
* @property \Illuminate\Support\Carbon|null $deleted_at
|
||||||
|
* @property \Illuminate\Support\Carbon|null $created_at
|
||||||
|
* @property \Illuminate\Support\Carbon|null $updated_at
|
||||||
|
* @property-read \App\Models\Category|null $category
|
||||||
|
* @property-read \App\Models\Entry|null $entry
|
||||||
|
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\Gallery> $gallery
|
||||||
|
* @property-read int|null $gallery_count
|
||||||
|
* @method static Builder<static>|News inQueue(int $daysRejected = 7)
|
||||||
|
* @method static Builder<static>|News newModelQuery()
|
||||||
|
* @method static Builder<static>|News newQuery()
|
||||||
|
* @method static Builder<static>|News onlyTrashed()
|
||||||
|
* @method static Builder<static>|News published()
|
||||||
|
* @method static Builder<static>|News query()
|
||||||
|
* @method static Builder<static>|News whereCategoryId($value)
|
||||||
|
* @method static Builder<static>|News whereCommentsThreadId($value)
|
||||||
|
* @method static Builder<static>|News whereCreatedAt($value)
|
||||||
|
* @method static Builder<static>|News whereDeletedAt($value)
|
||||||
|
* @method static Builder<static>|News whereDescription($value)
|
||||||
|
* @method static Builder<static>|News whereEntryId($value)
|
||||||
|
* @method static Builder<static>|News whereId($value)
|
||||||
|
* @method static Builder<static>|News whereRejectedAt($value)
|
||||||
|
* @method static Builder<static>|News whereRelevantLink($value)
|
||||||
|
* @method static Builder<static>|News whereSlug($value)
|
||||||
|
* @method static Builder<static>|News whereStaffComment($value)
|
||||||
|
* @method static Builder<static>|News whereState($value)
|
||||||
|
* @method static Builder<static>|News whereTitle($value)
|
||||||
|
* @method static Builder<static>|News whereUpdatedAt($value)
|
||||||
|
* @method static Builder<static>|News whereUserId($value)
|
||||||
|
* @method static Builder<static>|News whereYoutubeLink($value)
|
||||||
|
* @method static Builder<static>|News withTrashed(bool $withTrashed = true)
|
||||||
|
* @method static Builder<static>|News withoutTrashed()
|
||||||
|
* @mixin \Eloquent
|
||||||
|
*/
|
||||||
|
class News extends Model
|
||||||
|
{
|
||||||
|
|
||||||
|
use SoftDeletes, HasGallery;
|
||||||
|
|
||||||
|
protected $table = 'news';
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'title',
|
||||||
|
'slug',
|
||||||
|
'description',
|
||||||
|
'state',
|
||||||
|
'category_id',
|
||||||
|
'entry_id',
|
||||||
|
'relevant_link',
|
||||||
|
'youtube_link',
|
||||||
|
'user_id',
|
||||||
|
'comments_thread_id',
|
||||||
|
'staff_comment',
|
||||||
|
'rejected_at',
|
||||||
|
'created_at'
|
||||||
|
];
|
||||||
|
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'rejected_at' => 'datetime',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function scopePublished(Builder $query): Builder
|
||||||
|
{
|
||||||
|
return $query->where('state', 'published' );
|
||||||
|
}
|
||||||
|
|
||||||
|
public function scopeInQueue( Builder $query, int $daysRejected = 7 ): Builder
|
||||||
|
{
|
||||||
|
return $query->withTrashed()->where(function($q) use($daysRejected) {
|
||||||
|
$q->where('state', 'pending')->whereNull('deleted_at');
|
||||||
|
})->orWhere(function($q) use($daysRejected) {
|
||||||
|
$q->where('state', 'rejected')->whereNotNull('rejected_at')->where('rejected_at', '>=', now()->subDays($daysRejected) );
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function entry(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Entry::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function category(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Category::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getYoutubeVideoId(): ?string {
|
||||||
|
if( !$this->youtube_link )
|
||||||
|
return null;
|
||||||
|
|
||||||
|
return EntryHelpers::getYoutubeVideoId( $this->youtube_link );
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -25,6 +25,8 @@ use Illuminate\Database\Eloquent\Relations\HasMany;
|
|||||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Platform whereShortName($value)
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|Platform whereShortName($value)
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Platform whereSlug($value)
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|Platform whereSlug($value)
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Platform whereUpdatedAt($value)
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|Platform whereUpdatedAt($value)
|
||||||
|
* @property string|null $play_online_core
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|Platform wherePlayOnlineCore($value)
|
||||||
* @mixin \Eloquent
|
* @mixin \Eloquent
|
||||||
*/
|
*/
|
||||||
class Platform extends Model
|
class Platform extends Model
|
||||||
|
|||||||
45
app/Models/PlayOnlineSetting.php
Normal file
45
app/Models/PlayOnlineSetting.php
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @property int $file_id
|
||||||
|
* @property string $core
|
||||||
|
* @property bool $threads
|
||||||
|
* @property \Illuminate\Support\Carbon|null $created_at
|
||||||
|
* @property \Illuminate\Support\Carbon|null $updated_at
|
||||||
|
* @property-read \App\Models\EntryFile $file
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|PlayOnlineSetting newModelQuery()
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|PlayOnlineSetting newQuery()
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|PlayOnlineSetting query()
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|PlayOnlineSetting whereCore($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|PlayOnlineSetting whereCreatedAt($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|PlayOnlineSetting whereFileId($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|PlayOnlineSetting whereThreads($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|PlayOnlineSetting whereUpdatedAt($value)
|
||||||
|
* @mixin \Eloquent
|
||||||
|
*/
|
||||||
|
class PlayOnlineSetting extends Model
|
||||||
|
{
|
||||||
|
protected $primaryKey = 'file_id';
|
||||||
|
public $incrementing = false;
|
||||||
|
protected $keyType = 'int';
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'file_id',
|
||||||
|
'core',
|
||||||
|
'threads'
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'threads' => 'boolean',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function file(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(EntryFile::class, 'file_id');
|
||||||
|
}
|
||||||
|
}
|
||||||
165
app/Policies/NewsPolicy.php
Normal file
165
app/Policies/NewsPolicy.php
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Policies;
|
||||||
|
|
||||||
|
use App\Models\Entry;
|
||||||
|
use App\Models\News;
|
||||||
|
use App\Auth\XenForoUser as User;
|
||||||
|
|
||||||
|
class NewsPolicy
|
||||||
|
{
|
||||||
|
public function viewAny(User $user): bool
|
||||||
|
{
|
||||||
|
if( $user->_can( 'romhackplaza', 'view' ) )
|
||||||
|
return true;
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function viewPending(User $user, News $news): bool
|
||||||
|
{
|
||||||
|
// Author.
|
||||||
|
if( $news->user_id === $user->user_id )
|
||||||
|
return true;
|
||||||
|
|
||||||
|
return $user->_can( 'romhackplaza', 'canModerateEntries' );
|
||||||
|
}
|
||||||
|
|
||||||
|
public function viewDraft(User $user, News $news): bool {
|
||||||
|
// Author.
|
||||||
|
if( $news->user_id === $user->user_id )
|
||||||
|
return true;
|
||||||
|
|
||||||
|
return $user->_can( 'romhackplaza', 'canSeeOthersDrafts' );
|
||||||
|
}
|
||||||
|
|
||||||
|
public function viewRejected(User $user, News $news): bool
|
||||||
|
{
|
||||||
|
// Author.
|
||||||
|
if( $news->user_id === $user->user_id )
|
||||||
|
return true;
|
||||||
|
|
||||||
|
return $user->_can( 'romhackplaza', 'canSeeRejectedEntries' );
|
||||||
|
}
|
||||||
|
|
||||||
|
public function viewHidden(User $user, News $news): bool
|
||||||
|
{
|
||||||
|
return $user->_can('romhackplaza', 'canSeeHiddenEntries' );
|
||||||
|
}
|
||||||
|
|
||||||
|
public function viewLocked(User $user, News $news): bool
|
||||||
|
{
|
||||||
|
// Author.
|
||||||
|
if( $news->user_id === $user->user_id )
|
||||||
|
return true;
|
||||||
|
|
||||||
|
return $user->_can('romhackplaza', 'canSeeLockedEntries' );
|
||||||
|
}
|
||||||
|
|
||||||
|
public function create(User $user, ?News $news = null ): bool
|
||||||
|
{
|
||||||
|
return $user->_can( 'romhackplaza', 'canSubmitEntry' );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine whether the user can update the model.
|
||||||
|
*/
|
||||||
|
public function update(User $user, News $news): bool
|
||||||
|
{
|
||||||
|
if( $news->state === 'published' ){
|
||||||
|
|
||||||
|
// Staff editors
|
||||||
|
if( $user->_can('romhackplaza', 'canEditOthersEntries') )
|
||||||
|
return true;
|
||||||
|
|
||||||
|
// Author.
|
||||||
|
if( $user->_can( 'romhackplaza', 'canEditMyEntries' ) && $news->user_id === $user->user_id )
|
||||||
|
return true;
|
||||||
|
|
||||||
|
return false;
|
||||||
|
|
||||||
|
} else if( $news->state === 'pending' ){
|
||||||
|
|
||||||
|
// Staff moderation.
|
||||||
|
if( $user->_can('romhackplaza', 'canEditOthersEntries') && $user->_can('romhackplaza', 'canModerateEntries') )
|
||||||
|
return true;
|
||||||
|
|
||||||
|
// Author.
|
||||||
|
if( $user->_can( 'romhackplaza', 'canEditMyEntries' ) && $news->user_id === $user->user_id )
|
||||||
|
return true;
|
||||||
|
|
||||||
|
} else if( $news->state === 'draft' ){
|
||||||
|
|
||||||
|
// Staff.
|
||||||
|
if( $user->_can('romhackplaza', 'canEditOthersEntries') && $user->_can( 'romhackplaza', 'canSeeOthersDrafts' ) )
|
||||||
|
return true;
|
||||||
|
|
||||||
|
// Author.
|
||||||
|
if( $user->_can( 'romhackplaza', 'canEditMyEntries' ) && $news->user_id === $user->user_id )
|
||||||
|
return true;
|
||||||
|
|
||||||
|
} else if( $news->state === 'rejected' ){
|
||||||
|
|
||||||
|
// Staff.
|
||||||
|
if( $user->_can('romhackplaza', 'canEditOthersEntries') && $user->_can( 'romhackplaza', 'canSeeRejectedEntries' ) )
|
||||||
|
return true;
|
||||||
|
|
||||||
|
// Author.
|
||||||
|
if( $user->_can( 'romhackplaza', 'canEditMyEntries' ) && $news->user_id === $user->user_id )
|
||||||
|
return true;
|
||||||
|
|
||||||
|
} else if( $news->state === 'locked' ){
|
||||||
|
|
||||||
|
// Staff.
|
||||||
|
if( $user->_can('romhackplaza', 'canEditOthersEntries') && $user->_can( 'romhackplaza', 'canSeeLockedEntries' ) )
|
||||||
|
return true;
|
||||||
|
|
||||||
|
return false;
|
||||||
|
|
||||||
|
} else if( $news->state === 'hidden' ){
|
||||||
|
|
||||||
|
// Staff.
|
||||||
|
if( $user->_can('romhackplaza', 'canEditOthersEntries') && $user->_can( 'romhackplaza', 'canSeeHiddenEntries' ) )
|
||||||
|
return true;
|
||||||
|
|
||||||
|
return false;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function skipQueue(User $user, ?News $news = null): bool
|
||||||
|
{
|
||||||
|
return $user->_can( 'romhackplaza', 'canSubmitEntryInPublished' );
|
||||||
|
}
|
||||||
|
|
||||||
|
public function updateComment(User $user, News $news): bool
|
||||||
|
{
|
||||||
|
return $user->_can('romhackplaza', 'canModerateEntries' );
|
||||||
|
}
|
||||||
|
|
||||||
|
public function manageButtonsInQueue(User $user, News $news): bool
|
||||||
|
{
|
||||||
|
if( $news->state === 'rejected' ){
|
||||||
|
return $this->viewRejected( $user, $news );
|
||||||
|
}
|
||||||
|
|
||||||
|
return $user->_can('romhackplaza', 'canModerateEntries' );
|
||||||
|
}
|
||||||
|
|
||||||
|
public function approve(User $user, News $news): bool
|
||||||
|
{
|
||||||
|
return $user->_can('romhackplaza', 'canModerateEntries' );
|
||||||
|
}
|
||||||
|
|
||||||
|
public function reject(User $user, News $news): bool
|
||||||
|
{
|
||||||
|
return $user->_can('romhackplaza', 'canModerateEntries' );
|
||||||
|
}
|
||||||
|
|
||||||
|
public function moderate(User $user, News $news): bool
|
||||||
|
{
|
||||||
|
return $user->_can('romhackplaza', 'canModerateEntries' );
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,8 +6,10 @@ use App\Auth\XenForoGuard;
|
|||||||
use App\Auth\XenForoUser;
|
use App\Auth\XenForoUser;
|
||||||
use App\Policies\TempFilePolicy;
|
use App\Policies\TempFilePolicy;
|
||||||
use App\Services\TemporaryFileService;
|
use App\Services\TemporaryFileService;
|
||||||
|
use App\Support\XenForoCauserResolver;
|
||||||
use Illuminate\Support\Facades\Gate;
|
use Illuminate\Support\Facades\Gate;
|
||||||
use Illuminate\Support\ServiceProvider;
|
use Illuminate\Support\ServiceProvider;
|
||||||
|
use Spatie\Activitylog\Support\CauserResolver;
|
||||||
|
|
||||||
class AppServiceProvider extends ServiceProvider
|
class AppServiceProvider extends ServiceProvider
|
||||||
{
|
{
|
||||||
@@ -16,7 +18,7 @@ class AppServiceProvider extends ServiceProvider
|
|||||||
*/
|
*/
|
||||||
public function register(): void
|
public function register(): void
|
||||||
{
|
{
|
||||||
//
|
$this->app->bind(CauserResolver::class, XenForoCauserResolver::class );
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
223
app/Services/ActivityService.php
Normal file
223
app/Services/ActivityService.php
Normal file
@@ -0,0 +1,223 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services;
|
||||||
|
|
||||||
|
use App\Models\Entry;
|
||||||
|
use App\Models\News;
|
||||||
|
use App\View\Components\EntryCard;
|
||||||
|
use Illuminate\Support\Carbon;
|
||||||
|
use Illuminate\Support\Collection;
|
||||||
|
use Illuminate\Support\Facades\Cache;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
|
class ActivityService
|
||||||
|
{
|
||||||
|
private const CACHE_ENTRIES = 300; // seconds.
|
||||||
|
private const CACHE_NEWS = 300; // seconds.
|
||||||
|
private const CACHE_MESSAGES = 300; // seconds.
|
||||||
|
private const CACHE_THREADS = 300; // seconds.
|
||||||
|
private const CACHE_CLUBS = 300; // seconds.
|
||||||
|
private const ITEMS_PER_TYPE = 15;
|
||||||
|
|
||||||
|
public function getActivities( array $activities = [ 'entries', 'news', 'messages', 'threads', 'clubs' ] ): Collection
|
||||||
|
{
|
||||||
|
$c = collect();
|
||||||
|
if( in_array( 'entries', $activities ) ) {
|
||||||
|
$c = $c->merge($this->getEntries());
|
||||||
|
}
|
||||||
|
if( in_array( 'news', $activities ) ) {
|
||||||
|
$c = $c->merge($this->getNews());
|
||||||
|
}
|
||||||
|
if( in_array( 'messages', $activities ) ) {
|
||||||
|
$c = $c->merge($this->getMessages());
|
||||||
|
}
|
||||||
|
if( in_array( 'threads', $activities ) ) {
|
||||||
|
$c = $c->merge($this->getThreads());
|
||||||
|
}
|
||||||
|
if( in_array( 'clubs', $activities ) ) {
|
||||||
|
$c = $c->merge($this->getClubs());
|
||||||
|
}
|
||||||
|
|
||||||
|
return $c->sortByDesc('date')
|
||||||
|
->values()
|
||||||
|
->take(30)
|
||||||
|
->map(function(array $item){
|
||||||
|
$obj = (object) $item;
|
||||||
|
$obj->date = Carbon::createFromTimestamp($obj->date);
|
||||||
|
return $obj;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private function formatEntry( Entry $entry ): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'type' => 'entry',
|
||||||
|
'title' => $entry->complete_title ?? $entry->title,
|
||||||
|
'url' => route('entries.show', ['section' => $entry->type, 'entry' => $entry]),
|
||||||
|
'image' => $entry->main_image ? \Storage::url($entry->main_image) : null,
|
||||||
|
'date' => $entry->created_at->timestamp,
|
||||||
|
'author' => $entry->authors->pluck('name')->implode(', '),
|
||||||
|
'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,
|
||||||
|
'meta' => $entry->getRealPlatform()?->name
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function formatNews( News $news ): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'type' => 'news',
|
||||||
|
'title' => $news->title,
|
||||||
|
'url' => route('news.show', ['news' => $news]),
|
||||||
|
'image' => $news->gallery()->first() ? \Storage::url($news->gallery()->first()->image) : null,
|
||||||
|
'date' => $news->created_at->timestamp,
|
||||||
|
'author' => null,
|
||||||
|
'user_id' => $news->user_id,
|
||||||
|
'badge' => 'News',
|
||||||
|
'badge_class' => 'news',
|
||||||
|
'excerpt' => $news->description ? \Str::limit(strip_tags($news->description), 80) : null,
|
||||||
|
'meta' => $news->category?->name
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function formatMessage( object $message ): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'type' => 'message',
|
||||||
|
'title' => $message->title,
|
||||||
|
'url' => xfRoute('threads/.' ) . $message->thread_id . '/post-' . $message->post_id,
|
||||||
|
'image' => null,
|
||||||
|
'date' => $message->post_date,
|
||||||
|
'author' => null,
|
||||||
|
'user_id' => $message->user_id,
|
||||||
|
'badge' => 'Post',
|
||||||
|
'badge_class' => 'message',
|
||||||
|
'excerpt' => $message->message ? \Str::limit(strip_tags($message->message), 80) : null,
|
||||||
|
'meta' => null
|
||||||
|
];
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private function formatThread( object $thread ): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'type' => 'thread',
|
||||||
|
'title' => $thread->title,
|
||||||
|
'url' => xfRoute('threads/.' ) . $thread->thread_id,
|
||||||
|
'image' => null,
|
||||||
|
'date' => $thread->post_date,
|
||||||
|
'author' => null,
|
||||||
|
'user_id' => $thread->user_id,
|
||||||
|
'badge' => 'Thread',
|
||||||
|
'badge_class' => 'thread',
|
||||||
|
'excerpt' => $thread->message ? \Str::limit(strip_tags($thread->message), 80) : null,
|
||||||
|
'meta' => null
|
||||||
|
];
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private function formatClub( object $club ): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'type' => 'club',
|
||||||
|
'title' => $club->title,
|
||||||
|
'url' => xfRoute('forums/.' ) . $club->node_id,
|
||||||
|
'image' => null, // TODO: Remplacer par banner_date
|
||||||
|
'date' => $club->club_creation_date,
|
||||||
|
'author' => null,
|
||||||
|
'user_id' => $club->user_id,
|
||||||
|
'badge' => 'Club',
|
||||||
|
'badge_class' => 'club',
|
||||||
|
'excerpt' => $club->description ? \Str::limit(strip_tags($club->description), 80) : null,
|
||||||
|
'meta' => null
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getEntries(): array
|
||||||
|
{
|
||||||
|
return Cache::remember('activity_entries', self::CACHE_ENTRIES, function() {
|
||||||
|
return Entry::published()
|
||||||
|
->with(['authors', 'game.platform'])
|
||||||
|
->latest('created_at')
|
||||||
|
->limit(self::ITEMS_PER_TYPE)
|
||||||
|
->get()
|
||||||
|
->map($this->formatEntry(...))
|
||||||
|
->toArray();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getNews(): array
|
||||||
|
{
|
||||||
|
return Cache::remember('activity_news', self::CACHE_NEWS, function() {
|
||||||
|
return News::published()
|
||||||
|
->with('gallery')
|
||||||
|
->latest('created_at')
|
||||||
|
->limit(self::ITEMS_PER_TYPE)
|
||||||
|
->get()
|
||||||
|
->map($this->formatNews(...))
|
||||||
|
->toArray();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getMessages(): array
|
||||||
|
{
|
||||||
|
return Cache::remember('activity_messages', self::CACHE_MESSAGES, function() {
|
||||||
|
return DB::connection('xenforo')
|
||||||
|
->table('post')
|
||||||
|
->join('user', 'post.user_id', '=', 'user.user_id')
|
||||||
|
->join('thread', 'post.thread_id', '=', 'thread.thread_id')
|
||||||
|
->where('post.message_state', 'visible')
|
||||||
|
->where('thread.first_post_id', '!=', 'post.post_id')
|
||||||
|
->orderByDesc('post.post_date')
|
||||||
|
->limit(self::ITEMS_PER_TYPE)
|
||||||
|
->select([
|
||||||
|
'thread.title', 'thread.thread_id', 'post.post_id', 'post.post_date',
|
||||||
|
'post.user_id', 'post.message'
|
||||||
|
])
|
||||||
|
->get()
|
||||||
|
->map($this->formatMessage(...))
|
||||||
|
->toArray();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getThreads(): array
|
||||||
|
{
|
||||||
|
return Cache::remember('activity_threads', self::CACHE_THREADS, function() {
|
||||||
|
return DB::connection('xenforo')
|
||||||
|
->table('thread')
|
||||||
|
->join('user', 'thread.user_id', '=', 'user.user_id')
|
||||||
|
->join('post', 'thread.first_post_id', '=', 'post.post_id')
|
||||||
|
->where('thread.discussion_state', 'visible')
|
||||||
|
->where('thread.discussion_type', '!=', 'redirect' )
|
||||||
|
->orderByDesc('thread.post_date')
|
||||||
|
->limit(self::ITEMS_PER_TYPE)
|
||||||
|
->select([
|
||||||
|
'thread.title', 'thread.thread_id', 'thread.post_date', 'thread.user_id',
|
||||||
|
'post.message'
|
||||||
|
])
|
||||||
|
->get()
|
||||||
|
->map($this->formatThread(...))
|
||||||
|
->toArray();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getClubs(): array
|
||||||
|
{
|
||||||
|
return Cache::remember('activity_clubs', self::CACHE_CLUBS, function() {
|
||||||
|
return DB::connection('xenforo')
|
||||||
|
->table('club')
|
||||||
|
->where('club_state', 'visible')
|
||||||
|
->orderByDesc('club_creation_date')
|
||||||
|
->limit(self::ITEMS_PER_TYPE)
|
||||||
|
->select([
|
||||||
|
'club.title', 'club.description', 'club.node_id',
|
||||||
|
'club.club_creation_date', 'club.user_id'
|
||||||
|
])
|
||||||
|
->get()
|
||||||
|
->map($this->formatClub(...))
|
||||||
|
->toArray();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
namespace App\Services;
|
namespace App\Services;
|
||||||
|
|
||||||
use App\Models\EntryFile;
|
use App\Models\EntryFile;
|
||||||
|
use App\Models\LogXfUser;
|
||||||
use Illuminate\Http\Client\ConnectionException;
|
use Illuminate\Http\Client\ConnectionException;
|
||||||
use Illuminate\Http\UploadedFile;
|
use Illuminate\Http\UploadedFile;
|
||||||
use Illuminate\Support\Facades\Http;
|
use Illuminate\Support\Facades\Http;
|
||||||
@@ -110,7 +111,6 @@ class FileServersService {
|
|||||||
'filename' => $filename,
|
'filename' => $filename,
|
||||||
'current_chunk' => $currentChunk,
|
'current_chunk' => $currentChunk,
|
||||||
'total_chunks' => $totalChunks,
|
'total_chunks' => $totalChunks,
|
||||||
// TODO : Must replace User ID
|
|
||||||
'zeus' => $this->generateZeusToken( \Auth::user()->user_id, $server['base_url'], "Uploadchunk" ),
|
'zeus' => $this->generateZeusToken( \Auth::user()->user_id, $server['base_url'], "Uploadchunk" ),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -129,4 +129,43 @@ class FileServersService {
|
|||||||
return $data;
|
return $data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function deleteFile(
|
||||||
|
string $filePath,
|
||||||
|
string $fileName,
|
||||||
|
int $userId
|
||||||
|
): bool {
|
||||||
|
|
||||||
|
foreach( $this->servers as $serverKey => $server ){
|
||||||
|
|
||||||
|
$response = Http::withHeaders([])
|
||||||
|
->post( $server['delete_file'], [
|
||||||
|
'filepath' => $filePath,
|
||||||
|
'filename' => $fileName,
|
||||||
|
'zeus' => $this->generateZeusToken( $userId, $server['base_url'], "Deletefile" ),
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (!$response->successful()) {
|
||||||
|
throw new \RuntimeException( $response->body() );
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = $response->json();
|
||||||
|
if( isset( $data['status'] ) && $data['status'] === 'deleted' ){
|
||||||
|
continue;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
activity('entry-file')
|
||||||
|
->causedBy(LogXfUser::find($userId))
|
||||||
|
->withProperties([
|
||||||
|
'filepath' => $filePath,
|
||||||
|
'filename' => $fileName,
|
||||||
|
])
|
||||||
|
->event('file_deletion')
|
||||||
|
->log('File deleted');
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
194
app/Services/NewsService.php
Normal file
194
app/Services/NewsService.php
Normal file
@@ -0,0 +1,194 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services;
|
||||||
|
|
||||||
|
use App\Helpers\EntryHelpers;
|
||||||
|
use App\Jobs\CreateXenForoCommentsThread;
|
||||||
|
use App\Models\Entry;
|
||||||
|
use App\Models\News;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
|
||||||
|
class NewsService
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Request for store/edit.
|
||||||
|
* @var Request|null
|
||||||
|
*/
|
||||||
|
private ?Request $request = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Entry for edit.
|
||||||
|
* @var News|null
|
||||||
|
*/
|
||||||
|
private ?News $news = null;
|
||||||
|
|
||||||
|
|
||||||
|
public function storeNews(Request $request): News
|
||||||
|
{
|
||||||
|
$this->request = $request;
|
||||||
|
$user_id = \Auth::user()->user_id;
|
||||||
|
|
||||||
|
$news = DB::transaction(function () use ($user_id) {
|
||||||
|
|
||||||
|
// STEP 1 : Slug
|
||||||
|
$slug = EntryHelpers::uniqueSlug( $this->request->input('title'), News::class );
|
||||||
|
|
||||||
|
$fields = [
|
||||||
|
'title' => $this->request->input('title'),
|
||||||
|
'slug' => $slug,
|
||||||
|
'category_id' => $this->request->input('category'),
|
||||||
|
'description' => $this->request->input('description'),
|
||||||
|
'state' => $this->request->input('submit-state'),
|
||||||
|
'entry_id' => $this->request->input('entry_id'),
|
||||||
|
'relevant_link' => $this->request->input('release_site'),
|
||||||
|
'youtube_link' => $this->request->input('youtube_link'),
|
||||||
|
'user_id' => $user_id,
|
||||||
|
];
|
||||||
|
|
||||||
|
$news = News::create( $fields );
|
||||||
|
|
||||||
|
// STEP 3 : Prepare Gallery images.
|
||||||
|
$this->Step3a_PrepareGalleryImages( $news );
|
||||||
|
|
||||||
|
return $news;
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
$this->Step3b_SaveGalleryImages( $news );
|
||||||
|
|
||||||
|
$this->Step4_CreateCommentsThread( $news );
|
||||||
|
|
||||||
|
return $news;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function Step3a_PrepareGalleryImages(News $news): void
|
||||||
|
{
|
||||||
|
foreach( $this->request->input('gallery', [] ) ?? [] as $i => $imagePath ){
|
||||||
|
$news->gallery()->create([
|
||||||
|
'image' => $imagePath,
|
||||||
|
'order' => $i
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function Step3b_SaveGalleryImages(News $news): void
|
||||||
|
{
|
||||||
|
foreach ( $news->gallery ?? [] as $galleryItem ) {
|
||||||
|
$newPath = 'news/gallery-images/' . $news->id . '/' . basename($galleryItem->image);
|
||||||
|
|
||||||
|
if( !Storage::disk('public')->move($galleryItem->image, $newPath) )
|
||||||
|
continue;
|
||||||
|
|
||||||
|
$galleryItem->update(['image' => $newPath]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function Step4_CreateCommentsThread( News $news ): void
|
||||||
|
{
|
||||||
|
if( $news->state !== 'published' )
|
||||||
|
return;
|
||||||
|
|
||||||
|
if( !$news->comments_thread_id )
|
||||||
|
CreateXenForoCommentsThread::dispatch( $news );
|
||||||
|
}
|
||||||
|
|
||||||
|
public function editNews(Request $request, News $news): News
|
||||||
|
{
|
||||||
|
$this->request = $request;
|
||||||
|
$this->news = $news;
|
||||||
|
|
||||||
|
if( \Auth::user()->can('moderate', $news) ){
|
||||||
|
$user_id = $this->request->input('owner_user_id');
|
||||||
|
} else {
|
||||||
|
$user_id = \Auth::user()->user_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
$galleryPaths = [];
|
||||||
|
|
||||||
|
$news = DB::transaction(function () use ($user_id, &$galleryPaths) {
|
||||||
|
|
||||||
|
// STEP 1 : Refresh slug.
|
||||||
|
if( $this->request->input('title') !== $this->news->title ){
|
||||||
|
$this->news->slug = EntryHelpers::uniqueSlug( $this->request->input('title'), News::class, $this->news->id );
|
||||||
|
}
|
||||||
|
|
||||||
|
$fields = [
|
||||||
|
'title' => $this->request->input('title'),
|
||||||
|
'slug' => $this->news->slug,
|
||||||
|
'category_id' => $this->request->input('category'),
|
||||||
|
'description' => $this->request->input('description'),
|
||||||
|
'state' => $this->request->input('submit-state'),
|
||||||
|
'entry_id' => $this->request->input('entry_id'),
|
||||||
|
'relevant_link' => $this->request->input('release_site'),
|
||||||
|
'youtube_link' => $this->request->input('youtube_link'),
|
||||||
|
'user_id' => $user_id,
|
||||||
|
];
|
||||||
|
|
||||||
|
if( \Auth::user()->can('moderate', $this->news) ){
|
||||||
|
$fields['staff_comment'] = $this->request->input('staff_comment');
|
||||||
|
$fields['comments_thread_id'] = $this->request->input('comments_thread_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->news->update( $fields );
|
||||||
|
|
||||||
|
$galleryPaths = $this->eStep3a_UpdateGalleryImages();
|
||||||
|
|
||||||
|
return $this->news;
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
$this->eStep3b_UpdateGalleryImages( $galleryPaths );
|
||||||
|
|
||||||
|
$this->step4_CreateCommentsThread( $news );
|
||||||
|
|
||||||
|
return $news;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private function eStep3a_UpdateGalleryImages(): array
|
||||||
|
{
|
||||||
|
$requestGallery = $this->request->input('gallery', [] ) ?? [];
|
||||||
|
$existingGalleryPaths = $this->news->gallery->pluck('image')->toArray();
|
||||||
|
|
||||||
|
$needDeletion = array_diff( $existingGalleryPaths, $requestGallery );
|
||||||
|
|
||||||
|
if( !empty( $needDeletion ) ){
|
||||||
|
$this->news->gallery()->whereIn('image', $needDeletion )->delete();
|
||||||
|
}
|
||||||
|
|
||||||
|
$needAddition = array_diff( $requestGallery, $existingGalleryPaths );
|
||||||
|
$images = [];
|
||||||
|
foreach( $needAddition as $imagePath ){
|
||||||
|
$images[] = $this->news->gallery()->create([
|
||||||
|
'image' => $imagePath,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach( $requestGallery as $i => $imagePath ){
|
||||||
|
$this->news->gallery()->where('image', $imagePath )->update(['order' => $i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [ 'addition' => $images, 'deletion' => $needDeletion ];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function eStep3b_UpdateGalleryImages( array $pathsChanges ): void
|
||||||
|
{
|
||||||
|
foreach ( $pathsChanges['deletion'] as $deletePath ){
|
||||||
|
if( Storage::disk('public')->exists($deletePath) )
|
||||||
|
Storage::disk('public')->delete($deletePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ( $pathsChanges['addition'] as $galleryItem ){
|
||||||
|
$newPath = 'news/gallery-images/' . $this->news->id . '/' . basename( $galleryItem->image );
|
||||||
|
|
||||||
|
if( !Storage::disk('public')->move( $galleryItem->image, $newPath ) ){
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$galleryItem->update(['image' => $newPath]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -4,9 +4,11 @@ namespace App\Services;
|
|||||||
|
|
||||||
use App\Exceptions\SubmissionException;
|
use App\Exceptions\SubmissionException;
|
||||||
use App\Helpers\EntryHelpers;
|
use App\Helpers\EntryHelpers;
|
||||||
|
use App\Helpers\PlayOnlineHelpers;
|
||||||
use App\Helpers\XenForoHelpers;
|
use App\Helpers\XenForoHelpers;
|
||||||
use App\Http\Requests\StoreEntryRequest;
|
use App\Http\Requests\StoreEntryRequest;
|
||||||
use App\Jobs\CreateXenForoCommentsThread;
|
use App\Jobs\CreateXenForoCommentsThread;
|
||||||
|
use App\Jobs\DeleteFile;
|
||||||
use App\Models\Author;
|
use App\Models\Author;
|
||||||
use App\Models\Category;
|
use App\Models\Category;
|
||||||
use App\Models\Entry;
|
use App\Models\Entry;
|
||||||
@@ -76,8 +78,12 @@ class SubmissionsService {
|
|||||||
'error' => null,
|
'error' => null,
|
||||||
'uuid' => $uuid,
|
'uuid' => $uuid,
|
||||||
'state' => $file->state,
|
'state' => $file->state,
|
||||||
|
'can_be_online_patched' => EntryHelpers::enableOnlinePatcherBasedOnExtension($file['filename']),
|
||||||
'meta_online_patcher' => $file->online_patcher,
|
'meta_online_patcher' => $file->online_patcher,
|
||||||
'meta_secondary_online_patcher' => $file->secondary_online_patcher,
|
'meta_secondary_online_patcher' => $file->secondary_online_patcher,
|
||||||
|
'meta_play_online' => $file->playOnlineSetting()->exists() ? true : false,
|
||||||
|
'meta_play_online_core' => $file->playOnlineSetting()->exists() ? $file->playOnlineSetting->core : '',
|
||||||
|
'meta_play_online_threads' => $file->playOnlineSetting()->exists() ? $file->playOnlineSetting->threads : false,
|
||||||
];
|
];
|
||||||
|
|
||||||
$file = Cache::get("uploaded_file_{$uuid}");
|
$file = Cache::get("uploaded_file_{$uuid}");
|
||||||
@@ -92,8 +98,12 @@ class SubmissionsService {
|
|||||||
'error' => null,
|
'error' => null,
|
||||||
'uuid' => $uuid,
|
'uuid' => $uuid,
|
||||||
'state' => $file['state'],
|
'state' => $file['state'],
|
||||||
|
'can_be_online_patched' => EntryHelpers::enableOnlinePatcherBasedOnExtension($file['filename']),
|
||||||
'meta_online_patcher' => false,
|
'meta_online_patcher' => false,
|
||||||
'meta_secondary_online_patcher' => false,
|
'meta_secondary_online_patcher' => false,
|
||||||
|
'meta_play_online' => false,
|
||||||
|
'meta_play_online_core' => null,
|
||||||
|
'meta_play_online_threads' => false
|
||||||
];
|
];
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
@@ -298,15 +308,20 @@ class SubmissionsService {
|
|||||||
foreach ( $uuidData ?? [] as $uuid ) {
|
foreach ( $uuidData ?? [] as $uuid ) {
|
||||||
$fileData = Cache::pull("uploaded_file_{$uuid}");
|
$fileData = Cache::pull("uploaded_file_{$uuid}");
|
||||||
if( !$fileData )
|
if( !$fileData )
|
||||||
throw new SubmissionException( "File {$uuid} has expired. Please delete all your files and retry. If it's an edition, delete all your new files and retry." );
|
throw new SubmissionException( "File {$uuid} has expired. Please delete all your files and retry. If it's an edition, delete all the new files and retry." );
|
||||||
|
|
||||||
$onlinePatcher = (bool)($metadataArray[$uuid]['online_patcher'] ?? false);
|
if( section_must_be( [ 'romhacks', 'translations' ], $entry->type ) ) {
|
||||||
if( !$onlinePatcher )
|
$onlinePatcher = (bool)($metadataArray[$uuid]['online_patcher'] ?? false);
|
||||||
$onlinePatcher = EntryHelpers::enableOnlinePatcherBasedOnExtension( $fileData['filename'] );
|
if (!$onlinePatcher)
|
||||||
|
$onlinePatcher = EntryHelpers::enableOnlinePatcherBasedOnExtension($fileData['filename']);
|
||||||
|
|
||||||
$secondaryOnlinePatcher = (bool)($metadataArray[$uuid]['secondary_online_patcher'] ?? false);
|
$secondaryOnlinePatcher = (bool)($metadataArray[$uuid]['secondary_online_patcher'] ?? false);
|
||||||
|
} else {
|
||||||
|
$onlinePatcher = false;
|
||||||
|
$secondaryOnlinePatcher = false;
|
||||||
|
}
|
||||||
|
|
||||||
EntryFile::create([
|
$file = EntryFile::create([
|
||||||
'entry_id' => $entry->id,
|
'entry_id' => $entry->id,
|
||||||
'file_uuid' => $uuid,
|
'file_uuid' => $uuid,
|
||||||
'filename' => $fileData['filename'],
|
'filename' => $fileData['filename'],
|
||||||
@@ -319,6 +334,26 @@ class SubmissionsService {
|
|||||||
'secondary_online_patcher' => $secondaryOnlinePatcher,
|
'secondary_online_patcher' => $secondaryOnlinePatcher,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
if( section_must_be( ['romhacks', 'translations', 'homebrew'], $entry->type ) ) {
|
||||||
|
$playOnline = (bool)($metadataArray[$uuid]['play_online'] ?? false);
|
||||||
|
$playOnlineCore = $metadataArray[$uuid]['play_online_core'] ?? null;
|
||||||
|
$playOnlineThreads = (bool)($metadataArray[$uuid]['play_online_threads'] ?? false);
|
||||||
|
|
||||||
|
if (!$playOnline && $entry->getRealPlatform()?->play_online_core !== null) {
|
||||||
|
$playOnline = true;
|
||||||
|
$playOnlineCore = $entry->getRealPlatform()?->play_online_core;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($playOnline) {
|
||||||
|
$file->playOnlineSetting()->updateOrCreate(
|
||||||
|
['file_id' => $file->id],
|
||||||
|
[
|
||||||
|
'core' => $playOnlineCore,
|
||||||
|
'threads' => $playOnlineThreads,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -452,9 +487,10 @@ class SubmissionsService {
|
|||||||
|
|
||||||
private function Step12a_PrepareGalleryImages( Entry $entry ): void
|
private function Step12a_PrepareGalleryImages( Entry $entry ): void
|
||||||
{
|
{
|
||||||
foreach ( $this->request->input('gallery', [] ) ?? [] as $imagePath ) {
|
foreach ( $this->request->input('gallery', [] ) ?? [] as $i => $imagePath ) {
|
||||||
$entry->gallery()->create([
|
$entry->gallery()->create([
|
||||||
'image' => $imagePath,
|
'image' => $imagePath,
|
||||||
|
'order' => $i
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -556,6 +592,10 @@ class SubmissionsService {
|
|||||||
if( \Auth::user()->can('moderate', $this->entry) ){
|
if( \Auth::user()->can('moderate', $this->entry) ){
|
||||||
$fields['staff_comment'] = $this->request->input('staff_comment');
|
$fields['staff_comment'] = $this->request->input('staff_comment');
|
||||||
$fields['featured'] = $this->request->input('featured') ?? false;
|
$fields['featured'] = $this->request->input('featured') ?? false;
|
||||||
|
if( $fields['featured'] == true && $this->entry->featured_at === null )
|
||||||
|
$fields['featured_at'] = now();
|
||||||
|
if( $fields['featured'] == false )
|
||||||
|
$fields['featured_at'] = null;
|
||||||
$fields['comments_thread_id'] = $this->request->input('comments_thread_id');
|
$fields['comments_thread_id'] = $this->request->input('comments_thread_id');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -666,6 +706,10 @@ class SubmissionsService {
|
|||||||
|
|
||||||
$needDeletion = array_diff( $existingUuids, $requestUuids );
|
$needDeletion = array_diff( $existingUuids, $requestUuids );
|
||||||
if( !empty( $needDeletion ) ){
|
if( !empty( $needDeletion ) ){
|
||||||
|
$userId = \Auth::user()->user_id;
|
||||||
|
EntryFile::where('entry_id', $entryId)->whereIn('file_uuid', $needDeletion)->get()->each( function ( $f ) use ( $userId ) {
|
||||||
|
DeleteFile::dispatch( $f->filepath, $f->filename, $userId);
|
||||||
|
});
|
||||||
EntryFile::where('entry_id', $entryId)->whereIn('file_uuid', $needDeletion)->delete();
|
EntryFile::where('entry_id', $entryId)->whereIn('file_uuid', $needDeletion)->delete();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -680,15 +724,45 @@ class SubmissionsService {
|
|||||||
|
|
||||||
foreach( $stateMap as $uuid => $state ){
|
foreach( $stateMap as $uuid => $state ){
|
||||||
|
|
||||||
$onlinePatcher = (bool)($metadataArray[$uuid]['online_patcher'] ?? false);
|
if( section_must_be( ['romhacks', 'translations'], $this->entry->type ) ) {
|
||||||
$secondaryOnlinePatcher = (bool)($metadataArray[$uuid]['secondary_online_patcher'] ?? false);
|
$onlinePatcher = (bool)($metadataArray[$uuid]['online_patcher'] ?? false);
|
||||||
|
$secondaryOnlinePatcher = (bool)($metadataArray[$uuid]['secondary_online_patcher'] ?? false);
|
||||||
|
} else {
|
||||||
|
$onlinePatcher = false;
|
||||||
|
$secondaryOnlinePatcher = false;
|
||||||
|
}
|
||||||
|
|
||||||
EntryFile::where('file_uuid', $uuid)->where('entry_id', $entryId)->where('state', '!=', 'archived')
|
$entryFile = EntryFile::where('file_uuid', $uuid)->where('entry_id', $entryId)->where('state', '!=', 'archived')->first();
|
||||||
->update([
|
if( !$entryFile )
|
||||||
'state' => $state,
|
continue;
|
||||||
'online_patcher' => $onlinePatcher,
|
|
||||||
'secondary_online_patcher' => $secondaryOnlinePatcher,
|
$entryFile->update([
|
||||||
]);
|
'state' => $state,
|
||||||
|
'online_patcher' => $onlinePatcher,
|
||||||
|
'secondary_online_patcher' => $secondaryOnlinePatcher,
|
||||||
|
]);
|
||||||
|
|
||||||
|
if( section_must_be( ['romhacks', 'translations', 'homebrew'], $this->entry->type ) ) {
|
||||||
|
|
||||||
|
$playOnline = (bool)($metadataArray[$uuid]['play_online'] ?? false);
|
||||||
|
$playOnlineCore = $metadataArray[$uuid]['play_online_core'] ?? null;
|
||||||
|
$playOnlineThreads = (bool)($metadataArray[$uuid]['play_online_threads'] ?? false);
|
||||||
|
|
||||||
|
if ($playOnline) {
|
||||||
|
if ($playOnlineCore === null || !in_array($playOnlineCore, PlayOnlineHelpers::getCoreLists()))
|
||||||
|
$playOnlineCore = $this->entry->getRealPlatform()->play_online_core ? $this->entry->getRealPlatform()->play_online_core : 'nes';
|
||||||
|
|
||||||
|
$entryFile->playOnlineSetting()->updateOrCreate(
|
||||||
|
['file_id' => $entryFile->id],
|
||||||
|
[
|
||||||
|
'core' => $playOnlineCore,
|
||||||
|
'threads' => $playOnlineThreads,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
$entryFile->playOnlineSetting()->delete();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -860,6 +934,10 @@ class SubmissionsService {
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
foreach ( $requestGallery as $i => $imagePath ){
|
||||||
|
$this->entry->gallery()->where('image', $imagePath )->update(['order' => $i]);
|
||||||
|
}
|
||||||
|
|
||||||
return [ 'addition' => $images, 'deletion' => $needDeletion ];
|
return [ 'addition' => $images, 'deletion' => $needDeletion ];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ class XenforoApiService {
|
|||||||
return $response['success'] ?? false;
|
return $response['success'] ?? false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function createCommentsThread( Entry $entry ): bool
|
public function createCommentsThread( Entry|News $entry ): bool
|
||||||
{
|
{
|
||||||
if( !$entry->comments_thread_id || $entry->comments_thread_id <= 0 ){
|
if( !$entry->comments_thread_id || $entry->comments_thread_id <= 0 ){
|
||||||
$data = [
|
$data = [
|
||||||
@@ -134,6 +134,14 @@ class XenforoApiService {
|
|||||||
return $response['success'] ?? false;
|
return $response['success'] ?? false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function featuredRequest( Entry $entry ): bool
|
||||||
|
{
|
||||||
|
$response = $this->post("romhackplaza_entry/featured", data: [
|
||||||
|
'entry_id' => $entry->id, 'user_id' => $entry->user_id, 'entry_title' => $entry->complete_title ?? $entry->title,
|
||||||
|
]);
|
||||||
|
return $response['success'] ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
public function deleteThreadWithEntry(int $threadId): bool
|
public function deleteThreadWithEntry(int $threadId): bool
|
||||||
{
|
{
|
||||||
return (bool) $this->delete( "threads/{$threadId}", data: ['reason' => "Deletion with entry." ] );
|
return (bool) $this->delete( "threads/{$threadId}", data: ['reason' => "Deletion with entry." ] );
|
||||||
|
|||||||
@@ -195,9 +195,9 @@ class XenforoService {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private function hashCSRFToken( string $token ): string
|
private function hashCSRFToken( string $token, int $timestamp ): string
|
||||||
{
|
{
|
||||||
return hash_hmac('md5', $token . time(), config('app.xf_salt') );
|
return hash_hmac('md5', $token . $timestamp, config('app.xf_salt') );
|
||||||
}
|
}
|
||||||
public function getCSRFToken(): string
|
public function getCSRFToken(): string
|
||||||
{
|
{
|
||||||
@@ -207,6 +207,28 @@ class XenforoService {
|
|||||||
Cookie::queue('xf_csrf', $token, 0, '/', config('session.domain'), 0, false, false );
|
Cookie::queue('xf_csrf', $token, 0, '/', config('session.domain'), 0, false, false );
|
||||||
}
|
}
|
||||||
|
|
||||||
return time() . ',' . $this->hashCSRFToken($token);
|
$timestamp = time();
|
||||||
|
return $timestamp . ',' . $this->hashCSRFToken($token, $timestamp);
|
||||||
|
}
|
||||||
|
public function verifyCSRFToken( string $requestToken ): bool
|
||||||
|
{
|
||||||
|
$token = Cookie::get('xf_csrf');
|
||||||
|
if( !$token ){
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
[$timestamp, $hash] = explode(',', $requestToken);
|
||||||
|
} catch (\Throwable $th) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$timestamp = intval($timestamp);
|
||||||
|
$currentTimestamp = time();
|
||||||
|
|
||||||
|
if( abs( $currentTimestamp - $timestamp ) > 3600 )
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return $hash === $this->hashCSRFToken($token, $timestamp);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
22
app/Support/XenForoCauserResolver.php
Normal file
22
app/Support/XenForoCauserResolver.php
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Support;
|
||||||
|
|
||||||
|
use App\Auth\XenForoUser;
|
||||||
|
use App\Models\LogXfUser;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Spatie\Activitylog\Support\CauserResolver;
|
||||||
|
|
||||||
|
class XenForoCauserResolver extends CauserResolver
|
||||||
|
{
|
||||||
|
public function getDefaultCauser(): ?Model
|
||||||
|
{
|
||||||
|
$user = \Auth::user();
|
||||||
|
|
||||||
|
if( $user instanceof XenForoUser && $user->getAuthIdentifier() ){
|
||||||
|
return LogXfUser::find($user->getAuthIdentifier());
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,6 +9,6 @@ trait HasGallery
|
|||||||
{
|
{
|
||||||
public function gallery(): MorphMany
|
public function gallery(): MorphMany
|
||||||
{
|
{
|
||||||
return $this->morphMany(Gallery::class, 'galleryable')->orderBy('id');
|
return $this->morphMany(Gallery::class, 'galleryable')->orderBy('order')->orderBy('id');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,8 @@ class CategorySelector extends Component
|
|||||||
public function __construct(
|
public function __construct(
|
||||||
public string $section,
|
public string $section,
|
||||||
public array $selected = [],
|
public array $selected = [],
|
||||||
public bool $required = true
|
public bool $required = true,
|
||||||
|
public bool $news = false
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
$this->categories = Category::query()
|
$this->categories = Category::query()
|
||||||
@@ -36,6 +37,6 @@ class CategorySelector extends Component
|
|||||||
*/
|
*/
|
||||||
public function render(): View|Closure|string
|
public function render(): View|Closure|string
|
||||||
{
|
{
|
||||||
return view('components.category-selector');
|
return $this->news === true ? view('components.news-category-selector' ) : view('components.category-selector');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
31
app/View/Components/NewsCard.php
Normal file
31
app/View/Components/NewsCard.php
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\View\Components;
|
||||||
|
|
||||||
|
use App\Models\Entry;
|
||||||
|
use App\Models\News;
|
||||||
|
use Closure;
|
||||||
|
use Illuminate\Contracts\View\View;
|
||||||
|
use Illuminate\View\Component;
|
||||||
|
|
||||||
|
class NewsCard extends Component
|
||||||
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new component instance.
|
||||||
|
*/
|
||||||
|
public function __construct(
|
||||||
|
public News $news
|
||||||
|
)
|
||||||
|
{
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the view / contents that represent the component.
|
||||||
|
*/
|
||||||
|
public function render(): View|Closure|string
|
||||||
|
{
|
||||||
|
return view('components.news-card');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
namespace App\View\Components;
|
namespace App\View\Components;
|
||||||
|
|
||||||
use App\Models\Entry;
|
use App\Models\Entry;
|
||||||
|
use App\Models\News;
|
||||||
use Closure;
|
use Closure;
|
||||||
use Illuminate\Contracts\View\View;
|
use Illuminate\Contracts\View\View;
|
||||||
use Illuminate\View\Component;
|
use Illuminate\View\Component;
|
||||||
@@ -16,11 +17,16 @@ class SubmitEntryStatus extends Component
|
|||||||
public string $section,
|
public string $section,
|
||||||
public bool $isEdit,
|
public bool $isEdit,
|
||||||
public ?string $currentState,
|
public ?string $currentState,
|
||||||
public null|string|Entry $entry = 'App\Models\Entry',
|
public null|string|Entry|News $entry = null,
|
||||||
|
public bool $news = false
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
if( $this->entry === null )
|
if( $this->entry === null ){
|
||||||
$this->entry = 'App\Models\Entry';
|
if( $news )
|
||||||
|
$this->entry = 'App\Models\News';
|
||||||
|
else
|
||||||
|
$this->entry = 'App\Models\Entry';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function availableStates(): array
|
public function availableStates(): array
|
||||||
|
|||||||
@@ -25,6 +25,15 @@ if( !function_exists( 'section_must_not_be' ) ){
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if( !function_exists('userTheme' ) ){
|
||||||
|
function userTheme(): string {
|
||||||
|
if( !\Auth::guest() ){
|
||||||
|
return \Auth::user()->style_variation ?? 'default';
|
||||||
|
}
|
||||||
|
return \Illuminate\Support\Facades\Cookie::get('xf_style_variation', 'default');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if( !function_exists( 'databaseRoute' ) ){
|
if( !function_exists( 'databaseRoute' ) ){
|
||||||
|
|
||||||
function databaseRoute( array $params = [] ): string
|
function databaseRoute( array $params = [] ): string
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ return Application::configure(basePath: dirname(__DIR__))
|
|||||||
health: '/up',
|
health: '/up',
|
||||||
)
|
)
|
||||||
->withMiddleware(function (Middleware $middleware): void {
|
->withMiddleware(function (Middleware $middleware): void {
|
||||||
$middleware->encryptCookies(except: ['xf_session','xf_user','xf_csrf','theme','entries_per_page']);
|
$middleware->encryptCookies(except: ['xf_session','xf_user','xf_csrf','xf_style_variation','activity_filters']);
|
||||||
$middleware->alias([
|
$middleware->alias([
|
||||||
'xf.auth' => \App\Http\Middleware\CheckXenForoPermissions::class,
|
'xf.auth' => \App\Http\Middleware\CheckXenForoPermissions::class,
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -7,14 +7,16 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"require": {
|
"require": {
|
||||||
"php": "^8.4",
|
"php": "^8.4",
|
||||||
|
"ext-pdo": "*",
|
||||||
|
"ext-simplexml": "*",
|
||||||
|
"ext-xmlreader": "*",
|
||||||
"diglactic/laravel-breadcrumbs": "^10.1",
|
"diglactic/laravel-breadcrumbs": "^10.1",
|
||||||
"filament/filament": "^5.6",
|
"filament/filament": "^5.6",
|
||||||
"laravel/framework": "^13.7",
|
"laravel/framework": "^13.7",
|
||||||
"laravel/tinker": "^3.0",
|
"laravel/tinker": "^3.0",
|
||||||
"livewire/livewire": "^4.3",
|
"livewire/livewire": "^4.3",
|
||||||
"predis/predis": "^3.4",
|
"predis/predis": "^3.4",
|
||||||
"ext-xmlreader": "*",
|
"spatie/laravel-activitylog": "^5.0"
|
||||||
"ext-simplexml": "*"
|
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"barryvdh/laravel-debugbar": "^4.2",
|
"barryvdh/laravel-debugbar": "^4.2",
|
||||||
|
|||||||
100
composer.lock
generated
100
composer.lock
generated
@@ -4,7 +4,7 @@
|
|||||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||||
"This file is @generated automatically"
|
"This file is @generated automatically"
|
||||||
],
|
],
|
||||||
"content-hash": "79ce85f4c121f30d964f9322cb7120a4",
|
"content-hash": "d561062afd8c291a93e8fd1e00f4e901",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
"name": "blade-ui-kit/blade-heroicons",
|
"name": "blade-ui-kit/blade-heroicons",
|
||||||
@@ -5273,6 +5273,99 @@
|
|||||||
],
|
],
|
||||||
"time": "2024-05-17T09:06:10+00:00"
|
"time": "2024-05-17T09:06:10+00:00"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "spatie/laravel-activitylog",
|
||||||
|
"version": "5.0.0",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/spatie/laravel-activitylog.git",
|
||||||
|
"reference": "0e00fe74fd071cc572a045459f6d4c9de33130bd"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/spatie/laravel-activitylog/zipball/0e00fe74fd071cc572a045459f6d4c9de33130bd",
|
||||||
|
"reference": "0e00fe74fd071cc572a045459f6d4c9de33130bd",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"illuminate/config": "^12.0 || ^13.0",
|
||||||
|
"illuminate/database": "^12.0 || ^13.0",
|
||||||
|
"illuminate/support": "^12.0 || ^13.0",
|
||||||
|
"php": "^8.4",
|
||||||
|
"spatie/laravel-package-tools": "^1.6.3"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"ext-json": "*",
|
||||||
|
"larastan/larastan": "^3.0",
|
||||||
|
"laravel/pint": "^1.29",
|
||||||
|
"orchestra/testbench": "^10.0 || ^11.0",
|
||||||
|
"pestphp/pest": "^4.0"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"extra": {
|
||||||
|
"laravel": {
|
||||||
|
"providers": [
|
||||||
|
"Spatie\\Activitylog\\ActivitylogServiceProvider"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"files": [
|
||||||
|
"src/helpers.php"
|
||||||
|
],
|
||||||
|
"psr-4": {
|
||||||
|
"Spatie\\Activitylog\\": "src"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Freek Van der Herten",
|
||||||
|
"email": "freek@spatie.be",
|
||||||
|
"homepage": "https://spatie.be",
|
||||||
|
"role": "Developer"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Sebastian De Deyne",
|
||||||
|
"email": "sebastian@spatie.be",
|
||||||
|
"homepage": "https://spatie.be",
|
||||||
|
"role": "Developer"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Tom Witkowski",
|
||||||
|
"email": "dev.gummibeer@gmail.com",
|
||||||
|
"homepage": "https://gummibeer.de",
|
||||||
|
"role": "Developer"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "A very simple activity logger to monitor the users of your website or application",
|
||||||
|
"homepage": "https://github.com/spatie/activitylog",
|
||||||
|
"keywords": [
|
||||||
|
"activity",
|
||||||
|
"laravel",
|
||||||
|
"log",
|
||||||
|
"spatie",
|
||||||
|
"user"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/spatie/laravel-activitylog/issues",
|
||||||
|
"source": "https://github.com/spatie/laravel-activitylog/tree/5.0.0"
|
||||||
|
},
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"url": "https://spatie.be/open-source/support-us",
|
||||||
|
"type": "custom"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://github.com/spatie",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"time": "2026-03-25T10:04:54+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "spatie/laravel-package-tools",
|
"name": "spatie/laravel-package-tools",
|
||||||
"version": "1.93.0",
|
"version": "1.93.0",
|
||||||
@@ -11241,8 +11334,9 @@
|
|||||||
"prefer-lowest": false,
|
"prefer-lowest": false,
|
||||||
"platform": {
|
"platform": {
|
||||||
"php": "^8.4",
|
"php": "^8.4",
|
||||||
"ext-xmlreader": "*",
|
"ext-pdo": "*",
|
||||||
"ext-simplexml": "*"
|
"ext-simplexml": "*",
|
||||||
|
"ext-xmlreader": "*"
|
||||||
},
|
},
|
||||||
"platform-dev": {},
|
"platform-dev": {},
|
||||||
"plugin-api-version": "2.9.0"
|
"plugin-api-version": "2.9.0"
|
||||||
|
|||||||
@@ -129,6 +129,13 @@ return [
|
|||||||
'database' => storage_path('hashes.sqlite'),
|
'database' => storage_path('hashes.sqlite'),
|
||||||
'prefix' => '',
|
'prefix' => '',
|
||||||
'foreign_key_constraints' => env('DB_FOREIGN_KEYS', true),
|
'foreign_key_constraints' => env('DB_FOREIGN_KEYS', true),
|
||||||
|
],
|
||||||
|
|
||||||
|
'discord' => [
|
||||||
|
'driver' => 'sqlite',
|
||||||
|
'database' => env('DISCORD_DB_PATH'),
|
||||||
|
'prefix' => '',
|
||||||
|
'foreign_key_constraints' => env('DB_FOREIGN_KEYS', true),
|
||||||
]
|
]
|
||||||
|
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -18,16 +18,21 @@ return [
|
|||||||
'icon' => 'database',
|
'icon' => 'database',
|
||||||
'route' => 'entries.index'
|
'route' => 'entries.index'
|
||||||
],
|
],
|
||||||
|
[
|
||||||
|
'name' => 'News',
|
||||||
|
'icon' => 'newspaper',
|
||||||
|
'route' => 'news.index'
|
||||||
|
],
|
||||||
[
|
[
|
||||||
'name' => "Submissions queue",
|
'name' => "Submissions queue",
|
||||||
'icon' => 'gavel',
|
'icon' => 'gavel',
|
||||||
'route' => 'queue.index'
|
'route' => 'queue.index'
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'name' => "My Drafts",
|
'name' => "My drafts",
|
||||||
'icon' => 'scissors',
|
'icon' => 'scissors',
|
||||||
'route' => 'entries.drafts',
|
'route' => 'entries.drafts',
|
||||||
'auth' => true
|
'requires_auth' => true
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ return new class extends Migration
|
|||||||
$table->longText( 'description' );
|
$table->longText( 'description' );
|
||||||
$table->string( 'main_image' )->nullable();
|
$table->string( 'main_image' )->nullable();
|
||||||
|
|
||||||
// TODO: Replace it by state.
|
|
||||||
$table->enum( 'state', [ 'draft', 'pending', 'published', 'locked', 'hidden' ] )->default('draft');
|
$table->enum( 'state', [ 'draft', 'pending', 'published', 'locked', 'hidden' ] )->default('draft');
|
||||||
$table->boolean('featured')->default(false);
|
$table->boolean('featured')->default(false);
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,22 @@ return new class extends Migration
|
|||||||
public function up(): void
|
public function up(): void
|
||||||
{
|
{
|
||||||
Schema::create('news', function (Blueprint $table) {
|
Schema::create('news', function (Blueprint $table) {
|
||||||
|
|
||||||
$table->id();
|
$table->id();
|
||||||
|
$table->string('title');
|
||||||
|
$table->string('slug')->unique();
|
||||||
|
$table->foreignId('category_id')->nullable()->constrained('categories')->nullOnDelete();
|
||||||
|
$table->longText('description');
|
||||||
|
$table->enum('state', [ 'draft', 'pending', 'published', 'locked', 'rejected', 'hidden' ] )->default('draft');
|
||||||
|
|
||||||
|
$table->foreignId('entry_id')->nullable()->constrained('entries')->nullOnDelete();
|
||||||
|
$table->string('relevant_link', 500 )->nullable();
|
||||||
|
$table->string('youtube_link', 500 )->nullable();
|
||||||
|
|
||||||
|
$table->unsignedBigInteger( 'user_id' ); // xf_user_id
|
||||||
|
$table->unsignedBigInteger( 'comments_thread_id' )->nullable(); // xf_thread
|
||||||
|
|
||||||
|
$table->softDeletes();
|
||||||
$table->timestamps();
|
$table->timestamps();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('news', function (Blueprint $table) {
|
||||||
|
$table->text("staff_comment")->nullable()->after("state");
|
||||||
|
$table->timestamp("rejected_at")->nullable()->after("staff_comment");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('news', function (Blueprint $table) {
|
||||||
|
$table->dropColumn(['staff_comment', 'rejected_at']);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('galleries', function (Blueprint $table) {
|
||||||
|
$table->unsignedSmallInteger('order')->default(0)->after('image');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('galleries', function (Blueprint $table) {
|
||||||
|
$table->dropColumn('order');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('entries', function (Blueprint $table) {
|
||||||
|
$table->dateTime('featured_at')->nullable()->after('featured');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('entries', function (Blueprint $table) {
|
||||||
|
$table->dropColumn('featured_at');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('play_online_settings', function (Blueprint $table) {
|
||||||
|
$table->unsignedBigInteger('file_id')->primary();
|
||||||
|
$table->foreign('file_id')
|
||||||
|
->references('id')
|
||||||
|
->on('entry_files')
|
||||||
|
->onDelete('cascade');
|
||||||
|
|
||||||
|
$table->string('core', 30);
|
||||||
|
$table->boolean('threads')->default(false);
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('play_online_settings');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('platforms', function (Blueprint $table) {
|
||||||
|
$table->string('play_online_core')->nullable()->default(null);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('platforms', function (Blueprint $table) {
|
||||||
|
$table->dropColumn('play_online_core');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('activity_log', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->string('log_name')->nullable()->index();
|
||||||
|
$table->text('description');
|
||||||
|
$table->nullableMorphs('subject', 'subject');
|
||||||
|
$table->string('event')->nullable();
|
||||||
|
$table->nullableMorphs('causer', 'causer');
|
||||||
|
$table->json('attribute_changes')->nullable();
|
||||||
|
$table->json('properties')->nullable();
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('entry_files', function (Blueprint $table) {
|
||||||
|
$table->unsignedBigInteger('download_count')->default(0);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('entry_files', function (Blueprint $table) {
|
||||||
|
$table->dropColumn('download_count');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::connection('discord')->create('actions', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->string('action');
|
||||||
|
$table->json('data');
|
||||||
|
$table->boolean('done')->default(false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::connection('discord')->dropIfExists('actions');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::connection('discord')->table('actions', function (Blueprint $table) {
|
||||||
|
$table->string('errors')->nullable();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::connection('discord')->table('actions', function (Blueprint $table) {
|
||||||
|
$table->dropColumn('errors');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
2172
extra.less
2172
extra.less
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
Before Width: | Height: | Size: 0 B After Width: | Height: | Size: 28 KiB |
BIN
public/logo/plaza-logo-text.png
Normal file
BIN
public/logo/plaza-logo-text.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 72 KiB |
BIN
public/logo/plaza-logo-wide.png
Normal file
BIN
public/logo/plaza-logo-wide.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 84 KiB |
BIN
public/logo/plaza-logo.png
Normal file
BIN
public/logo/plaza-logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 52 KiB |
@@ -35,7 +35,7 @@
|
|||||||
- switch to ES6 classes and modules?
|
- switch to ES6 classes and modules?
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const ROM_PATCHER_JS_PATH = './rom-patcher-js/';
|
const ROM_PATCHER_JS_PATH = '../../rom-patcher-js/';
|
||||||
|
|
||||||
const RomPatcherWeb = (function () {
|
const RomPatcherWeb = (function () {
|
||||||
const SCRIPT_DEPENDENCIES = [
|
const SCRIPT_DEPENDENCIES = [
|
||||||
@@ -198,7 +198,6 @@ const RomPatcherWeb = (function () {
|
|||||||
ZIPManager.unzipEmbededPatches(arrayBuffer, currentEmbededPatches);
|
ZIPManager.unzipEmbededPatches(arrayBuffer, currentEmbededPatches);
|
||||||
} else {
|
} else {
|
||||||
const parsedPatch = _parseEmbededPatchInfo(embededPatchInfo);
|
const parsedPatch = _parseEmbededPatchInfo(embededPatchInfo);
|
||||||
|
|
||||||
currentEmbededPatches = [parsedPatch];
|
currentEmbededPatches = [parsedPatch];
|
||||||
const option = document.createElement('option');
|
const option = document.createElement('option');
|
||||||
option.innerHTML = parsedPatch.name;
|
option.innerHTML = parsedPatch.name;
|
||||||
@@ -612,6 +611,7 @@ const RomPatcherWeb = (function () {
|
|||||||
const containerOptionalPatches = document.createElement('div');
|
const containerOptionalPatches = document.createElement('div');
|
||||||
containerOptionalPatches.id = 'rom-patcher-container-optional-patches';
|
containerOptionalPatches.id = 'rom-patcher-container-optional-patches';
|
||||||
containerOptionalPatches.style.display = 'none';
|
containerOptionalPatches.style.display = 'none';
|
||||||
|
containerOptionalPatches.classList.add("form-group", "level", "form-type-of-checkboxes");
|
||||||
htmlSelectPatch.parentElement.appendChild(containerOptionalPatches);
|
htmlSelectPatch.parentElement.appendChild(containerOptionalPatches);
|
||||||
} else {
|
} else {
|
||||||
const htmlInputFilePatch = htmlElements.get('input-file-patch');
|
const htmlInputFilePatch = htmlElements.get('input-file-patch');
|
||||||
@@ -1415,9 +1415,16 @@ const ZIPManager = (function (romPatcherWeb) {
|
|||||||
const optionalPatches = [];
|
const optionalPatches = [];
|
||||||
for (var i = 0; i < filteredEntries.length; i++) {
|
for (var i = 0; i < filteredEntries.length; i++) {
|
||||||
const embededPatchInfo = embededPatchesInfo.find((embededPatchInfo) => embededPatchInfo.file === filteredEntries[i].filename);
|
const embededPatchInfo = embededPatchesInfo.find((embededPatchInfo) => embededPatchInfo.file === filteredEntries[i].filename);
|
||||||
if (embededPatchInfo && embededPatchInfo.optional)
|
if (embededPatchInfo && embededPatchInfo.optional )
|
||||||
optionalPatches.push(filteredEntries[i]);
|
optionalPatches.push(filteredEntries[i]);
|
||||||
else
|
else if( filteredEntries[i].filename.startsWith('optional_') ){
|
||||||
|
embededPatchesInfo.push({
|
||||||
|
file: filteredEntries[i].filename,
|
||||||
|
name: filteredEntries[i].filename.replace(/^optional_/, '').replace(/_/g, ' ').replace(/\.[^.]+$/, ''),
|
||||||
|
optional: true
|
||||||
|
});
|
||||||
|
optionalPatches.push(filteredEntries[i]);
|
||||||
|
} else
|
||||||
selectablePatches.push(filteredEntries[i]);
|
selectablePatches.push(filteredEntries[i]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1446,6 +1453,7 @@ const ZIPManager = (function (romPatcherWeb) {
|
|||||||
const embededPatchInfo = embededPatchesInfo.find((embededPatchInfo) => embededPatchInfo.file === optionalPatches[i].filename);
|
const embededPatchInfo = embededPatchesInfo.find((embededPatchInfo) => embededPatchInfo.file === optionalPatches[i].filename);
|
||||||
|
|
||||||
const checkbox = document.createElement('input');
|
const checkbox = document.createElement('input');
|
||||||
|
checkbox.classList.add('form-checkbox');
|
||||||
checkbox.type = 'checkbox';
|
checkbox.type = 'checkbox';
|
||||||
checkbox.value = i;
|
checkbox.value = i;
|
||||||
checkbox.checked = false;
|
checkbox.checked = false;
|
||||||
|
|||||||
@@ -5,6 +5,8 @@
|
|||||||
@import './layout/content.css';
|
@import './layout/content.css';
|
||||||
@import './layout/entry.css';
|
@import './layout/entry.css';
|
||||||
@import './layout/news.css';
|
@import './layout/news.css';
|
||||||
|
@import './layout/activity.css';
|
||||||
|
@import './layout/submit.css';
|
||||||
|
|
||||||
@import './components/common.css';
|
@import './components/common.css';
|
||||||
@import './components/grid.css';
|
@import './components/grid.css';
|
||||||
|
|||||||
@@ -349,6 +349,50 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 5px;
|
gap: 5px;
|
||||||
}
|
}
|
||||||
|
.gallery-item {
|
||||||
|
position: relative;
|
||||||
|
cursor: grab;
|
||||||
|
transition: opacity 0.2s, transform 0.15s;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-item:active { cursor: grabbing; }
|
||||||
|
|
||||||
|
.gallery-item--dragging {
|
||||||
|
opacity: 0.4;
|
||||||
|
transform: scale(0.97);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-drag-handle {
|
||||||
|
position: absolute;
|
||||||
|
top: 4px;
|
||||||
|
left: 4px;
|
||||||
|
z-index: 10;
|
||||||
|
background-color: rgba(0,0,0,0.6);
|
||||||
|
color: #fff;
|
||||||
|
padding: 3px 4px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
cursor: grab;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-item:hover .gallery-drag-handle { opacity: 1; }
|
||||||
|
|
||||||
|
.gallery-order-badge {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 4px;
|
||||||
|
left: 4px;
|
||||||
|
z-index: 10;
|
||||||
|
background-color: rgba(0,0,0,0.7);
|
||||||
|
color: #fff;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
font-weight: 700;
|
||||||
|
padding: 2px 6px;
|
||||||
|
min-width: 20px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
.authors-list {
|
.authors-list {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(4, 1fr);
|
grid-template-columns: repeat(4, 1fr);
|
||||||
|
|||||||
@@ -48,6 +48,6 @@
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-content {
|
.modal-content, .modal-body {
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -318,3 +318,232 @@
|
|||||||
|
|
||||||
.modcp-list-item-edit--game .form-input { min-width: 180px; flex: 2; }
|
.modcp-list-item-edit--game .form-input { min-width: 180px; flex: 2; }
|
||||||
.modcp-list-item-edit--game .form-select { flex: 1; min-width: 120px; }
|
.modcp-list-item-edit--game .form-select { flex: 1; min-width: 120px; }
|
||||||
|
|
||||||
|
.log-filters {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
background-color: var(--bg3);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-filters-main {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 12px 14px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-search-wrap {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 200px;
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-search-wrap i {
|
||||||
|
position: absolute;
|
||||||
|
left: 10px;
|
||||||
|
color: var(--text2);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-search-wrap .form-input { padding-left: 30px; }
|
||||||
|
|
||||||
|
.log-select { min-width: 130px; }
|
||||||
|
|
||||||
|
.log-filter-dot {
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: var(--rhpz-orange);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-filters-extra {
|
||||||
|
border-top: 1px solid var(--border);
|
||||||
|
padding: 12px 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-filters-extra-inner {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
gap: 12px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-filter-field {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-filter-label {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.6px;
|
||||||
|
color: var(--text2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-transition-enter { transition: all .15s ease; }
|
||||||
|
.log-transition-leave { transition: all .1s ease; }
|
||||||
|
|
||||||
|
.log-results-bar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
font-size: 0.78rem;
|
||||||
|
color: var(--text2);
|
||||||
|
margin-bottom: 10px;
|
||||||
|
padding: 0 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-loading { opacity: 0.5; }
|
||||||
|
|
||||||
|
.log-item { align-items: flex-start; padding: 11px 14px; }
|
||||||
|
.log-item--open { background-color: var(--bg3); }
|
||||||
|
|
||||||
|
.log-event-dot {
|
||||||
|
width: 26px;
|
||||||
|
height: 26px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-top: 2px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
background-color: var(--bg3);
|
||||||
|
color: var(--text2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-event-dot--created {
|
||||||
|
background-color: rgba(129,199,132,.1);
|
||||||
|
border-color: rgba(129,199,132,.35);
|
||||||
|
color: var(--success);
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-event-dot--updated {
|
||||||
|
background-color: rgba(255,115,0,.1);
|
||||||
|
border-color: rgba(255,115,0,.35);
|
||||||
|
color: var(--rhpz-orange);
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-event-dot--deleted {
|
||||||
|
background-color: rgba(229,115,115,.1);
|
||||||
|
border-color: rgba(229,115,115,.35);
|
||||||
|
color: var(--error);
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-channel-badge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 0.68rem;
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
padding: 1px 6px;
|
||||||
|
background-color: rgba(255,255,255,.05);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
color: var(--text2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-id { color: var(--text2); font-size: 0.78rem; }
|
||||||
|
.log-sep { color: var(--border); }
|
||||||
|
|
||||||
|
.log-item-right {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-timestamp {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: var(--text2);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-expand-btn { padding: 4px 7px; }
|
||||||
|
|
||||||
|
.log-properties {
|
||||||
|
background-color: var(--bg);
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
padding: 14px 14px 14px 54px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-diff-label {
|
||||||
|
font-size: 0.68rem;
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.7px;
|
||||||
|
color: var(--text2);
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-diff {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-diff th {
|
||||||
|
text-align: left;
|
||||||
|
padding: 5px 10px;
|
||||||
|
font-size: 0.68rem;
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
color: var(--text2);
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-diff td {
|
||||||
|
padding: 5px 10px;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
vertical-align: top;
|
||||||
|
max-width: 300px;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-diff tr:last-child td { border-bottom: none; }
|
||||||
|
|
||||||
|
.log-diff-key {
|
||||||
|
font-size: 0.78rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text);
|
||||||
|
width: 160px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-diff-old-head { color: var(--error) !important; }
|
||||||
|
.log-diff-new-head { color: var(--success) !important; }
|
||||||
|
|
||||||
|
.log-diff-old {
|
||||||
|
color: var(--error);
|
||||||
|
background-color: rgba(229,115,115,.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-diff-new {
|
||||||
|
color: var(--success);
|
||||||
|
background-color: rgba(129,199,132,.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-raw {
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 0.78rem;
|
||||||
|
color: var(--text2);
|
||||||
|
background-color: var(--bg2);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
padding: 10px 12px;
|
||||||
|
overflow-x: auto;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-pagination {
|
||||||
|
padding: 14px 0 4px;
|
||||||
|
border-top: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|||||||
485
resources/css/layout/activity.css
Normal file
485
resources/css/layout/activity.css
Normal file
@@ -0,0 +1,485 @@
|
|||||||
|
|
||||||
|
.activity-hero-excerpt {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: rgba(255,255,255,0.75);
|
||||||
|
margin-bottom: 12px;
|
||||||
|
line-height: 1.5;
|
||||||
|
max-width: 600px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-tl-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
padding-bottom: 15px;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
gap: 15px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-tl-title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
font-size: 1.15rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text);
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-tl-filters {
|
||||||
|
display: flex;
|
||||||
|
gap: 5px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-tl-filter {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
padding: 5px 12px;
|
||||||
|
background: none;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
color: var(--text2);
|
||||||
|
font-size: 0.8rem;
|
||||||
|
cursor: pointer;
|
||||||
|
font-family: var(--typography);
|
||||||
|
transition: all 0.1s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-tl-filter:hover { background-color: var(--bg3); color: var(--text); }
|
||||||
|
.activity-tl-filter.active {
|
||||||
|
background-color: var(--bg3);
|
||||||
|
border-color: var(--rhpz-orange);
|
||||||
|
color: var(--rhpz-orange);
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-day-sep {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
padding-left: 54px;
|
||||||
|
margin: 20px 0 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-day-label {
|
||||||
|
font-size: 0.72rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text2);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.8px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-day-line {
|
||||||
|
flex: 1;
|
||||||
|
height: 1px;
|
||||||
|
background-color: var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-tl-item {
|
||||||
|
display: flex;
|
||||||
|
gap: 0;
|
||||||
|
margin-bottom: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-tl-left {
|
||||||
|
width: 54px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
padding-top: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-tl-dot {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
background-color: var(--bg2);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-tl-dot--entry {
|
||||||
|
background-color: rgba(255,115,0,0.1);
|
||||||
|
border-color: rgba(255,115,0,0.4);
|
||||||
|
color: var(--rhpz-orange);
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-tl-dot--news {
|
||||||
|
background-color: rgba(129,199,132,0.1);
|
||||||
|
border-color: rgba(129,199,132,0.4);
|
||||||
|
color: var(--success);
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-tl-dot--message, .activity-tl-dot--thread, .activity-tl-dot--club {
|
||||||
|
background-color: rgba(25,118,210,0.1);
|
||||||
|
border-color: rgba(25,118,210,0.4);
|
||||||
|
color: var(--info);
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-tl-line {
|
||||||
|
width: 1px;
|
||||||
|
flex: 1;
|
||||||
|
background-color: var(--border);
|
||||||
|
margin-top: 4px;
|
||||||
|
min-height: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-tl-item:last-of-type .activity-tl-line { display: none; }
|
||||||
|
|
||||||
|
.activity-tl-card {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
background-color: var(--bg2);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
padding: 10px 14px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: border-color 0.15s, background-color 0.1s;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-tl-card:hover {
|
||||||
|
border-color: var(--rhpz-orange);
|
||||||
|
background-color: var(--bg3);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-tl-thumb {
|
||||||
|
width: 52px;
|
||||||
|
height: 52px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background-color: var(--bg3);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-tl-thumb img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-tl-body {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-tl-badge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
font-size: 0.68rem;
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.6px;
|
||||||
|
padding: 2px 7px;
|
||||||
|
width: fit-content;
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-tl-badge--entry {
|
||||||
|
background-color: rgba(255,115,0,0.1);
|
||||||
|
color: var(--rhpz-orange);
|
||||||
|
border: 1px solid rgba(255,115,0,0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-tl-badge--news {
|
||||||
|
background-color: rgba(129,199,132,0.1);
|
||||||
|
color: var(--success);
|
||||||
|
border: 1px solid rgba(129,199,132,0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-tl-badge--message, .activity-tl-badge--thread, .activity-tl-dot--club {
|
||||||
|
background-color: rgba(25,118,210,0.1);
|
||||||
|
color: var(--info);
|
||||||
|
border: 1px solid rgba(25,118,210,0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-tl-card-title {
|
||||||
|
font-size: 0.92rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text);
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-tl-card-description {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: var(--text2);
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-tl-meta {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: var(--text2);
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-tl-meta span {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-tl-time {
|
||||||
|
font-size: 0.72rem;
|
||||||
|
color: var(--text2);
|
||||||
|
white-space: nowrap;
|
||||||
|
flex-shrink: 0;
|
||||||
|
align-self: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-tl-empty {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 60px;
|
||||||
|
color: var(--text2);
|
||||||
|
text-align: center;
|
||||||
|
padding-left: 54px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
.activity-tl-header { flex-direction: column; align-items: flex-start; }
|
||||||
|
.activity-tl-thumb { display: none; }
|
||||||
|
.activity-day-sep { padding-left: 44px; }
|
||||||
|
.activity-tl-left { width: 44px; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.home-section {
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.home-section-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding-bottom: 12px;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
margin-bottom: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.home-section-title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
font-size: 1.05rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text);
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.home-section-more {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: var(--text2);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
padding: 4px 10px;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: color 0.1s, border-color 0.1s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.home-section-more:hover {
|
||||||
|
color: var(--rhpz-orange);
|
||||||
|
border-color: var(--rhpz-orange);
|
||||||
|
}
|
||||||
|
|
||||||
|
.news-strip {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(5, 1fr);
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.news-strip-card {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background-color: var(--bg2);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
text-decoration: none;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: border-color 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.news-strip-card:hover { border-color: var(--rhpz-orange); text-decoration: none; }
|
||||||
|
|
||||||
|
.news-strip-cover {
|
||||||
|
height: 110px;
|
||||||
|
background-color: var(--bg3);
|
||||||
|
background-size: cover;
|
||||||
|
background-position: center;
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.news-strip-date {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 6px;
|
||||||
|
left: 8px;
|
||||||
|
font-size: 0.68rem;
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
color: rgba(255,255,255,0.8);
|
||||||
|
background: rgba(0,0,0,0.55);
|
||||||
|
padding: 2px 6px;
|
||||||
|
border: 1px solid rgba(255,255,255,0.07);
|
||||||
|
}
|
||||||
|
|
||||||
|
.news-strip-body {
|
||||||
|
padding: 10px 12px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 5px;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.news-strip-badge {
|
||||||
|
font-size: 0.65rem;
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
color: var(--success);
|
||||||
|
background: rgba(129,199,132,0.1);
|
||||||
|
border: 1px solid rgba(129,199,132,0.25);
|
||||||
|
padding: 1px 6px;
|
||||||
|
width: fit-content;
|
||||||
|
}
|
||||||
|
|
||||||
|
.news-strip-title {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text);
|
||||||
|
line-height: 1.35;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.news-strip-meta {
|
||||||
|
font-size: 0.72rem;
|
||||||
|
color: var(--text2);
|
||||||
|
margin-top: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.featured-entries-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, 1fr);
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.featured-entry-card {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background-color: var(--bg2);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
text-decoration: none;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: border-color 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.featured-entry-card:hover { border-color: var(--rhpz-orange); text-decoration: none; }
|
||||||
|
|
||||||
|
.featured-entry-cover {
|
||||||
|
height: 80px;
|
||||||
|
background-color: var(--bg3);
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.featured-entry-cover img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
opacity: 0.85;
|
||||||
|
}
|
||||||
|
|
||||||
|
.featured-entry-star {
|
||||||
|
position: absolute;
|
||||||
|
top: 6px;
|
||||||
|
right: 6px;
|
||||||
|
font-size: 0.65rem;
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
background: rgba(255,115,0,0.9);
|
||||||
|
color: #111;
|
||||||
|
padding: 2px 6px;
|
||||||
|
border: 1px solid rgba(255,115,0,0.5);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.featured-entry-body {
|
||||||
|
padding: 10px 12px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 5px;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.featured-entry-platform {
|
||||||
|
font-size: 0.65rem;
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
color: var(--rhpz-orange);
|
||||||
|
background: rgba(255,115,0,0.1);
|
||||||
|
border: 1px solid rgba(255,115,0,0.25);
|
||||||
|
padding: 1px 6px;
|
||||||
|
width: fit-content;
|
||||||
|
}
|
||||||
|
|
||||||
|
.featured-entry-title {
|
||||||
|
font-size: 0.88rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text);
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.featured-entry-meta {
|
||||||
|
font-size: 0.72rem;
|
||||||
|
color: var(--text2);
|
||||||
|
margin-top: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
.news-strip { grid-template-columns: repeat(3, 1fr); }
|
||||||
|
.featured-entries-grid { grid-template-columns: repeat(2, 1fr); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
.news-strip { grid-template-columns: repeat(2, 1fr); }
|
||||||
|
.featured-entries-grid { grid-template-columns: repeat(2, 1fr); }
|
||||||
|
.news-strip-cover { height: 80px; }
|
||||||
|
}
|
||||||
@@ -9,12 +9,18 @@
|
|||||||
z-index: 100;
|
z-index: 100;
|
||||||
|
|
||||||
.menu-header {
|
.menu-header {
|
||||||
padding: 20px;
|
padding: 10px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
border-bottom: 1px solid var(--border);
|
border-bottom: 1px solid var(--border);
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
.menu-logo {
|
.menu-logo {
|
||||||
width: 32px;
|
width: 32px;
|
||||||
height: 32px;
|
height: 32px;
|
||||||
|
|||||||
@@ -46,3 +46,441 @@
|
|||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#news-container {
|
||||||
|
background-color: var(--bg2);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.news-header {
|
||||||
|
width: 100%;
|
||||||
|
height: 300px;
|
||||||
|
background-size: cover;
|
||||||
|
background-position: center;
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
padding: 40px 30px;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.news-header-content {
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.news-header .news-title {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text);
|
||||||
|
margin-bottom: 12px;
|
||||||
|
text-shadow: 0 2px 4px rgba(0,0,0,0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.news-header .news-meta {
|
||||||
|
color: var(--text2);
|
||||||
|
display: flex;
|
||||||
|
gap: 20px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.news-header .meta-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
background-color: rgba(0, 0, 0, 0.4);
|
||||||
|
padding: 4px 10px;
|
||||||
|
border-radius: 2px;
|
||||||
|
border: 1px solid rgba(255,255,255,0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.news-layout {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 30px;
|
||||||
|
padding: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 992px) {
|
||||||
|
.news-layout {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.news-main-content {
|
||||||
|
flex-grow: 1;
|
||||||
|
flex-basis: 0;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.news-body-text {
|
||||||
|
line-height: 1.75;
|
||||||
|
color: var(--text);
|
||||||
|
font-size: 1.05rem;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.news-body-text p {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.news-sidebar {
|
||||||
|
width: 320px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 992px) {
|
||||||
|
.news-sidebar {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-block {
|
||||||
|
background-color: var(--bg);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-title {
|
||||||
|
font-size: 1rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
color: var(--text);
|
||||||
|
margin-bottom: 15px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
padding-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-sidebar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8px;
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px 15px;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
font-weight: 500;
|
||||||
|
text-decoration: none;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 2px;
|
||||||
|
transition: background-color 0.2s ease, border-color 0.2s ease;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-orange {
|
||||||
|
background-color: var(--rhpz-orange);
|
||||||
|
color: #fff;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-orange:hover {
|
||||||
|
background-color: var(--rhpz-orange-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.related-card {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.related-card-cover {
|
||||||
|
width: 100%;
|
||||||
|
height: 150px;
|
||||||
|
background-color: var(--bg2);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.related-card-cover img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
padding: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.related-card-info h4 {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
color: var(--text);
|
||||||
|
margin-bottom: 10px;
|
||||||
|
font-weight: 500;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.news-sidebar .video-thumbnail-wrapper {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
aspect-ratio: 16/9;
|
||||||
|
background-color: #000;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
cursor: pointer;
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.news-sidebar .video-thumbnail-wrapper img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
opacity: 0.7;
|
||||||
|
transition: transform 0.3s ease, opacity 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.news-sidebar .video-thumbnail-wrapper:hover img {
|
||||||
|
transform: scale(1.03);
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.news-sidebar .play-trigger {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
width: 50px;
|
||||||
|
height: 50px;
|
||||||
|
background-color: rgba(0, 0, 0, 0.75);
|
||||||
|
border: 2px solid #fff;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: #fff;
|
||||||
|
transition: background-color 0.2s, transform 0.2s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.news-sidebar .video-thumbnail-wrapper:hover .play-trigger {
|
||||||
|
background-color: var(--rhpz-orange);
|
||||||
|
transform: translate(-50%, -50%) scale(1.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.news-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 15px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.news-header .news-actions .btn {
|
||||||
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
|
border-color: rgba(255, 255, 255, 0.15);
|
||||||
|
backdrop-filter: blur(4px);
|
||||||
|
color: var(--text);
|
||||||
|
transition: background-color 0.15s, border-color 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.news-header .news-actions .btn:hover {
|
||||||
|
background-color: rgba(0, 0, 0, 0.7);
|
||||||
|
border-color: rgba(255, 255, 255, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.news-header .news-actions .btn.success {
|
||||||
|
background-color: rgba(56, 142, 60, 0.6);
|
||||||
|
border-color: rgba(129, 199, 132, 0.4);
|
||||||
|
color: #81c784;
|
||||||
|
}
|
||||||
|
|
||||||
|
.news-header .news-actions .btn.danger {
|
||||||
|
background-color: rgba(183, 28, 28, 0.5);
|
||||||
|
border-color: rgba(229, 115, 115, 0.4);
|
||||||
|
color: #e57373;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Hero ────────────────────────────────────────────────── */
|
||||||
|
.news-hero {
|
||||||
|
display: block;
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 360px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
overflow: hidden;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: border-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.news-hero:hover {
|
||||||
|
border-color: var(--rhpz-orange);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.news-hero-bg {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background-size: cover;
|
||||||
|
background-position: center;
|
||||||
|
background-color: var(--bg3);
|
||||||
|
transition: transform 0.4s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.news-hero:hover .news-hero-bg {
|
||||||
|
transform: scale(1.02);
|
||||||
|
}
|
||||||
|
|
||||||
|
.news-hero-content {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
padding: 30px;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.news-hero-badge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
background-color: var(--rhpz-orange);
|
||||||
|
color: #111;
|
||||||
|
font-size: 0.72rem;
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.8px;
|
||||||
|
padding: 4px 10px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.news-hero-title {
|
||||||
|
font-size: 2rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #fff;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
text-shadow: 0 2px 8px rgba(0,0,0,0.5);
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.news-hero-meta {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 15px;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: rgba(255,255,255,0.75);
|
||||||
|
}
|
||||||
|
|
||||||
|
.news-hero-meta span {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
background-color: rgba(0,0,0,0.4);
|
||||||
|
padding: 3px 10px;
|
||||||
|
border: 1px solid rgba(255,255,255,0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.news-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||||
|
gap: 15px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.news-card {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background-color: var(--bg2);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
text-decoration: none;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: border-color 0.15s, transform 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.news-card:hover {
|
||||||
|
border-color: var(--rhpz-orange);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.news-card-cover {
|
||||||
|
height: 160px;
|
||||||
|
background-size: cover;
|
||||||
|
background-position: center;
|
||||||
|
background-color: var(--bg3);
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.news-card:hover .news-card-cover {
|
||||||
|
transform: scale(1.03);
|
||||||
|
}
|
||||||
|
|
||||||
|
.news-card-state-badge {
|
||||||
|
position: absolute;
|
||||||
|
top: 10px;
|
||||||
|
right: 10px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
background-color: rgba(0,0,0,0.7);
|
||||||
|
color: var(--rhpz-orange);
|
||||||
|
font-size: 0.72rem;
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 3px 8px;
|
||||||
|
border: 1px solid rgba(255,115,0,0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.news-card-body {
|
||||||
|
padding: 16px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex: 1;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.news-card-title {
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text);
|
||||||
|
line-height: 1.3;
|
||||||
|
margin: 0;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.news-card-excerpt {
|
||||||
|
font-size: 0.82rem;
|
||||||
|
color: var(--text2);
|
||||||
|
line-height: 1.5;
|
||||||
|
margin: 0;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.news-card-meta {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: var(--text2);
|
||||||
|
margin-top: auto;
|
||||||
|
padding-top: 8px;
|
||||||
|
border-top: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.news-card-meta span {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
.news-hero { height: 240px; }
|
||||||
|
.news-hero-title { font-size: 1.4rem; }
|
||||||
|
.news-grid { grid-template-columns: 1fr; }
|
||||||
|
}
|
||||||
|
|||||||
297
resources/css/layout/submit.css
Normal file
297
resources/css/layout/submit.css
Normal file
@@ -0,0 +1,297 @@
|
|||||||
|
.submit-hero {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 24px;
|
||||||
|
background-color: var(--bg2);
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
padding: 40px 36px 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-eyebrow {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
font-size: 0.68rem;
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
color: var(--rhpz-orange);
|
||||||
|
background: rgba(255,115,0,.1);
|
||||||
|
border: 1px solid rgba(255,115,0,.3);
|
||||||
|
padding: 3px 10px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-hero-title {
|
||||||
|
font-size: 1.9rem;
|
||||||
|
font-weight: 300;
|
||||||
|
color: var(--text);
|
||||||
|
margin-bottom: 10px;
|
||||||
|
line-height: 1.25;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-hero-sub {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: var(--text2);
|
||||||
|
max-width: 460px;
|
||||||
|
line-height: 1.65;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-review-note {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: var(--text2);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
background: var(--bg3);
|
||||||
|
padding: 14px 18px;
|
||||||
|
max-width: 210px;
|
||||||
|
line-height: 1.6;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-review-note strong { color: var(--rhpz-orange); }
|
||||||
|
|
||||||
|
.submit-body { padding: 28px 36px 40px; }
|
||||||
|
|
||||||
|
.submit-section-label {
|
||||||
|
font-size: 0.68rem;
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
color: var(--text2);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-section-label::after {
|
||||||
|
content: '';
|
||||||
|
flex: 1;
|
||||||
|
height: 1px;
|
||||||
|
background: var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-card {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background: var(--bg2);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
text-decoration: none;
|
||||||
|
transition: border-color .15s, background .1s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-card:hover {
|
||||||
|
border-color: var(--card-color);
|
||||||
|
background: var(--bg3);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-card-top {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 14px;
|
||||||
|
padding: 20px 20px 16px;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-card-icon {
|
||||||
|
width: 38px;
|
||||||
|
height: 38px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: var(--card-bg);
|
||||||
|
border: 1px solid var(--card-border);
|
||||||
|
color: var(--card-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-card-title {
|
||||||
|
font-size: 0.95rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text);
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-card-desc {
|
||||||
|
font-size: 0.78rem;
|
||||||
|
color: var(--text2);
|
||||||
|
line-height: 1.55;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-card-bottom {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 10px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-card-tag {
|
||||||
|
font-size: 0.65rem;
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
color: var(--card-color);
|
||||||
|
background: var(--card-bg);
|
||||||
|
border: 1px solid var(--card-border);
|
||||||
|
padding: 2px 7px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-card-cta {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: var(--text2);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
transition: color .15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-card:hover .submit-card-cta { color: var(--card-color); }
|
||||||
|
|
||||||
|
.submit-news-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-news-card {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 14px;
|
||||||
|
background: var(--bg2);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
padding: 18px 22px;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: border-color .15s, background .1s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-news-card:hover {
|
||||||
|
border-color: var(--success);
|
||||||
|
background: var(--bg3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-news-card--disabled { opacity: .5; cursor: not-allowed; }
|
||||||
|
.submit-news-card--disabled:hover { border-color: var(--border); background: var(--bg2); }
|
||||||
|
|
||||||
|
.submit-news-icon {
|
||||||
|
width: 38px;
|
||||||
|
height: 38px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: rgba(129,199,132,.1);
|
||||||
|
border: 1px solid rgba(129,199,132,.3);
|
||||||
|
color: var(--success);
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-news-title {
|
||||||
|
font-size: 0.95rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text);
|
||||||
|
margin-bottom: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-news-desc { font-size: 0.78rem; color: var(--text2); line-height: 1.55; }
|
||||||
|
|
||||||
|
.submit-news-cta {
|
||||||
|
flex-shrink: 0;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: var(--text2);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
transition: color .15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-news-card:hover .submit-news-cta { color: var(--success); }
|
||||||
|
|
||||||
|
.submit-news-staff-note {
|
||||||
|
background: var(--bg2);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
padding: 18px 22px;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: var(--text2);
|
||||||
|
line-height: 1.65;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-news-staff-badge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
font-size: 0.65rem;
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: .6px;
|
||||||
|
color: var(--rhpz-orange);
|
||||||
|
background: rgba(255,115,0,.1);
|
||||||
|
border: 1px solid rgba(255,115,0,.3);
|
||||||
|
padding: 2px 8px;
|
||||||
|
width: fit-content;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-news-staff-note a { color: var(--text2); text-decoration: underline; }
|
||||||
|
.submit-news-staff-note a:hover { color: var(--text); }
|
||||||
|
|
||||||
|
.submit-rules {
|
||||||
|
display: flex;
|
||||||
|
gap: 0;
|
||||||
|
background: var(--bg2);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-rule {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 18px 22px;
|
||||||
|
border-right: 1px solid var(--border);
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: var(--text2);
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-rule:last-child { border-right: none; }
|
||||||
|
.submit-rule strong { color: var(--text); }
|
||||||
|
|
||||||
|
.submit-rule-num {
|
||||||
|
width: 22px;
|
||||||
|
height: 22px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: rgba(255,115,0,.1);
|
||||||
|
border: 1px solid rgba(255,115,0,.3);
|
||||||
|
color: var(--rhpz-orange);
|
||||||
|
font-size: 0.72rem;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
.submit-hero { flex-direction: column; align-items: flex-start; }
|
||||||
|
.submit-grid { grid-template-columns: repeat(2, 1fr); }
|
||||||
|
.submit-rules { flex-direction: column; }
|
||||||
|
.submit-rule { border-right: none; border-bottom: 1px solid var(--border); }
|
||||||
|
.submit-rule:last-child { border-bottom: none; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
.submit-hero, .submit-body { padding-left: 20px; padding-right: 20px; }
|
||||||
|
.submit-grid { grid-template-columns: 1fr; }
|
||||||
|
.submit-news-row { grid-template-columns: 1fr; }
|
||||||
|
.submit-review-note { max-width: 100%; }
|
||||||
|
}
|
||||||
85
resources/js/PlayOnlineAndPatcher.js
Normal file
85
resources/js/PlayOnlineAndPatcher.js
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
import { RomPatcher } from './RomPatcher.js';
|
||||||
|
|
||||||
|
window.PlayOnline = function( initialPatches = {}, emulatorJsConfig = {} ){
|
||||||
|
|
||||||
|
const parent = RomPatcher( initialPatches );
|
||||||
|
|
||||||
|
return {
|
||||||
|
|
||||||
|
...parent,
|
||||||
|
|
||||||
|
currentBlobUrl: null,
|
||||||
|
emuConfig: emulatorJsConfig,
|
||||||
|
launchGame: false,
|
||||||
|
|
||||||
|
init(){
|
||||||
|
parent.init({
|
||||||
|
language: 'en',
|
||||||
|
requireValidation: false,
|
||||||
|
onpatch: this.handlePatchedRomFile.bind(this),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
cleanEmulatorJsVars() {
|
||||||
|
['EJS_player','EJS_core','EJS_gameUrl','EJS_pathtodata',
|
||||||
|
'EJS_startOnLoaded','EJS_threads']
|
||||||
|
.forEach(k => delete window[k]);
|
||||||
|
},
|
||||||
|
|
||||||
|
prepareEmulatorJs(){
|
||||||
|
window.EJS_player = '#game';
|
||||||
|
window.EJS_core = this.emuConfig.core;
|
||||||
|
window.EJS_gameUrl = this.currentBlobUrl;
|
||||||
|
window.EJS_pathtodata = "https://cdn.emulatorjs.org/stable/data/";
|
||||||
|
window.EJS_startOnLoaded = true;
|
||||||
|
window.EJS_threads = this.emuConfig.threads ?? false;
|
||||||
|
},
|
||||||
|
|
||||||
|
launchEmulatorJs(){
|
||||||
|
if(!this.currentBlobUrl){
|
||||||
|
console.error("EmulatorJS: Empty Blob field");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(this.currentBlobUrl);
|
||||||
|
|
||||||
|
this.cleanEmulatorJsVars();
|
||||||
|
this.prepareEmulatorJs();
|
||||||
|
|
||||||
|
const script = document.createElement('script');
|
||||||
|
script.id = 'ejs-loader';
|
||||||
|
script.src = 'https://cdn.emulatorjs.org/stable/data/loader.js';
|
||||||
|
document.body.appendChild(script);
|
||||||
|
|
||||||
|
this.launchGame = true;
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {BinFile} patchedRomFile
|
||||||
|
*/
|
||||||
|
handlePatchedRomFile( patchedRomFile ){
|
||||||
|
|
||||||
|
patchedRomFile.save = function(){
|
||||||
|
// Remove save.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const u8 = patchedRomFile._u8array;
|
||||||
|
if( !u8 || u8.byteLength === 0 ){
|
||||||
|
console.error("Patch error: Empty ROM file");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(this.currentBlobUrl){
|
||||||
|
URL.revokeObjectURL(this.currentBlobUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
const blob = new Blob([u8], { type: 'application/octet-stream' });
|
||||||
|
this.currentBlobUrl = URL.createObjectURL(blob);
|
||||||
|
|
||||||
|
this.launchEmulatorJs()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
export function RomPatcher( initialPatches = {} ) {
|
export const RomPatcher = function( initialPatches = {} ) {
|
||||||
|
|
||||||
let patchesArray = [];
|
let patchesArray = [];
|
||||||
if (initialPatches) {
|
if (initialPatches) {
|
||||||
@@ -39,9 +39,9 @@ export function RomPatcher( initialPatches = {} ) {
|
|||||||
patchesData: patchesArray,
|
patchesData: patchesArray,
|
||||||
hasEmbedded: patchesArray.length > 0,
|
hasEmbedded: patchesArray.length > 0,
|
||||||
|
|
||||||
init() {
|
init( config = {language: 'en', requireValidation: false} ) {
|
||||||
|
|
||||||
const CONFIG = {language: 'en', requireValidation: false};
|
const CONFIG = config;
|
||||||
|
|
||||||
if (!RomPatcherWeb.isInitialized()){
|
if (!RomPatcherWeb.isInitialized()){
|
||||||
if (this.hasEmbedded) {
|
if (this.hasEmbedded) {
|
||||||
@@ -112,3 +112,7 @@ export function RomPatcher( initialPatches = {} ) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
window.RomPatcher = RomPatcher;
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,10 @@
|
|||||||
|
|
||||||
export const CHUNK_SIZE = 8192;
|
export const CHUNK_SIZE = 8192;
|
||||||
|
|
||||||
|
const PATCH_EXTENSIONS = new Set([
|
||||||
|
'ips', 'bps', 'ups', 'aps', 'ppf', 'xdelta', "zip"
|
||||||
|
]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An uploaded file instance.
|
* An uploaded file instance.
|
||||||
* Create a new file data.
|
* Create a new file data.
|
||||||
@@ -12,6 +16,8 @@ export const CHUNK_SIZE = 8192;
|
|||||||
*/
|
*/
|
||||||
export function FSFileData(name, totalChunks, rawFile ) {
|
export function FSFileData(name, totalChunks, rawFile ) {
|
||||||
|
|
||||||
|
const extension = name.split('.').pop().toLowerCase();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -66,6 +72,8 @@ export function FSFileData(name, totalChunks, rawFile ) {
|
|||||||
*/
|
*/
|
||||||
state: 'public',
|
state: 'public',
|
||||||
|
|
||||||
|
can_be_online_patched: PATCH_EXTENSIONS.has(extension),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* If the online patcher is enabled
|
* If the online patcher is enabled
|
||||||
*/
|
*/
|
||||||
@@ -76,6 +84,21 @@ export function FSFileData(name, totalChunks, rawFile ) {
|
|||||||
*/
|
*/
|
||||||
meta_secondary_online_patcher: false,
|
meta_secondary_online_patcher: false,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If this patch can be played online.
|
||||||
|
*/
|
||||||
|
meta_play_online: false,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Selected core for play online
|
||||||
|
*/
|
||||||
|
meta_play_online_core: null,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If the threads are enabled for playing online.
|
||||||
|
*/
|
||||||
|
meta_play_online_threads: null,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Look if this file is currently uploading.
|
* Look if this file is currently uploading.
|
||||||
* @returns {boolean}
|
* @returns {boolean}
|
||||||
@@ -164,6 +187,6 @@ export function FSFileData(name, totalChunks, rawFile ) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ export function GalleryManager() {
|
|||||||
*/
|
*/
|
||||||
images: [],
|
images: [],
|
||||||
|
|
||||||
|
dragSrcI: null,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Forward to this.images.length
|
* Forward to this.images.length
|
||||||
* @returns {number}
|
* @returns {number}
|
||||||
@@ -123,6 +125,25 @@ export function GalleryManager() {
|
|||||||
handleRemoveFile(index){
|
handleRemoveFile(index){
|
||||||
this.images[index].handleRemoveFile(null);
|
this.images[index].handleRemoveFile(null);
|
||||||
this.images.splice(index, 1);
|
this.images.splice(index, 1);
|
||||||
|
},
|
||||||
|
|
||||||
|
dragStart(index){
|
||||||
|
this.dragSrcI = index;
|
||||||
|
},
|
||||||
|
|
||||||
|
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;
|
||||||
|
},
|
||||||
|
|
||||||
|
dragEnd(){
|
||||||
|
this.dragSrcI = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import hovercard from "./hovercard.js";
|
|||||||
import notifications from "./notifications.js";
|
import notifications from "./notifications.js";
|
||||||
import conversations from "./conversations.js";
|
import conversations from "./conversations.js";
|
||||||
import settings from "./settings.js";
|
import settings from "./settings.js";
|
||||||
import {RomPatcher} from "./RomPatcher.js";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get config defined in meta.blade.php
|
* Get config defined in meta.blade.php
|
||||||
@@ -15,7 +14,7 @@ import {RomPatcher} from "./RomPatcher.js";
|
|||||||
* @return {string|null}
|
* @return {string|null}
|
||||||
*/
|
*/
|
||||||
window.getConfig = function( key ){
|
window.getConfig = function( key ){
|
||||||
return document.querySelector('meta[name="config-' + key + '"]').getAttribute('content') ?? null;
|
return document.querySelector('meta[name="config-' + key + '"]')?.getAttribute('content') ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Lucide icons.
|
// Lucide icons.
|
||||||
@@ -44,6 +43,3 @@ Alpine.store('conversations', conversations() );
|
|||||||
|
|
||||||
// Settings
|
// Settings
|
||||||
Alpine.store('settings', settings() );
|
Alpine.store('settings', settings() );
|
||||||
|
|
||||||
// ROMPatcher
|
|
||||||
window.RomPatcher = RomPatcher;
|
|
||||||
|
|||||||
169
resources/js/news-submissions.js
Normal file
169
resources/js/news-submissions.js
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
import { GalleryManager } from "./SubmissionsClass/GalleryManager.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If there is some server side errors.
|
||||||
|
* We may need reload some things.
|
||||||
|
* @type {boolean}
|
||||||
|
*/
|
||||||
|
const SERVER_SIDE_ERRORS = document.querySelector('meta[name="submission-has-errors"]')?.content === '1';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Object map of errors messages
|
||||||
|
* @type {Object<string,string>}
|
||||||
|
*/
|
||||||
|
const ERROR_TABLE = {
|
||||||
|
noDescription: "Please provide a description.",
|
||||||
|
noGalleryImages: "Please select at least a gallery image.",
|
||||||
|
isSubmitting: "The entry is already during submission."
|
||||||
|
}
|
||||||
|
|
||||||
|
window.GalleryManager = GalleryManager;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify if an EasyMDE field is filled.
|
||||||
|
*
|
||||||
|
* @param {string} fieldName
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
function verifyMDE( fieldName ){
|
||||||
|
const textarea = document.querySelector('#field_' + fieldName);
|
||||||
|
if( textarea && textarea.value.trim().length > 0 ) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const field = window['mde_' + fieldName] || null;
|
||||||
|
return field && typeof field.value === 'function' && field.value().trim().length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.SubmissionVerifications = {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify if the description field has at least one character.
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
step1_VerifyDescription: function(){
|
||||||
|
return verifyMDE('description');
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify if at least one image is uploaded in the gallery.
|
||||||
|
* @param element this.$el
|
||||||
|
* @return {boolean}
|
||||||
|
*/
|
||||||
|
step2_verifyGallery: function( element){
|
||||||
|
let GalleryData = element.querySelector('[x-data="GalleryManager()"]');
|
||||||
|
GalleryData = GalleryData ? Alpine.$data(GalleryData) : null;
|
||||||
|
|
||||||
|
if( ! GalleryData ){
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return GalleryData.number > 0 && GalleryData.allUploaded;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle entire submission process.
|
||||||
|
*/
|
||||||
|
window.NewsSubmission = function(){
|
||||||
|
return {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If the script is during a try of submission process.
|
||||||
|
* @type {boolean}
|
||||||
|
*/
|
||||||
|
duringSubmissionProcess: false,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Error checked.
|
||||||
|
* @type {string|null}
|
||||||
|
*/
|
||||||
|
errorKey: null,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return error message.
|
||||||
|
* @return {string}
|
||||||
|
*/
|
||||||
|
get errorMessage(){
|
||||||
|
return ERROR_TABLE[this.errorKey] ?? "Unknown error";
|
||||||
|
},
|
||||||
|
|
||||||
|
init(){
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Do each form verifications.
|
||||||
|
* Update also this.errorKey.
|
||||||
|
*
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
verifyForm(){
|
||||||
|
|
||||||
|
console.log( "Step 1" );
|
||||||
|
if( !SubmissionVerifications.step1_VerifyDescription() ){
|
||||||
|
this.errorKey = "noDescription";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log( "Step 2" );
|
||||||
|
if( !SubmissionVerifications.step2_verifyGallery( this.$el )){
|
||||||
|
this.errorKey = "noGalleryImages";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scroll to the specific error field.
|
||||||
|
*/
|
||||||
|
scrollToError(){
|
||||||
|
const refMap = {
|
||||||
|
noDescription: 'descriptionField',
|
||||||
|
noGalleryImages: 'gallery-field',
|
||||||
|
isSubmitting: 'submitButton'
|
||||||
|
};
|
||||||
|
|
||||||
|
const target = this.$refs[refMap[this.errorKey]]
|
||||||
|
|| this.$el.querySelector('.upload-list')
|
||||||
|
|| this.$el.querySelector('.form-upload');
|
||||||
|
|
||||||
|
if (target) {
|
||||||
|
target.scrollIntoView({behavior: 'smooth', block: 'center'});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If you want to submit the form.
|
||||||
|
* @param {Event} e
|
||||||
|
*/
|
||||||
|
submitForm( e ){
|
||||||
|
|
||||||
|
if( this.duringSubmissionProcess )
|
||||||
|
return; // Don't submit two times.
|
||||||
|
|
||||||
|
this.errorKey = null; // Reset.
|
||||||
|
this.duringSubmissionProcess = true;
|
||||||
|
|
||||||
|
const STATE = document.querySelector('select[name="submit-state"]')?.value;
|
||||||
|
if( STATE === 'draft' ){
|
||||||
|
e.target.submit();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if( !this.verifyForm() ){
|
||||||
|
|
||||||
|
this.scrollToError();
|
||||||
|
|
||||||
|
this.duringSubmissionProcess = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
e.target.submit();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,20 +14,15 @@ export default function settings() {
|
|||||||
*/
|
*/
|
||||||
xfUrls: {},
|
xfUrls: {},
|
||||||
|
|
||||||
/**
|
|
||||||
* @type {number[]}
|
|
||||||
*/
|
|
||||||
entriesPerPage: [ 12, 30, 48 ],
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @type {string}
|
* @type {string}
|
||||||
*/
|
*/
|
||||||
currentTheme: Cookies.get("theme") ?? 'default',
|
currentTheme: 'default',
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @type {number}
|
* @type {list|null}
|
||||||
*/
|
*/
|
||||||
currentEntriesPerPage: Cookies.get("entries_per_page") ?? 30,
|
currentActivityFilters: null,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
@@ -43,7 +38,7 @@ export default function settings() {
|
|||||||
this.currentTheme = newTheme;
|
this.currentTheme = newTheme;
|
||||||
document.documentElement.classList.toggle('light-mode', this.currentTheme === 'alternate');
|
document.documentElement.classList.toggle('light-mode', this.currentTheme === 'alternate');
|
||||||
|
|
||||||
Cookies.set('theme', this.currentTheme, { expires: 365, path: '/', domain: window.getConfig('session-domain') } );
|
// Cookies.set('theme', this.currentTheme, { expires: 365, path: '/', domain: window.getConfig('session-domain') } );
|
||||||
this.syncXF();
|
this.syncXF();
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -62,22 +57,48 @@ export default function settings() {
|
|||||||
this.themeChanged(this.currentTheme === 'default' ? 'alternate' : 'default');
|
this.themeChanged(this.currentTheme === 'default' ? 'alternate' : 'default');
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @param n
|
|
||||||
*/
|
|
||||||
entriesPerPageChanged( n ){
|
|
||||||
if( !this.entriesPerPage.includes(n) )
|
|
||||||
return;
|
|
||||||
|
|
||||||
this.entriesPerPage = n;
|
|
||||||
Cookies.set('entries_per_page', this.entriesPerPage, { expires: 365, path: '/', domain: window.getConfig('session-domain') } );
|
|
||||||
if( window.Livewire ){
|
|
||||||
Livewire.dispatch('entriesPerPageChanged', {n});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
open(){ this.start = !this.start; },
|
open(){ this.start = !this.start; },
|
||||||
close(){ this.start = false; },
|
close(){ this.start = false; },
|
||||||
|
|
||||||
|
async toggleActivityFilter( type ){
|
||||||
|
|
||||||
|
if( this.currentActivityFilters === null )
|
||||||
|
return;
|
||||||
|
|
||||||
|
const i = this.currentActivityFilters.indexOf( type );
|
||||||
|
if( i !== -1 && this.currentActivityFilters.length === 1)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if( i === -1 )
|
||||||
|
this.currentActivityFilters.push( type );
|
||||||
|
else
|
||||||
|
this.currentActivityFilters.splice( i, 1 );
|
||||||
|
|
||||||
|
Cookies.set( 'activity_filters', JSON.stringify(this.currentActivityFilters), { expires: 365, path: '/', domain: window.getConfig('session-domain') } );
|
||||||
|
await this.syncTimeline();
|
||||||
|
},
|
||||||
|
|
||||||
|
async syncTimeline(){
|
||||||
|
|
||||||
|
const tl = document.getElementById('activity-timeline');
|
||||||
|
if( !tl )
|
||||||
|
return;
|
||||||
|
|
||||||
|
tl.style.opacity = 0.5;
|
||||||
|
|
||||||
|
const params = this.currentActivityFilters.join(',');
|
||||||
|
const response = await fetch(`/api/dynamic/activity/feed?filters=${params}`);
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if( !data.html )
|
||||||
|
return;
|
||||||
|
|
||||||
|
tl.innerHTML = data.html;
|
||||||
|
tl.style.opacity = 1;
|
||||||
|
|
||||||
|
refreshIcons(tl);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ const ERROR_TABLE = {
|
|||||||
* @constructor
|
* @constructor
|
||||||
*/
|
*/
|
||||||
const SECTION = () => document.querySelector("meta[name='fs-section']")?.content ?? '';
|
const SECTION = () => document.querySelector("meta[name='fs-section']")?.content ?? '';
|
||||||
|
const CSRF = () => document.querySelector("meta[name='csrf-token']")?.content ?? '';
|
||||||
|
|
||||||
window.FSUploader = FSUploader;
|
window.FSUploader = FSUploader;
|
||||||
window.HashesManager = HashesManager;
|
window.HashesManager = HashesManager;
|
||||||
@@ -403,6 +404,31 @@ window.Submission = function(){
|
|||||||
}
|
}
|
||||||
|
|
||||||
e.target.submit();
|
e.target.submit();
|
||||||
|
},
|
||||||
|
|
||||||
|
async requestFeatured( entryId ){
|
||||||
|
const csrf = CSRF();
|
||||||
|
|
||||||
|
const response = await fetch(`/api/entry/${entryId}/featured`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-CSRF-Token': csrf
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const json = await response.json();
|
||||||
|
|
||||||
|
const entry_featured_button = document.querySelector('#entry-featured-button');
|
||||||
|
const entry_featured_body = document.querySelector('#entry-featured-body');
|
||||||
|
|
||||||
|
if( json.success ){
|
||||||
|
entry_featured_body.innerHTML = '<p>Request submitted</p>';
|
||||||
|
entry_featured_button.style.display = 'none';
|
||||||
|
} else {
|
||||||
|
entry_featured_body.innerHTML = '<p>Request failed. Please refresh the page and retry.</p>';
|
||||||
|
entry_featured_button.style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
40
resources/views/activity/featured-entries.blade.php
Normal file
40
resources/views/activity/featured-entries.blade.php
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
<?php /** @var \App\Models\Entry $entry */ ?>
|
||||||
|
@if($featuredEntries->isNotEmpty())
|
||||||
|
<section class="home-section">
|
||||||
|
<div class="home-section-header">
|
||||||
|
<h2 class="home-section-title">
|
||||||
|
<i data-lucide="star" size="16"></i>
|
||||||
|
Featured entries
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="featured-entries-grid">
|
||||||
|
@foreach($featuredEntries as $entry)
|
||||||
|
<a href="{{ route('entries.show', [ 'section' => $entry->type, 'entry' => $entry ]) }}" class="featured-entry-card">
|
||||||
|
<div class="featured-entry-cover">
|
||||||
|
@if($entry->main_image)
|
||||||
|
<img src="{{ Storage::url($entry->main_image) }}"
|
||||||
|
alt="{{ $entry->title }}"
|
||||||
|
loading="lazy">
|
||||||
|
@endif
|
||||||
|
<span class="featured-entry-star">
|
||||||
|
{{ \App\View\Components\EntryCard::ENTRY_TYPES_BADGE[ $entry->type ] }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="featured-entry-body">
|
||||||
|
@if(!empty($entry->getRealPlatform()))
|
||||||
|
<span class="featured-entry-platform">{{ $entry->getRealPlatform()->name }}</span>
|
||||||
|
@endif
|
||||||
|
<div class="featured-entry-title">{{ $entry->title }}</div>
|
||||||
|
<div class="featured-entry-meta">
|
||||||
|
@if($entry->user_id)
|
||||||
|
<x-xf-username-link :user-id="$entry->user_id"/>
|
||||||
|
@endif
|
||||||
|
· {{ $entry->featured_at->format('M Y') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
@endif
|
||||||
34
resources/views/activity/latest-news.blade.php
Normal file
34
resources/views/activity/latest-news.blade.php
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
<?php /** @var \App\Models\News $news */ ?>
|
||||||
|
<section class="home-section">
|
||||||
|
<div class="home-section-header">
|
||||||
|
<h2 class="home-section-title">
|
||||||
|
<i data-lucide="newspaper" size="16"></i>
|
||||||
|
Latest news
|
||||||
|
</h2>
|
||||||
|
<a href="{{ route('news.index') }}" class="home-section-more">
|
||||||
|
See all <i data-lucide="arrow-right" size="12"></i>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="news-strip">
|
||||||
|
@foreach($latestNews as $news)
|
||||||
|
<a href="{{ route('news.show', $news) }}" class="news-strip-card">
|
||||||
|
<div class="news-strip-cover"
|
||||||
|
@if($news->gallery->first())
|
||||||
|
style="background-image: url('{{ Storage::url($news->gallery()->first()->image) }}')"
|
||||||
|
@endif>
|
||||||
|
<span class="news-strip-date">
|
||||||
|
{{ $news->created_at->format('M j') }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="news-strip-body">
|
||||||
|
<span class="news-strip-badge">News</span>
|
||||||
|
<h3 class="news-strip-title">{{ $news->title }}</h3>
|
||||||
|
<span class="news-strip-meta">
|
||||||
|
{{ $news->created_at->diffForHumans() }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
103
resources/views/activity/timeline.blade.php
Normal file
103
resources/views/activity/timeline.blade.php
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
@php $currentDay = null; @endphp
|
||||||
|
<div class="activity-timeline" id="activity-timeline">
|
||||||
|
@forelse($items as $item)
|
||||||
|
|
||||||
|
@php
|
||||||
|
$day = $item->date->format('Y-m-d');
|
||||||
|
$dayLabel = $item->date->isToday() ? 'Today'
|
||||||
|
: ($item->date->isYesterday() ? 'Yesterday'
|
||||||
|
: $item->date->format('M d, Y'));
|
||||||
|
@endphp
|
||||||
|
|
||||||
|
@if($day !== $currentDay)
|
||||||
|
@php $currentDay = $day; @endphp
|
||||||
|
<div class="activity-day-sep">
|
||||||
|
<span class="activity-day-label">{{ $dayLabel }}</span>
|
||||||
|
<div class="activity-day-line"></div>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<div class="activity-tl-item" data-type="{{ $item->type }}">
|
||||||
|
|
||||||
|
<div class="activity-tl-left">
|
||||||
|
<div class="activity-tl-dot activity-tl-dot--{{ $item->type }}">
|
||||||
|
@if($item->type === 'entry')
|
||||||
|
<i data-lucide="database" size="14"></i>
|
||||||
|
@elseif($item->type === 'news')
|
||||||
|
<i data-lucide="newspaper" size="14"></i>
|
||||||
|
@elseif($item->type === 'message')
|
||||||
|
<i data-lucide="message-square" size="14"></i>
|
||||||
|
@elseif($item->type === 'thread')
|
||||||
|
<i data-lucide="messages-square" size="14"></i>
|
||||||
|
@elseif($item->type === 'club')
|
||||||
|
<i data-lucide="balloon" size="14"></i>
|
||||||
|
@else
|
||||||
|
<i data-lucide="target" size="14"></i>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
<div class="activity-tl-line"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a
|
||||||
|
href="{{ $item->url }}"
|
||||||
|
class="activity-tl-card"
|
||||||
|
>
|
||||||
|
@if( !empty($item->image) )
|
||||||
|
<div class="activity-tl-thumb activity-tl-thumb--{{ $item->type }}">
|
||||||
|
<img src="{{ $item->image }}" alt="{{ $item->title }}" loading="lazy">
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<div class="activity-tl-body">
|
||||||
|
<span class="activity-tl-badge activity-tl-badge--{{ $item->type }}">
|
||||||
|
{{ $item->badge }}
|
||||||
|
</span>
|
||||||
|
<div class="activity-tl-card-title">{{ $item->title }}</div>
|
||||||
|
@if(!empty($item->excerpt))
|
||||||
|
<div class="activity-tl-card-description">
|
||||||
|
{{ $item->excerpt }}
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
<div class="activity-tl-meta">
|
||||||
|
@if(!empty($item->user_id))
|
||||||
|
<span>
|
||||||
|
<i data-lucide="user" size="11"></i>
|
||||||
|
<x-xf-username-link :user-id="$item->user_id"/>
|
||||||
|
</span>
|
||||||
|
@elseif(!empty($item->author))
|
||||||
|
<span>
|
||||||
|
<i data-lucide="users" size="11"></i>
|
||||||
|
{{ $item->author }}
|
||||||
|
</span>
|
||||||
|
@endif
|
||||||
|
@if(!empty($item->meta))
|
||||||
|
<span>
|
||||||
|
<i data-lucide="monitor" size="11"></i>
|
||||||
|
{{ $item->meta }}
|
||||||
|
</span>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="activity-tl-time">
|
||||||
|
@if($item->date->isToday())
|
||||||
|
{{ $item->date->format('g:i A') }}
|
||||||
|
@elseif($item->date->isYesterday())
|
||||||
|
Yesterday {{ $item->date->format('g:i A') }}
|
||||||
|
@elseif($item->date->diffInDays() < 7)
|
||||||
|
{{ $item->date->format('D g:i A') }}
|
||||||
|
@else
|
||||||
|
{{ $item->date->format('M j, g:i A') }}
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@empty
|
||||||
|
<div class="activity-tl-empty">
|
||||||
|
<i data-lucide="inbox" size="36"></i>
|
||||||
|
<p>No recent activity.</p>
|
||||||
|
</div>
|
||||||
|
@endforelse
|
||||||
|
</div>
|
||||||
@@ -15,8 +15,14 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="form-gallery form-group level" style="flex:4;">
|
<div class="form-gallery form-group level" style="flex:4;">
|
||||||
<template x-for="(image,i) in images" :key="image.key">
|
<template x-for="(image,i) in images" :key="image.key">
|
||||||
<div class="gallery-item">
|
<div class="gallery-item" :class="{ 'gallery-item--dragging': dragSrcI === i }" draggable="true" @dragstart="dragStart(i)" @dragover="dragOver($event, i)" @dragend="dragEnd()">
|
||||||
<div class="form-image-preview-wrap">
|
<div class="form-image-preview-wrap">
|
||||||
|
<div class="gallery-drag-handle" title="Drag to reorder">
|
||||||
|
<i data-lucide="grip-vertical" size="14"></i>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<span class="gallery-order-badge" x-text="i + 1"></span>
|
||||||
|
|
||||||
<img :src="image.preview" :alt="image.name">
|
<img :src="image.preview" :alt="image.name">
|
||||||
<button type="button" class="form-image-remove" @click="handleRemoveFile(i)">
|
<button type="button" class="form-image-remove" @click="handleRemoveFile(i)">
|
||||||
X
|
X
|
||||||
|
|||||||
@@ -1,12 +1,7 @@
|
|||||||
<nav id="menu">
|
<nav id="menu">
|
||||||
|
|
||||||
<div class="menu-header">
|
<div class="menu-header">
|
||||||
<div class="menu-logo">
|
<img src="{{ asset('logo/plaza-logo-wide.png') }}">
|
||||||
RP
|
|
||||||
</div>
|
|
||||||
<div class="menu-title">
|
|
||||||
Romhack Plaza
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="menu-navigation">
|
<div class="menu-navigation">
|
||||||
@@ -15,7 +10,7 @@
|
|||||||
<div class="menu-group">
|
<div class="menu-group">
|
||||||
<div class="menu-group-title">{{ $menu['name'] }}</div>
|
<div class="menu-group-title">{{ $menu['name'] }}</div>
|
||||||
@foreach( $menu['items'] as $item )
|
@foreach( $menu['items'] as $item )
|
||||||
@if( !isset( $item['requires_auth'] ) || $item['requires_auth'] && !\Auth::guest() )
|
@if( !isset( $item['requires_auth'] ) || $item['requires_auth'] === true && !\Auth::guest() )
|
||||||
<a href="{{ isset($item['xf_route']) ? xfRoute($item['xf_route']) : route($item['route']) }}"
|
<a href="{{ isset($item['xf_route']) ? xfRoute($item['xf_route']) : route($item['route']) }}"
|
||||||
@class(['menu-item', 'active' => request()->routeIs( $item['route'] ?? '' )]) >
|
@class(['menu-item', 'active' => request()->routeIs( $item['route'] ?? '' )]) >
|
||||||
<i data-lucide="{{ $item['icon'] }}"></i><span>{{ $item['name'] }}</span>
|
<i data-lucide="{{ $item['icon'] }}"></i><span>{{ $item['name'] }}</span>
|
||||||
|
|||||||
41
resources/views/components/news-card.blade.php
Normal file
41
resources/views/components/news-card.blade.php
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
<a href="{{ route('news.show', $news->slug) }}" class="news-card">
|
||||||
|
<div class="news-card-cover"
|
||||||
|
style="background-image:
|
||||||
|
linear-gradient(rgba(0,0,0,0.1), rgba(20,20,20,0.9)),
|
||||||
|
url('{{ $news->gallery()->first() ? Storage::url($news->gallery()->first()->image) : '' }}')">
|
||||||
|
@if($news->state !== 'published')
|
||||||
|
<span class="news-card-state-badge">
|
||||||
|
@if($news->state === 'pending')
|
||||||
|
<i data-lucide="clock" size="11"></i> Pending
|
||||||
|
@elseif($news->state === 'draft')
|
||||||
|
<i data-lucide="scissors" size="11"></i> Draft
|
||||||
|
@elseif($news->state === 'hidden')
|
||||||
|
<i data-lucide="eye-off" size="11"></i> Hidden
|
||||||
|
@endif
|
||||||
|
</span>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
<div class="news-card-body">
|
||||||
|
<h2 class="news-card-title">{{ $news->title }}</h2>
|
||||||
|
@if($news->description)
|
||||||
|
<p class="news-card-excerpt">
|
||||||
|
{{ Str::limit(strip_tags($news->description), 100) }}
|
||||||
|
</p>
|
||||||
|
@endif
|
||||||
|
<div class="news-card-meta">
|
||||||
|
@if($news->user_id)
|
||||||
|
<span><i data-lucide="user" size="12"></i>
|
||||||
|
<x-xf-username-link :user-id="$news->user_id"/>
|
||||||
|
</span>
|
||||||
|
@endif
|
||||||
|
<span><i data-lucide="calendar" size="12"></i>
|
||||||
|
{{ $news->created_at->format('M d, Y') }}
|
||||||
|
</span>
|
||||||
|
@if($news->gallery->isNotEmpty())
|
||||||
|
<span><i data-lucide="images" size="12"></i>
|
||||||
|
{{ $news->gallery->count() }}
|
||||||
|
</span>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
13
resources/views/components/news-category-selector.blade.php
Normal file
13
resources/views/components/news-category-selector.blade.php
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<?php /** @var \App\Models\Category $category */ ?>
|
||||||
|
<div class="form-group">
|
||||||
|
<select class="form-select" name="category">
|
||||||
|
<option value="" disabled {{ empty($selected) ? 'selected' : '' }} hidde*>
|
||||||
|
Select a category...
|
||||||
|
</option>
|
||||||
|
@foreach($categories as $category)
|
||||||
|
<option value="{{ $category->id }}" {{ in_array($category->id, (array) $selected) ? 'selected' : '' }}>
|
||||||
|
{{ $category->name }}
|
||||||
|
</option>
|
||||||
|
@endforeach
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
@@ -32,25 +32,6 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="settings-separator"></div>
|
|
||||||
|
|
||||||
<div class="settings-section">
|
|
||||||
<div class="settings-section-title">
|
|
||||||
<i data-lucide="layout-grid" size="14"></i>
|
|
||||||
Entries per page
|
|
||||||
</div>
|
|
||||||
<div class="settings-perpage">
|
|
||||||
<template x-for="n in $store.settings.entriesPerPage" :key="n">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="settings-perpage-btn"
|
|
||||||
:class="{ 'active': $store.settings.currentEntriesPerPage == n }"
|
|
||||||
@click="$store.settings.entriesPerPageChanged(n)"
|
|
||||||
x-text="n"
|
|
||||||
></button>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="settings-separator"></div>
|
<div class="settings-separator"></div>
|
||||||
|
|
||||||
@auth
|
@auth
|
||||||
@@ -67,6 +48,4 @@
|
|||||||
</div>
|
</div>
|
||||||
@endauth
|
@endauth
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
nsfw: null,
|
nsfw: null,
|
||||||
state: '{{ old('submit-state', $defaultState) }}',
|
state: '{{ old('submit-state', $defaultState) }}',
|
||||||
deleteOpen: false,
|
deleteOpen: false,
|
||||||
|
featuredOpen: false,
|
||||||
init(){
|
init(){
|
||||||
this.$watch('nsfw', (val) => {
|
this.$watch('nsfw', (val) => {
|
||||||
if( val && this.state === 'published' ) {
|
if( val && this.state === 'published' ) {
|
||||||
@@ -11,6 +12,13 @@
|
|||||||
}
|
}
|
||||||
}" x-init="init()">
|
}" x-init="init()">
|
||||||
@if($isEdit)
|
@if($isEdit)
|
||||||
|
@if(!$news && $entry && !$entry->featured )
|
||||||
|
<div>
|
||||||
|
<button type="button" id="entry-featured-button" class="btn" @click="featuredOpen = true; $dispatch('modal:opened')">
|
||||||
|
<i data-lucide="star" size="13"></i> Featured
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
<div>
|
<div>
|
||||||
<button type="button" class="btn danger" @click="deleteOpen = true; $dispatch('modal:opened')">
|
<button type="button" class="btn danger" @click="deleteOpen = true; $dispatch('modal:opened')">
|
||||||
<i data-lucide="trash-2" size="13"></i> Delete
|
<i data-lucide="trash-2" size="13"></i> Delete
|
||||||
@@ -35,6 +43,46 @@
|
|||||||
</select>
|
</select>
|
||||||
|
|
||||||
@if($isEdit)
|
@if($isEdit)
|
||||||
|
@if(!$news && $entry && !$entry->featured )
|
||||||
|
<template x-teleport="body">
|
||||||
|
<div
|
||||||
|
class="modal-overlay"
|
||||||
|
x-cloak
|
||||||
|
x-show="featuredOpen"
|
||||||
|
x-transition.opacity
|
||||||
|
@click.self="featuredOpen = false"
|
||||||
|
@keydown.escape.window="featuredOpen = false"
|
||||||
|
@modal:opened.window="refreshIcons($el)"
|
||||||
|
>
|
||||||
|
<div class="modal-window" x-show="featuredOpen" x-transition>
|
||||||
|
<div class="modal-header">
|
||||||
|
<span class="modal-title">Request as featured</span>
|
||||||
|
<button type="button" class="modal-close" @click="featuredOpen = false">
|
||||||
|
<i data-lucide="x" size="20"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-body" id="entry-featured-body">
|
||||||
|
<p style="margin-bottom: 1.5rem; color: var(--text, #333);">
|
||||||
|
Please do not overuse this feature. Send only one request at a time.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="queue-mod-actions" id="entry-featured-actions">
|
||||||
|
<button type="button" class="btn" @click="featuredOpen = false">
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn" @click="Submission().requestFeatured({{ $entry->id }})">
|
||||||
|
<i data-lucide="star" size="14"></i>
|
||||||
|
Request as featured
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
@endif
|
||||||
<template x-teleport="body">
|
<template x-teleport="body">
|
||||||
<div
|
<div
|
||||||
class="modal-overlay"
|
class="modal-overlay"
|
||||||
@@ -58,7 +106,7 @@
|
|||||||
Are you sure you want to delete this entry? This action cannot be undone.
|
Are you sure you want to delete this entry? This action cannot be undone.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<form action="{{ route('submit.destroy', ['section' => $section, 'entry' => $entry ]) }}" method="POST">
|
<form action="{{ $news ? route('news.destroy', [ 'news' => $news ] ) : route('submit.destroy', ['section' => $section, 'entry' => $entry ]) }}" method="POST">
|
||||||
@csrf
|
@csrf
|
||||||
@method('DELETE')
|
@method('DELETE')
|
||||||
|
|
||||||
|
|||||||
@@ -46,7 +46,7 @@
|
|||||||
|
|
||||||
{{-- Users --}}
|
{{-- Users --}}
|
||||||
@can('create','\App\Models\Entry')
|
@can('create','\App\Models\Entry')
|
||||||
<a href="#" class="btn">
|
<a href="{{ route('submit.index') }}" class="btn">
|
||||||
<i data-lucide="hard-drive-upload" size="18"></i>
|
<i data-lucide="hard-drive-upload" size="18"></i>
|
||||||
</a>
|
</a>
|
||||||
@endcan
|
@endcan
|
||||||
@@ -78,7 +78,7 @@
|
|||||||
@include('components.conversations')
|
@include('components.conversations')
|
||||||
</div>
|
</div>
|
||||||
@endif
|
@endif
|
||||||
<div x-data style="position: relative;" x-init="$store.settings.xfUrls = { 'default': '{{ xfStyleVariationUrl( 'default' ) }}', 'alternate': '{{ xfStyleVariationUrl( 'alternate' ) }}' }">
|
<div x-data style="position: relative;" x-init="$store.settings.xfUrls = { 'default': '{{ xfStyleVariationUrl( 'default' ) }}', 'alternate': '{{ xfStyleVariationUrl( 'alternate' ) }}' }; $store.settings.currentTheme = '{{ userTheme() }}'">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="btn"
|
class="btn"
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
<div class="grid-c2" style="margin-top:1%;">
|
@php if( !isset($entry) && isset($news) ){ $entry = $news; $newsMode = true; } else { $newsMode = false; } @endphp
|
||||||
<div id="reviews-section">
|
<div class="{{ !$newsMode ? 'grid-c2' : '' }}" style="margin-top:1%;">
|
||||||
<div class="entry-content">
|
@if( !$newsMode )
|
||||||
<x-entry-section-title label="Reviews" icon="star" />
|
<div id="reviews-section">
|
||||||
|
<div class="entry-content">
|
||||||
|
<x-entry-section-title label="Reviews" icon="star" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
@endif
|
||||||
<div id="comments-section">
|
<div id="comments-section">
|
||||||
<div class="entry-content">
|
<div class="entry-content">
|
||||||
<x-entry-section-title label="Last comments" icon="message-circle" />
|
<x-entry-section-title label="Last comments" icon="message-circle" />
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
@section('page-title', "My Drafts - " . config('app.name'))
|
@section('page-title', "My Drafts - " . config('app.name'))
|
||||||
|
|
||||||
@section('content')
|
@section('content')
|
||||||
|
{{ \Diglactic\Breadcrumbs\Breadcrumbs::render() }}
|
||||||
<div class="page-title">
|
<div class="page-title">
|
||||||
My Drafts
|
My Drafts
|
||||||
</div>
|
</div>
|
||||||
@@ -16,6 +17,9 @@
|
|||||||
<span>{{ $drafts->total() }} draft{{ $drafts->total() > 1 ? 's' : '' }}</span>
|
<span>{{ $drafts->total() }} draft{{ $drafts->total() > 1 ? 's' : '' }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="drafts-list">
|
<div class="drafts-list">
|
||||||
|
@foreach($newsDrafts as $draft)
|
||||||
|
@include('news.draft_item', ['news' => $draft])
|
||||||
|
@endforeach
|
||||||
@foreach($drafts as $draft)
|
@foreach($drafts as $draft)
|
||||||
@include('entries.draft_item', ['entry' => $draft])
|
@include('entries.draft_item', ['entry' => $draft])
|
||||||
@endforeach
|
@endforeach
|
||||||
|
|||||||
@@ -3,5 +3,9 @@
|
|||||||
@section('page-title', "Database - " . config('app.name') )
|
@section('page-title', "Database - " . config('app.name') )
|
||||||
|
|
||||||
@section('content')
|
@section('content')
|
||||||
|
{{ \Diglactic\Breadcrumbs\Breadcrumbs::render() }}
|
||||||
|
<div class="page-title">
|
||||||
|
Database
|
||||||
|
</div>
|
||||||
@livewire('database')
|
@livewire('database')
|
||||||
@endsection
|
@endsection
|
||||||
|
|||||||
@@ -279,7 +279,7 @@
|
|||||||
|
|
||||||
<div class="video-thumbnail-wrapper"
|
<div class="video-thumbnail-wrapper"
|
||||||
@click="src = 'https://www.youtube.com/embed/{{ $entry->getYoutubeVideoId() }}?autoplay=1'; open = true">
|
@click="src = 'https://www.youtube.com/embed/{{ $entry->getYoutubeVideoId() }}?autoplay=1'; open = true">
|
||||||
<img src="https://img.youtube.com/vi/{{ $entry->youtube_id }}/maxresdefault.jpg">
|
<img src="https://img.youtube.com/vi/{{ $entry->getYoutubeVideoId() }}/maxresdefault.jpg">
|
||||||
<div class="play-trigger">
|
<div class="play-trigger">
|
||||||
<i data-lucide="play"></i>
|
<i data-lucide="play"></i>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,10 +3,28 @@
|
|||||||
@section('page-title', "Home - " . config('app.name') )
|
@section('page-title', "Home - " . config('app.name') )
|
||||||
|
|
||||||
@section('content')
|
@section('content')
|
||||||
<div class="block">
|
|
||||||
Ceci est un block !
|
|
||||||
</div>
|
|
||||||
<x-error-block error-type="page-not-allowed" />
|
|
||||||
|
|
||||||
<x-xf-username-link user-id="2" />
|
@include('activity.latest-news')
|
||||||
|
@include('activity.featured-entries')
|
||||||
|
<div class="activity-tl-header" x-data x-init="$store.settings.currentActivityFilters = {{ Js::from($activeFilters) }};">
|
||||||
|
<h2 class="activity-tl-title">
|
||||||
|
<i data-lucide="radio-tower" size="18"></i>
|
||||||
|
Latest activity
|
||||||
|
</h2>
|
||||||
|
<div class="activity-tl-filters">
|
||||||
|
@foreach($viewFilters as $key => $config)
|
||||||
|
<button
|
||||||
|
class="activity-tl-filter {{ in_array($key, $activeFilters) ? 'active' : '' }}"
|
||||||
|
data-filter="{{ $key }}"
|
||||||
|
x-data
|
||||||
|
@click="$store.settings.toggleActivityFilter('{{$key}}');$el.classList.toggle('active');">
|
||||||
|
<i data-lucide="{{ $config['icon'] }}" size="12"></i>
|
||||||
|
{{ $config['label'] }}
|
||||||
|
</button>
|
||||||
|
@endforeach
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@include('activity.timeline')
|
||||||
@endsection
|
@endsection
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en" class="{{ \Illuminate\Support\Facades\Cookie::get('theme', 'default') === 'alternate' ? 'light-mode' : '' }}">
|
<html lang="en" class="{{ userTheme() === 'alternate' ? 'light-mode' : '' }}">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user