A lot of things

This commit is contained in:
2026-06-16 16:21:43 +02:00
parent 4f9f6c63b3
commit 7e1e26f20b
126 changed files with 7917 additions and 204 deletions

View File

@@ -3,8 +3,10 @@
namespace App\Http\Controllers;
use App\Helpers\XenForoHelpers;
use App\Services\ActivityService;
use App\Services\XenforoApiService;
use App\Services\XenforoService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
@@ -63,4 +65,24 @@ class DynamicLoadController extends Controller
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(),
]);
}
}

View File

@@ -4,6 +4,7 @@ namespace App\Http\Controllers;
use App\Helpers\EntryHelpers;
use App\Models\Entry;
use App\Models\News;
use Illuminate\Support\Facades\Gate;
use Illuminate\Http\Request;
use Illuminate\View\View;
@@ -11,7 +12,7 @@ use Illuminate\View\View;
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
{
@@ -31,7 +32,8 @@ class EntryController extends Controller
if ($entry->type !== $section)
abort(404);
Gate::authorize('viewAny', $entry);
if( !\Auth::guest() )
Gate::authorize('viewAny', $entry);
// Permissions.
$entryPolicy = match ($entry->state) {
@@ -61,7 +63,12 @@ class EntryController extends Controller
->orderBy('updated_at', 'desc')
->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'));
}
}

View 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,
]);
}
}

View File

@@ -4,6 +4,7 @@ namespace App\Http\Controllers;
use App\Helpers\EntryHelpers;
use App\Models\EntryFile;
use App\Models\LogXfUser;
use App\Services\FileServersService;
use Illuminate\Http\Client\ConnectionException;
use Illuminate\Http\JsonResponse;
@@ -45,7 +46,7 @@ class FileServerController extends Controller {
return response()->json($data);
}
\Cache::put("uploaded_file_{$fileUuid}", [
$fileData = [
'uuid' => $fileUuid,
'type' => $type,
'filename' => $filename,
@@ -54,7 +55,16 @@ class FileServerController extends Controller {
'favorite_server' => $data['favorite_server'],
'favorite_at' => time(),
'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;
return response()->json($data);
@@ -67,7 +77,11 @@ class FileServerController extends Controller {
abort(404);
}
// TODO: DL Count.
if( !EntryHelpers::fileAlreadyDownloaded($file) ) {
EntryHelpers::markFileAsDownloaded($file);
$file->increaseDownloadCount();
}
return redirect( $this->fs->getDownloadFileUrl( $file) );
}
}

View File

@@ -2,14 +2,42 @@
namespace App\Http\Controllers;
use App\Models\Entry;
use App\Services\ActivityService;
use Illuminate\Http\Request;
use Illuminate\View\View;
use App\Models\News;
class HomeController extends Controller
{
public function index(): View {
return view('home');
public function __construct( private ActivityService $service ) {}
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'));
}
}

View File

@@ -75,4 +75,9 @@ class ModCPController extends Controller
$entry->forceDelete();
return back()->with('success', "Entry permanently deleted");
}
public function logs()
{
return view('modcp.logs');
}
}

View 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.");
}
}

View File

@@ -4,6 +4,7 @@ namespace App\Http\Controllers;
use App\Helpers\XenForoHelpers;
use App\Models\Entry;
use App\Models\News;
use App\Services\XenforoService;
use Illuminate\Http\Request;
class QueueController extends Controller
@@ -14,7 +15,25 @@ class QueueController extends Controller
->with(['authors', 'game.platform'])
->orderByRaw("CASE WHEN state = 'pending' THEN 1 ELSE 0 END")
->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'));
}
@@ -25,24 +44,53 @@ class QueueController extends Controller
$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');
}
public function approve(Request $request, Entry $entry)
{
// $entry->update(['state' => 'published']);
$entry->update(['state' => 'published', 'created_at' => now()]);
XenForoHelpers::entryApproved($entry);
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)
{
$request->validate(['reason' => 'nullable|string|max:2000']);
$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');
}

View File

@@ -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.");
}
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.");
}
}

View File

@@ -33,6 +33,62 @@ class SubmissionController extends Controller
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)
{
$data = [
@@ -108,7 +164,7 @@ class SubmissionController extends Controller
if( $entry->type !== $section )
abort(404);
if( $entry->comments_thread_id)
if( $entry->comments_thread_id )
DeleteXenForoCommentsThread::dispatch( $entry->comments_thread_id );
$entry->delete();

View File

@@ -2,6 +2,7 @@
namespace App\Http\Controllers;
use App\Models\Entry;
use App\Models\EntryFile;
use App\Services\FileServersService;
use Illuminate\Http\Request;
@@ -15,9 +16,29 @@ class ToolsController extends Controller
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);
}
@@ -28,7 +49,11 @@ class ToolsController extends Controller
'name' => $file->entry->title,
'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'));
}
}

View File

@@ -149,6 +149,9 @@ class StoreEntryRequest extends FormRequest
$rules['files_metadata'] = 'array|nullable';
$rules['files_metadata.*.online_patcher'] = 'nullable|boolean';
$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') ) ){

View 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;
}
}

View 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;
}
}