From 4f9f6c63b3427067d9bfe44abb8c89a18028bf1c Mon Sep 17 00:00:00 2001 From: Benjamin Date: Wed, 10 Jun 2026 11:04:26 +0200 Subject: [PATCH 1/4] Update Submissions and add fields --- .../Resources/Categories/CategoryResource.php | 48 +++ .../Categories/Pages/CreateCategory.php | 11 + .../Categories/Pages/EditCategory.php | 19 ++ .../Categories/Pages/ListCategories.php | 19 ++ .../Categories/Schemas/CategoryForm.php | 24 ++ .../Categories/Tables/CategoriesTable.php | 42 +++ .../Resources/Levels/LevelResource.php | 48 +++ .../Resources/Levels/Pages/CreateLevel.php | 11 + .../Resources/Levels/Pages/EditLevel.php | 19 ++ .../Resources/Levels/Pages/ListLevels.php | 19 ++ .../Resources/Levels/Schemas/LevelForm.php | 20 ++ .../Resources/Levels/Tables/LevelsTable.php | 42 +++ .../Resources/Systems/Pages/CreateSystem.php | 11 + .../Resources/Systems/Pages/EditSystem.php | 19 ++ .../Resources/Systems/Pages/ListSystems.php | 19 ++ .../Resources/Systems/Schemas/SystemForm.php | 20 ++ .../Resources/Systems/SystemResource.php | 48 +++ .../Resources/Systems/Tables/SystemsTable.php | 42 +++ app/Helpers/FormHelpers.php | 59 +++- app/Http/Controllers/SubmissionController.php | 28 +- app/Http/Requests/StoreEntryRequest.php | 37 ++- app/Livewire/Database.php | 74 ++++- app/Livewire/GameSelector.php | 72 ++++- app/Models/Author.php | 22 ++ app/Models/Category.php | 33 ++ app/Models/Entry.php | 102 +++++- app/Models/EntryFile.php | 33 ++ app/Models/EntryGallery.php | 11 +- app/Models/EntryHash.php | 23 ++ app/Models/Gallery.php | 34 ++ app/Models/Game.php | 24 ++ app/Models/Genre.php | 18 ++ app/Models/Language.php | 18 ++ app/Models/Level.php | 26 ++ app/Models/Modification.php | 16 + app/Models/Platform.php | 22 ++ app/Models/Status.php | 16 + app/Models/System.php | 26 ++ app/Models/User.php | 25 ++ app/Services/SubmissionsService.php | 149 +++++++-- app/Traits/HasGallery.php | 14 + app/View/Components/CategorySelector.php | 41 +++ composer.json | 1 + composer.lock | 301 +++++++++++++++++- ...026_06_09_122425_create_category_table.php | 29 ++ .../2026_06_09_122655_create_os_table.php | 29 ++ .../2026_06_09_122817_create_level_table.php | 29 ++ ..._06_09_123242_add_entry_level_id_field.php | 28 ++ ...6_09_123458_create_entry_systems_table.php | 28 ++ ...9_123533_create_entry_categories_table.php | 28 ++ ...832_add_restricted_field_to_categories.php | 28 ++ ...84936_make_entry_galleries_polymorphic.php | 40 +++ .../2026_06_10_090320_create_news_table.php | 27 ++ extra.less | 2 +- resources/css/components/common.css | 5 + resources/css/components/forms.css | 41 +++ resources/js/submissions.js | 18 +- .../components/category-selector.blade.php | 29 ++ .../views/components/entry-card.blade.php | 19 +- resources/views/entries/show.blade.php | 99 ++++-- resources/views/livewire/database.blade.php | 9 + .../views/livewire/game-selector.blade.php | 168 ++++++---- resources/views/submissions/form.blade.php | 76 +++-- routes/web.php | 14 +- 64 files changed, 2278 insertions(+), 174 deletions(-) create mode 100644 app/Filament/Resources/Categories/CategoryResource.php create mode 100644 app/Filament/Resources/Categories/Pages/CreateCategory.php create mode 100644 app/Filament/Resources/Categories/Pages/EditCategory.php create mode 100644 app/Filament/Resources/Categories/Pages/ListCategories.php create mode 100644 app/Filament/Resources/Categories/Schemas/CategoryForm.php create mode 100644 app/Filament/Resources/Categories/Tables/CategoriesTable.php create mode 100644 app/Filament/Resources/Levels/LevelResource.php create mode 100644 app/Filament/Resources/Levels/Pages/CreateLevel.php create mode 100644 app/Filament/Resources/Levels/Pages/EditLevel.php create mode 100644 app/Filament/Resources/Levels/Pages/ListLevels.php create mode 100644 app/Filament/Resources/Levels/Schemas/LevelForm.php create mode 100644 app/Filament/Resources/Levels/Tables/LevelsTable.php create mode 100644 app/Filament/Resources/Systems/Pages/CreateSystem.php create mode 100644 app/Filament/Resources/Systems/Pages/EditSystem.php create mode 100644 app/Filament/Resources/Systems/Pages/ListSystems.php create mode 100644 app/Filament/Resources/Systems/Schemas/SystemForm.php create mode 100644 app/Filament/Resources/Systems/SystemResource.php create mode 100644 app/Filament/Resources/Systems/Tables/SystemsTable.php create mode 100644 app/Models/Category.php create mode 100644 app/Models/Gallery.php create mode 100644 app/Models/Level.php create mode 100644 app/Models/System.php create mode 100644 app/Traits/HasGallery.php create mode 100644 app/View/Components/CategorySelector.php create mode 100644 database/migrations/2026_06_09_122425_create_category_table.php create mode 100644 database/migrations/2026_06_09_122655_create_os_table.php create mode 100644 database/migrations/2026_06_09_122817_create_level_table.php create mode 100644 database/migrations/2026_06_09_123242_add_entry_level_id_field.php create mode 100644 database/migrations/2026_06_09_123458_create_entry_systems_table.php create mode 100644 database/migrations/2026_06_09_123533_create_entry_categories_table.php create mode 100644 database/migrations/2026_06_09_124832_add_restricted_field_to_categories.php create mode 100644 database/migrations/2026_06_10_084936_make_entry_galleries_polymorphic.php create mode 100644 database/migrations/2026_06_10_090320_create_news_table.php create mode 100644 resources/views/components/category-selector.blade.php diff --git a/app/Filament/Resources/Categories/CategoryResource.php b/app/Filament/Resources/Categories/CategoryResource.php new file mode 100644 index 0000000..2b6fa11 --- /dev/null +++ b/app/Filament/Resources/Categories/CategoryResource.php @@ -0,0 +1,48 @@ + ListCategories::route('/'), + 'create' => CreateCategory::route('/create'), + 'edit' => EditCategory::route('/{record}/edit'), + ]; + } +} diff --git a/app/Filament/Resources/Categories/Pages/CreateCategory.php b/app/Filament/Resources/Categories/Pages/CreateCategory.php new file mode 100644 index 0000000..a842af9 --- /dev/null +++ b/app/Filament/Resources/Categories/Pages/CreateCategory.php @@ -0,0 +1,11 @@ +components([ + TextInput::make('name') + ->required(), + TextInput::make('slug') + ->required(), + Textarea::make('restricted_to') + ->default(null) + ->columnSpanFull(), + ]); + } +} diff --git a/app/Filament/Resources/Categories/Tables/CategoriesTable.php b/app/Filament/Resources/Categories/Tables/CategoriesTable.php new file mode 100644 index 0000000..346d3a9 --- /dev/null +++ b/app/Filament/Resources/Categories/Tables/CategoriesTable.php @@ -0,0 +1,42 @@ +columns([ + TextColumn::make('name') + ->searchable(), + TextColumn::make('slug') + ->searchable(), + TextColumn::make('created_at') + ->dateTime() + ->sortable() + ->toggleable(isToggledHiddenByDefault: true), + TextColumn::make('updated_at') + ->dateTime() + ->sortable() + ->toggleable(isToggledHiddenByDefault: true), + ]) + ->filters([ + // + ]) + ->recordActions([ + EditAction::make(), + ]) + ->toolbarActions([ + BulkActionGroup::make([ + DeleteBulkAction::make(), + ]), + ]); + } +} diff --git a/app/Filament/Resources/Levels/LevelResource.php b/app/Filament/Resources/Levels/LevelResource.php new file mode 100644 index 0000000..c2bb67d --- /dev/null +++ b/app/Filament/Resources/Levels/LevelResource.php @@ -0,0 +1,48 @@ + ListLevels::route('/'), + 'create' => CreateLevel::route('/create'), + 'edit' => EditLevel::route('/{record}/edit'), + ]; + } +} diff --git a/app/Filament/Resources/Levels/Pages/CreateLevel.php b/app/Filament/Resources/Levels/Pages/CreateLevel.php new file mode 100644 index 0000000..e8cfbde --- /dev/null +++ b/app/Filament/Resources/Levels/Pages/CreateLevel.php @@ -0,0 +1,11 @@ +components([ + TextInput::make('name') + ->required(), + TextInput::make('slug') + ->required(), + ]); + } +} diff --git a/app/Filament/Resources/Levels/Tables/LevelsTable.php b/app/Filament/Resources/Levels/Tables/LevelsTable.php new file mode 100644 index 0000000..d64fd20 --- /dev/null +++ b/app/Filament/Resources/Levels/Tables/LevelsTable.php @@ -0,0 +1,42 @@ +columns([ + TextColumn::make('name') + ->searchable(), + TextColumn::make('slug') + ->searchable(), + TextColumn::make('created_at') + ->dateTime() + ->sortable() + ->toggleable(isToggledHiddenByDefault: true), + TextColumn::make('updated_at') + ->dateTime() + ->sortable() + ->toggleable(isToggledHiddenByDefault: true), + ]) + ->filters([ + // + ]) + ->recordActions([ + EditAction::make(), + ]) + ->toolbarActions([ + BulkActionGroup::make([ + DeleteBulkAction::make(), + ]), + ]); + } +} diff --git a/app/Filament/Resources/Systems/Pages/CreateSystem.php b/app/Filament/Resources/Systems/Pages/CreateSystem.php new file mode 100644 index 0000000..c03c9fd --- /dev/null +++ b/app/Filament/Resources/Systems/Pages/CreateSystem.php @@ -0,0 +1,11 @@ +components([ + TextInput::make('name') + ->required(), + TextInput::make('slug') + ->required(), + ]); + } +} diff --git a/app/Filament/Resources/Systems/SystemResource.php b/app/Filament/Resources/Systems/SystemResource.php new file mode 100644 index 0000000..14aead4 --- /dev/null +++ b/app/Filament/Resources/Systems/SystemResource.php @@ -0,0 +1,48 @@ + ListSystems::route('/'), + 'create' => CreateSystem::route('/create'), + 'edit' => EditSystem::route('/{record}/edit'), + ]; + } +} diff --git a/app/Filament/Resources/Systems/Tables/SystemsTable.php b/app/Filament/Resources/Systems/Tables/SystemsTable.php new file mode 100644 index 0000000..822e32e --- /dev/null +++ b/app/Filament/Resources/Systems/Tables/SystemsTable.php @@ -0,0 +1,42 @@ +columns([ + TextColumn::make('name') + ->searchable(), + TextColumn::make('slug') + ->searchable(), + TextColumn::make('created_at') + ->dateTime() + ->sortable() + ->toggleable(isToggledHiddenByDefault: true), + TextColumn::make('updated_at') + ->dateTime() + ->sortable() + ->toggleable(isToggledHiddenByDefault: true), + ]) + ->filters([ + // + ]) + ->recordActions([ + EditAction::make(), + ]) + ->toolbarActions([ + BulkActionGroup::make([ + DeleteBulkAction::make(), + ]), + ]); + } +} diff --git a/app/Helpers/FormHelpers.php b/app/Helpers/FormHelpers.php index b8aac16..efba8c8 100644 --- a/app/Helpers/FormHelpers.php +++ b/app/Helpers/FormHelpers.php @@ -44,7 +44,64 @@ class FormHelpers { 'homebrew' => [ 'page_title' => "Submit an homebrew", 'about_the' => "About the homebrew", - 'version' => "Patch version", + 'version' => "Version", + 'status' => "Status", + 'release_date' => "Release date", + 'release_date_helper' => "If only initial release exist, the release date.", + 'description' => "Description", + 'about_game' => "Game Information", + 'attachments' => "Attachments", + 'authors' => "Team members", + 'related_links' => "Related links", + 'release_site' => "Release site", + 'release_site_helper' => "Project entry on site/blog/forum/Github.", + 'youtube_video' => "YouTube video", + ], + 'utilities' => [ + 'page_title' => "Submit a utility", + 'entry_title' => "Title", + 'about_the' => "About the utility", + 'version' => "Version", + 'status' => "Status", + 'system' => "OS", + 'categories' => "Categories", + 'level' => "Experience Level", + 'release_date' => "Release date", + 'release_date_helper' => "If only initial release exist, the release date.", + 'description' => "Description", + 'about_game' => "Game Information", + 'attachments' => "Attachments", + 'authors' => "Team members", + 'related_links' => "Related links", + 'release_site' => "Release site", + 'release_site_helper' => "Project entry on site/blog/forum/Github.", + 'youtube_video' => "YouTube video", + ], + 'documents' => [ + 'page_title' => "Submit a document", + 'entry_title' => "Title", + 'about_the' => "About the document", + 'version' => "Version", + 'status' => "Status", + 'categories' => "Categories", + 'level' => "Experience Level", + 'release_date' => "Release date", + 'release_date_helper' => "If only initial release exist, the release date.", + 'description' => "Description", + 'about_game' => "Game Information", + 'attachments' => "Attachments", + 'authors' => "Team members", + 'related_links' => "Related links", + 'release_site' => "Release site", + 'release_site_helper' => "Project entry on site/blog/forum/Github.", + 'youtube_video' => "YouTube video", + ], + 'lua-scripts' => [ + 'page_title' => "Submit a LUA Script", + 'about_the' => "About the script", + 'entry_title' => "Title", + 'type_of_hack' => "Modifications", + 'version' => "Version", 'status' => "Status", 'release_date' => "Release date", 'release_date_helper' => "If only initial release exist, the release date.", diff --git a/app/Http/Controllers/SubmissionController.php b/app/Http/Controllers/SubmissionController.php index 2f1f7f1..5d8b4c5 100644 --- a/app/Http/Controllers/SubmissionController.php +++ b/app/Http/Controllers/SubmissionController.php @@ -10,14 +10,16 @@ use App\Jobs\DeleteXenForoCommentsThread; use App\Models\Author; use App\Models\Entry; use App\Models\EntryFile; -use App\Models\EntryGallery; +use App\Models\Gallery; use App\Models\EntryHash; use App\Models\Game; use App\Models\Genre; use App\Models\Language; +use App\Models\Level; use App\Models\Modification; use App\Models\Platform; use App\Models\Status; +use App\Models\System; use App\Services\SubmissionsService; use App\Services\XenforoApiService; use Illuminate\Http\Request; @@ -39,19 +41,27 @@ class SubmissionController extends Controller 'words' => FormHelpers::getEntryFormWords($section), 'isEdit' => false, 'oldModifications' => old( 'modifications', [] ), + 'oldSystems' => old( 'systems', [] ), 'oldLanguages' => old( 'languages', [] ), + 'oldCategories' => old( 'categories', [] ), 'oldFilesArray' => $this->services->prepareOldFiles( null ) ]; if( $data['words'] === [] ) abort(500); - if( section_must_be( 'romhacks', $section ) ){ + if( section_must_be( ['romhacks', 'lua-scripts'], $section ) ){ $data['modifications'] = Modification::orderBy('name')->get(); } - if( section_must_be( [ 'romhacks', 'translations', 'homebrew' ], $section ) ){ + if( section_must_be( [ 'romhacks', 'translations', 'homebrew', 'lua-scripts' ], $section ) ){ $data['statuses'] = Status::orderBy('id')->get(); } + if( section_must_be( 'utilities' , $section ) ){ + $data['systems'] = System::orderBy('name')->get(); + } + if( section_must_be( [ 'utilities', 'documents' ], $section ) ) { + $data['levels'] = Level::orderBy('id')->get(); + } return view('submissions.create', $data); } @@ -68,19 +78,27 @@ class SubmissionController extends Controller 'words' => FormHelpers::getEntryFormWords($section), 'isEdit' => true, 'oldModifications' => old('modifications', $entry->modifications->pluck('id')->toArray() ?? [] ), + 'oldSystems' => old( 'systems', $entry->systems->pluck('id')->toArray() ?? [] ), 'oldLanguages' => old('languages', $entry->languages->pluck('id')->toArray() ?? [] ), + 'oldCategories' => old('categories', $entry->categories->pluck('id')->toArray() ?? [] ), 'oldFilesArray' => $this->services->prepareOldFiles( $entry ) ]; if( $data['words'] === [] ) abort(500); - if( section_must_be( 'romhacks', $section ) ){ + if( section_must_be( [ 'romhacks', 'lua-scripts' ], $section ) ){ $data['modifications'] = Modification::orderBy('name')->get(); } - if( section_must_be( [ 'romhacks', 'translations' ], $section ) ){ + if( section_must_be( [ 'romhacks', 'translations', 'homebrew', 'lua-scripts' ], $section ) ){ $data['statuses'] = Status::orderBy('id')->get(); } + if( section_must_be( 'utilities' , $section ) ){ + $data['systems'] = System::orderBy('name')->get(); + } + if( section_must_be( [ 'utilities', 'documents' ], $section ) ) { + $data['levels'] = Level::orderBy('id')->get(); + } return view('submissions.edit', $data); } diff --git a/app/Http/Requests/StoreEntryRequest.php b/app/Http/Requests/StoreEntryRequest.php index 10b73ef..5884b85 100644 --- a/app/Http/Requests/StoreEntryRequest.php +++ b/app/Http/Requests/StoreEntryRequest.php @@ -70,28 +70,49 @@ class StoreEntryRequest extends FormRequest $rules['entry_title'] = "nullable|string|max:255"; } - if( section_must_be( 'romhacks', $section ) ){ + if( section_must_be( ['romhacks', 'lua-scripts'], $section ) ){ $rules['modifications'] = 'array|required|min:1'; $rules['modifications.*'] = 'integer|exists:modifications,id'; + } else if( section_must_be( 'utilities', $section ) ){ + $rules['categories'] = 'array|required|min:1'; + $rules['categories.*'] = 'integer|exists:categories,id'; + } + + if( section_must_be( 'utilities', $section ) ){ + $rules['systems'] = 'array|required|min:1'; + $rules['systems.*'] = 'integer|exists:systems,id'; } $rules['version'] = 'required|string|max:50'; $rules['release-date'] = 'required|date'; - $rules['status'] = 'required|integer|exists:statuses,id'; + if( section_must_not_be( 'utilities', $section ) ){ + $rules['status'] = 'required|integer|exists:statuses,id'; + } else { + $rules['level'] = 'required|integer|exists:levels,id'; + } $rules['description'] = 'required|string'; - if( section_must_be( ['romhacks', 'translations' ], $section ) ){ + $rules['game_selection_mode'] = 'required|string|in:game,platform,none'; + $gameSelectionMode = $this->input('game_selection_mode') !== '' ? $this->input('game_selection_mode') : 'game'; + + if( $gameSelectionMode === 'none' ){ + // ... + } else if( $gameSelectionMode === 'platform' ){ + $rules['platform_only_id'] = 'required|integer|exists:platforms,id'; + } else { $rules['game_id'] = 'required_without:new-game-title|nullable|integer|exists:games,id'; $rules['new-game-title'] = 'required_without:game_id|nullable|string|max:255'; $rules['new-game-platform'] = 'required_with:new-game-title|nullable|integer|exists:platforms,id'; $rules['new-game-genre'] = 'required_with:new-game-title|integer|nullable|exists:genres,id'; } - $rules['hashes'] = 'array|required|min:1'; - $rules['hashes.*.filename'] = 'required|string|max:512'; - $rules['hashes.*.hash_crc32'] = 'required|string|max:512'; - $rules['hashes.*.hash_sha1'] = 'required|string|max:512'; - $rules['hashes.*.verified'] = 'required|string|max:512'; + if( section_must_be( ['translations', 'romhacks'], $section ) ){ + $rules['hashes'] = 'array|required|min:1'; + $rules['hashes.*.filename'] = 'required|string|max:512'; + $rules['hashes.*.hash_crc32'] = 'required|string|max:512'; + $rules['hashes.*.hash_sha1'] = 'required|string|max:512'; + $rules['hashes.*.verified'] = 'required|string|max:512'; + } $rules['languages'] = 'array|required|min:1'; $rules['languages.*'] = 'integer|exists:languages,id'; diff --git a/app/Livewire/Database.php b/app/Livewire/Database.php index fa9d1c9..da4a926 100644 --- a/app/Livewire/Database.php +++ b/app/Livewire/Database.php @@ -3,13 +3,16 @@ namespace App\Livewire; use App\Models\Author; +use App\Models\Category; use App\Models\Entry; use App\Models\Game; use App\Models\Genre; use App\Models\Language; +use App\Models\Level; use App\Models\Modification; use App\Models\Platform; use App\Models\Status; +use App\Models\System; use Livewire\Attributes\Url; use Livewire\Component; use Livewire\WithPagination; @@ -114,6 +117,41 @@ class Database extends Component #[Url(except:'or')] public string $modificationsMode = 'or'; + /** + * Categories IDs filter. + * @var array + */ + #[Url(except:[])] + public array $categories = []; + + /** + * Categories mode and/or + * @var string + */ + #[Url(except:['or'])] + public string $categoriesMode = 'or'; + + /** + * Systems IDs filter. + * @var array + */ + #[Url(except:[])] + public array $systems = []; + + /** + * Systems mode and/or + * @var string + */ + #[Url(except:['or'])] + public string $systemsMode = 'or'; + + /** + * Levels IDs filter. + * @var array + */ + #[Url(except:[])] + public array $levels = []; + /** * Sort by field. * @var string @@ -164,11 +202,16 @@ class Database extends Component public function updatedLanguagesMode(): void { $this->resetPage(); $this->dispatch('filters-updated'); } public function updatedModifications(): void { $this->resetPage(); $this->dispatch('filters-updated'); } public function updatedModificationsMode(): 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 updatedSystems(): void { $this->resetPage(); $this->dispatch('filters-updated'); } + public function updatedSystemsMode(): void { $this->resetPage(); $this->dispatch('filters-updated'); } + public function updatedLevels(): void { $this->resetPage(); $this->dispatch('filters-updated'); } public function clearFilters(): void { $this->reset([ - 'search', 'types', 'platforms', 'genres', 'statuses', 'authors', 'authorsMode', 'languages', 'languagesMode', 'modifications', 'modificationsMode' + 'search', 'types', 'platforms', 'genres', 'statuses', 'authors', 'authorsMode', 'languages', 'languagesMode', 'modifications', 'modificationsMode', 'categories', 'categoriesMode', 'systems', 'systemsMode', 'levels' ]); $this->resetPage(); } @@ -188,7 +231,7 @@ class Database extends Component private function buildQuery() { $query = Entry::query()->published()->with([ - 'game.platform', 'game.genre', 'status', 'authors', 'languages' + 'game.platform', 'game.genre', 'status', 'authors', 'languages', 'level', 'systems', 'categories', 'modifications' ]); if( $this->search ) { @@ -253,6 +296,30 @@ class Database extends Component } } + if( $this->levels ) { + $query->whereIn('level_id', $this->levels); + } + + if( $this->categories ) { + if( $this->categoriesMode === 'and' ) { + foreach ( $this->categories as $categoryId ) { + $query->whereHas('categories', fn($q) => $q->where('categories.id', $categoryId)); + } + } else { + $query->whereHas('categories', fn($q) => $q->whereIn('categories.id', $this->categories)); + } + } + + if( $this->systems ) { + if( $this->systemsMode === 'and' ) { + foreach ( $this->systems as $systemId ) { + $query->whereHas('systems', fn($q) => $q->where('systems.id', $systemId)); + } + } else { + $query->whereHas('systems', fn($q) => $q->whereIn('systems.id', $this->systems)); + } + } + return $query->orderBy($this->sortBy, $this->sortDir); } @@ -267,6 +334,9 @@ class Database extends Component 'allAuthors' => Author::orderBy('name')->get(), 'allLanguages' => Language::orderBy('name')->get(), 'allModifications' => Modification::orderBy('name')->get(), + 'allCategories' => Category::orderBy('name')->get(), + 'allLevels' => Level::orderBy('name')->get(), + 'allSystems' => System::orderBy('name')->get(), ]); } } diff --git a/app/Livewire/GameSelector.php b/app/Livewire/GameSelector.php index 84b0b5b..2446d77 100644 --- a/app/Livewire/GameSelector.php +++ b/app/Livewire/GameSelector.php @@ -16,6 +16,17 @@ class GameSelector extends Component public const int REQUIRED_CHARS = 3; + /** + * Which section we can change selection mode. + */ + public const array CHANGE_SECTION_MODE = [ 'utilities', 'documents' ]; + + /** + * Selection mode between game|platform|none. + * @var string + */ + public string $selectionMode = 'game'; + /** * If we are in new game mode. * @var bool @@ -70,9 +81,25 @@ class GameSelector extends Component */ public bool $dropdown = false; - public function mount( ?int $gameId = null, ?string $newGameTitle = null, ?int $newGamePlatform = null, ?int $newGameGenre = null ): void + /** + * In platform mode. + * @var int|null + */ + public ?int $platformModeId = null; + + /** + * In platform mode. + * @var string|null + */ + public ?string $platformModeName = null; + + public ?string $section = null; + + public function mount( ?int $gameId = null, ?string $newGameTitle = null, ?int $newGamePlatform = null, ?int $newGameGenre = null, ?string $section, ?int $platformOnlyId ): void { + $this->section = $section; + // If we selected an existent game. if( $gameId ){ $game = Game::with(['platform','genre'])->find($gameId); @@ -93,6 +120,36 @@ class GameSelector extends Component $this->gamePlatformId = is_numeric($newGamePlatform) ? (int) $newGamePlatform : null; $this->gameGenreId = is_numeric($newGameGenre) ? (int) $newGameGenre : null; } + + if( in_array( $section, self::CHANGE_SECTION_MODE ) ) { + if ($platformOnlyId) { + $this->selectionMode = 'platform'; + $this->platformModeId = $platformOnlyId; + $platform = Platform::find($platformOnlyId); + if ($platform) { + $this->platformModeName = $platform->name; + } else { + $this->platformModeId = null; + } + } + } + } + + public function setSelectionMode(string $mode): void + { + if( !in_array( $this->section, self::CHANGE_SECTION_MODE ) ) + return; + + $this->selectionMode = $mode; + + if( $mode !== 'game' ){ + $this->clearGame(); + $this->newGame = false; + } + if( $mode !== 'platform' ){ + $this->platformModeId = null; + $this->platformModeName = null; + } } /** @@ -134,6 +191,15 @@ class GameSelector extends Component } + public function selectPlatformOnly( int $id ): void + { + $platform = Platform::find($id); + if( $platform ){ + $this->platformModeId = $platform->id; + $this->platformModeName = $platform->name; + } + } + /** * Clear existent game selection. * @return void @@ -179,6 +245,10 @@ class GameSelector extends Component } $data['hasOldNewGame'] = old('new-game-title') || old('new-game-platform') || old('new-game-genre'); + $data['canChangeSelection'] = in_array( $this->section, self::CHANGE_SECTION_MODE ); + if( in_array( $this->section, self::CHANGE_SECTION_MODE ) ) + $data['platforms'] = Platform::orderBy('name')->get(); + return view('livewire.game-selector', $data ); } } diff --git a/app/Models/Author.php b/app/Models/Author.php index 357367d..56eacce 100644 --- a/app/Models/Author.php +++ b/app/Models/Author.php @@ -8,6 +8,28 @@ use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Database\Eloquent\Relations\HasMany; +/** + * @property int $id + * @property string $name + * @property string $slug + * @property string|null $website + * @property int|null $user_id + * @property \Illuminate\Support\Carbon|null $created_at + * @property \Illuminate\Support\Carbon|null $updated_at + * @property-read \Illuminate\Database\Eloquent\Collection $entries + * @property-read int|null $entries_count + * @method static \Illuminate\Database\Eloquent\Builder|Author newModelQuery() + * @method static \Illuminate\Database\Eloquent\Builder|Author newQuery() + * @method static \Illuminate\Database\Eloquent\Builder|Author query() + * @method static \Illuminate\Database\Eloquent\Builder|Author whereCreatedAt($value) + * @method static \Illuminate\Database\Eloquent\Builder|Author whereId($value) + * @method static \Illuminate\Database\Eloquent\Builder|Author whereName($value) + * @method static \Illuminate\Database\Eloquent\Builder|Author whereSlug($value) + * @method static \Illuminate\Database\Eloquent\Builder|Author whereUpdatedAt($value) + * @method static \Illuminate\Database\Eloquent\Builder|Author whereUserId($value) + * @method static \Illuminate\Database\Eloquent\Builder|Author whereWebsite($value) + * @mixin \Eloquent + */ class Author extends Model { protected $fillable = [ diff --git a/app/Models/Category.php b/app/Models/Category.php new file mode 100644 index 0000000..69e3e41 --- /dev/null +++ b/app/Models/Category.php @@ -0,0 +1,33 @@ +|null $restricted_to + * @method static \Illuminate\Database\Eloquent\Builder|Category newModelQuery() + * @method static \Illuminate\Database\Eloquent\Builder|Category newQuery() + * @method static \Illuminate\Database\Eloquent\Builder|Category query() + * @method static \Illuminate\Database\Eloquent\Builder|Category whereCreatedAt($value) + * @method static \Illuminate\Database\Eloquent\Builder|Category whereId($value) + * @method static \Illuminate\Database\Eloquent\Builder|Category whereName($value) + * @method static \Illuminate\Database\Eloquent\Builder|Category whereRestrictedTo($value) + * @method static \Illuminate\Database\Eloquent\Builder|Category whereSlug($value) + * @method static \Illuminate\Database\Eloquent\Builder|Category whereUpdatedAt($value) + * @mixin \Eloquent + */ +class Category extends Model +{ + protected $fillable = ['name', 'slug', 'restricted_to']; + + protected $casts = [ + 'restricted_to' => 'array', + ]; + +} diff --git a/app/Models/Entry.php b/app/Models/Entry.php index db044fb..ef532b9 100644 --- a/app/Models/Entry.php +++ b/app/Models/Entry.php @@ -2,17 +2,100 @@ namespace App\Models; +use App\Traits\HasGallery; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\SoftDeletes; +use Monolog\Level; +/** + * @property int $id + * @property string $type + * @property string|null $title + * @property string|null $slug + * @property string|null $description + * @property string|null $main_image + * @property string $state + * @property string|null $staff_comment + * @property \Illuminate\Support\Carbon|null $rejected_at + * @property bool $featured + * @property int|null $game_id + * @property int|null $platform_id + * @property int|null $status_id + * @property string|null $version + * @property \Illuminate\Support\Carbon|null $release_date + * @property string|null $staff_credits + * @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 $created_at + * @property \Illuminate\Support\Carbon|null $updated_at + * @property string|null $complete_title + * @property \Illuminate\Support\Carbon|null $deleted_at + * @property int|null $level_id + * @property-read \Illuminate\Database\Eloquent\Collection $authors + * @property-read int|null $authors_count + * @property-read \Illuminate\Database\Eloquent\Collection $categories + * @property-read int|null $categories_count + * @property-read \Illuminate\Database\Eloquent\Collection $files + * @property-read int|null $files_count + * @property-read \Illuminate\Database\Eloquent\Collection $gallery + * @property-read int|null $gallery_count + * @property-read \App\Models\Game|null $game + * @property-read \Illuminate\Database\Eloquent\Collection $hashes + * @property-read int|null $hashes_count + * @property-read \Illuminate\Database\Eloquent\Collection $languages + * @property-read int|null $languages_count + * @property-read \App\Models\Level|null $level + * @property-read \Illuminate\Database\Eloquent\Collection $modifications + * @property-read int|null $modifications_count + * @property-read \App\Models\Platform|null $platform + * @property-read \App\Models\Status|null $status + * @property-read \Illuminate\Database\Eloquent\Collection $systems + * @property-read int|null $systems_count + * @method static Builder|Entry inQueue(int $daysRejected = 7) + * @method static Builder|Entry newModelQuery() + * @method static Builder|Entry newQuery() + * @method static Builder|Entry onlyTrashed() + * @method static Builder|Entry published() + * @method static Builder|Entry query() + * @method static Builder|Entry whereCommentsThreadId($value) + * @method static Builder|Entry whereCompleteTitle($value) + * @method static Builder|Entry whereCreatedAt($value) + * @method static Builder|Entry whereDeletedAt($value) + * @method static Builder|Entry whereDescription($value) + * @method static Builder|Entry whereFeatured($value) + * @method static Builder|Entry whereGameId($value) + * @method static Builder|Entry whereId($value) + * @method static Builder|Entry whereLevelId($value) + * @method static Builder|Entry whereMainImage($value) + * @method static Builder|Entry wherePlatformId($value) + * @method static Builder|Entry whereRejectedAt($value) + * @method static Builder|Entry whereReleaseDate($value) + * @method static Builder|Entry whereRelevantLink($value) + * @method static Builder|Entry whereSlug($value) + * @method static Builder|Entry whereStaffComment($value) + * @method static Builder|Entry whereStaffCredits($value) + * @method static Builder|Entry whereState($value) + * @method static Builder|Entry whereStatusId($value) + * @method static Builder|Entry whereTitle($value) + * @method static Builder|Entry whereType($value) + * @method static Builder|Entry whereUpdatedAt($value) + * @method static Builder|Entry whereUserId($value) + * @method static Builder|Entry whereVersion($value) + * @method static Builder|Entry whereYoutubeLink($value) + * @method static Builder|Entry withTrashed(bool $withTrashed = true) + * @method static Builder|Entry withoutTrashed() + * @mixin \Eloquent + */ class Entry extends Model { - use SoftDeletes; + use SoftDeletes, HasGallery; /** * @var string[] @@ -38,6 +121,7 @@ class Entry extends Model 'comments_thread_id', 'staff_comment', 'rejected_at', + 'level_id' ]; /** @@ -81,6 +165,10 @@ class Entry extends Model return $this->belongsTo(Status::class ); } + public function level(): BelongsTo { + return $this->belongsTo(\App\Models\Level::class); + } + public function authors(): BelongsToMany { return $this->belongsToMany(Author::class, 'entry_authors'); } @@ -93,12 +181,16 @@ class Entry extends Model return $this->belongsToMany( Modification::class, 'entry_modifications'); } - public function files(): HasMany { - return $this->hasMany(EntryFile::class)->orderBy('filename'); + public function categories(): BelongsToMany { + return $this->belongsToMany(Category::class, 'entry_categories'); } - public function gallery(): HasMany { - return $this->hasMany(EntryGallery::class)->orderBy('id'); + public function systems(): BelongsToMany { + return $this->belongsToMany(System::class, 'entry_systems'); + } + + public function files(): HasMany { + return $this->hasMany(EntryFile::class)->orderBy('filename'); } public function hashes(): HasMany { diff --git a/app/Models/EntryFile.php b/app/Models/EntryFile.php index c738572..99bcd7f 100644 --- a/app/Models/EntryFile.php +++ b/app/Models/EntryFile.php @@ -5,6 +5,39 @@ namespace App\Models; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; +/** + * @property int $id + * @property int $entry_id + * @property string $filename + * @property string $filepath + * @property string $favorite_server + * @property int $favorite_at + * @property int|null $filesize + * @property \Illuminate\Support\Carbon|null $created_at + * @property \Illuminate\Support\Carbon|null $updated_at + * @property string $file_uuid + * @property string $state + * @property int $online_patcher + * @property int $secondary_online_patcher + * @property-read \App\Models\Entry|null $entry + * @method static \Illuminate\Database\Eloquent\Builder|EntryFile newModelQuery() + * @method static \Illuminate\Database\Eloquent\Builder|EntryFile newQuery() + * @method static \Illuminate\Database\Eloquent\Builder|EntryFile query() + * @method static \Illuminate\Database\Eloquent\Builder|EntryFile whereCreatedAt($value) + * @method static \Illuminate\Database\Eloquent\Builder|EntryFile whereEntryId($value) + * @method static \Illuminate\Database\Eloquent\Builder|EntryFile whereFavoriteAt($value) + * @method static \Illuminate\Database\Eloquent\Builder|EntryFile whereFavoriteServer($value) + * @method static \Illuminate\Database\Eloquent\Builder|EntryFile whereFileUuid($value) + * @method static \Illuminate\Database\Eloquent\Builder|EntryFile whereFilename($value) + * @method static \Illuminate\Database\Eloquent\Builder|EntryFile whereFilepath($value) + * @method static \Illuminate\Database\Eloquent\Builder|EntryFile whereFilesize($value) + * @method static \Illuminate\Database\Eloquent\Builder|EntryFile whereId($value) + * @method static \Illuminate\Database\Eloquent\Builder|EntryFile whereOnlinePatcher($value) + * @method static \Illuminate\Database\Eloquent\Builder|EntryFile whereSecondaryOnlinePatcher($value) + * @method static \Illuminate\Database\Eloquent\Builder|EntryFile whereState($value) + * @method static \Illuminate\Database\Eloquent\Builder|EntryFile whereUpdatedAt($value) + * @mixin \Eloquent + */ class EntryFile extends Model { protected $fillable = [ diff --git a/app/Models/EntryGallery.php b/app/Models/EntryGallery.php index 0b478ec..48b6ff2 100644 --- a/app/Models/EntryGallery.php +++ b/app/Models/EntryGallery.php @@ -2,10 +2,7 @@ namespace App\Models; -use Illuminate\Database\Eloquent\Model; - -class EntryGallery extends Model -{ - protected $fillable = ['entry_id','image']; - -} +/** + * @deprecated Use Gallery instead. + */ +class EntryGallery extends Gallery {} diff --git a/app/Models/EntryHash.php b/app/Models/EntryHash.php index 35ef829..bdeb050 100644 --- a/app/Models/EntryHash.php +++ b/app/Models/EntryHash.php @@ -5,6 +5,29 @@ namespace App\Models; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; +/** + * @property int $id + * @property int $entry_id + * @property string $filename + * @property string $hash_crc32 + * @property string $hash_sha1 + * @property string $verified + * @property \Illuminate\Support\Carbon|null $created_at + * @property \Illuminate\Support\Carbon|null $updated_at + * @property-read \App\Models\Entry|null $entry + * @method static \Illuminate\Database\Eloquent\Builder|EntryHash newModelQuery() + * @method static \Illuminate\Database\Eloquent\Builder|EntryHash newQuery() + * @method static \Illuminate\Database\Eloquent\Builder|EntryHash query() + * @method static \Illuminate\Database\Eloquent\Builder|EntryHash whereCreatedAt($value) + * @method static \Illuminate\Database\Eloquent\Builder|EntryHash whereEntryId($value) + * @method static \Illuminate\Database\Eloquent\Builder|EntryHash whereFilename($value) + * @method static \Illuminate\Database\Eloquent\Builder|EntryHash whereHashCrc32($value) + * @method static \Illuminate\Database\Eloquent\Builder|EntryHash whereHashSha1($value) + * @method static \Illuminate\Database\Eloquent\Builder|EntryHash whereId($value) + * @method static \Illuminate\Database\Eloquent\Builder|EntryHash whereUpdatedAt($value) + * @method static \Illuminate\Database\Eloquent\Builder|EntryHash whereVerified($value) + * @mixin \Eloquent + */ class EntryHash extends Model { protected $fillable = [ diff --git a/app/Models/Gallery.php b/app/Models/Gallery.php new file mode 100644 index 0000000..3d5f7dd --- /dev/null +++ b/app/Models/Gallery.php @@ -0,0 +1,34 @@ +|Gallery newModelQuery() + * @method static \Illuminate\Database\Eloquent\Builder|Gallery newQuery() + * @method static \Illuminate\Database\Eloquent\Builder|Gallery query() + * @method static \Illuminate\Database\Eloquent\Builder|Gallery whereCreatedAt($value) + * @method static \Illuminate\Database\Eloquent\Builder|Gallery whereEntryId($value) + * @method static \Illuminate\Database\Eloquent\Builder|Gallery whereId($value) + * @method static \Illuminate\Database\Eloquent\Builder|Gallery whereImage($value) + * @method static \Illuminate\Database\Eloquent\Builder|Gallery whereUpdatedAt($value) + * @mixin \Eloquent + */ +class Gallery extends Model +{ + protected $table = 'galleries'; + protected $fillable = ['image', 'galleryable_id', 'galleryable_type']; + + public function galleryable(): MorphTo + { + return $this->morphTo(); + } + +} diff --git a/app/Models/Game.php b/app/Models/Game.php index db4b115..0878b20 100644 --- a/app/Models/Game.php +++ b/app/Models/Game.php @@ -5,6 +5,30 @@ namespace App\Models; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; +/** + * @property int $id + * @property string $name + * @property string $slug + * @property int $platform_id + * @property int $genre_id + * @property \Illuminate\Support\Carbon|null $created_at + * @property \Illuminate\Support\Carbon|null $updated_at + * @property-read \Illuminate\Database\Eloquent\Collection $entries + * @property-read int|null $entries_count + * @property-read \App\Models\Genre $genre + * @property-read \App\Models\Platform $platform + * @method static \Illuminate\Database\Eloquent\Builder|Game newModelQuery() + * @method static \Illuminate\Database\Eloquent\Builder|Game newQuery() + * @method static \Illuminate\Database\Eloquent\Builder|Game query() + * @method static \Illuminate\Database\Eloquent\Builder|Game whereCreatedAt($value) + * @method static \Illuminate\Database\Eloquent\Builder|Game whereGenreId($value) + * @method static \Illuminate\Database\Eloquent\Builder|Game whereId($value) + * @method static \Illuminate\Database\Eloquent\Builder|Game whereName($value) + * @method static \Illuminate\Database\Eloquent\Builder|Game wherePlatformId($value) + * @method static \Illuminate\Database\Eloquent\Builder|Game whereSlug($value) + * @method static \Illuminate\Database\Eloquent\Builder|Game whereUpdatedAt($value) + * @mixin \Eloquent + */ class Game extends Model { /** diff --git a/app/Models/Genre.php b/app/Models/Genre.php index c2d3551..5088894 100644 --- a/app/Models/Genre.php +++ b/app/Models/Genre.php @@ -6,6 +6,24 @@ use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Database\Eloquent\Relations\HasMany; +/** + * @property int $id + * @property string $name + * @property string $slug + * @property \Illuminate\Support\Carbon|null $created_at + * @property \Illuminate\Support\Carbon|null $updated_at + * @property-read \Illuminate\Database\Eloquent\Collection $games + * @property-read int|null $games_count + * @method static \Illuminate\Database\Eloquent\Builder|Genre newModelQuery() + * @method static \Illuminate\Database\Eloquent\Builder|Genre newQuery() + * @method static \Illuminate\Database\Eloquent\Builder|Genre query() + * @method static \Illuminate\Database\Eloquent\Builder|Genre whereCreatedAt($value) + * @method static \Illuminate\Database\Eloquent\Builder|Genre whereId($value) + * @method static \Illuminate\Database\Eloquent\Builder|Genre whereName($value) + * @method static \Illuminate\Database\Eloquent\Builder|Genre whereSlug($value) + * @method static \Illuminate\Database\Eloquent\Builder|Genre whereUpdatedAt($value) + * @mixin \Eloquent + */ class Genre extends Model { protected $fillable = [ 'name', 'slug' ]; diff --git a/app/Models/Language.php b/app/Models/Language.php index a2de75b..ba1cca0 100644 --- a/app/Models/Language.php +++ b/app/Models/Language.php @@ -6,6 +6,24 @@ use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Database\Eloquent\Relations\HasMany; +/** + * @property int $id + * @property string $name + * @property string $slug + * @property \Illuminate\Support\Carbon|null $created_at + * @property \Illuminate\Support\Carbon|null $updated_at + * @property-read \Illuminate\Database\Eloquent\Collection $entries + * @property-read int|null $entries_count + * @method static \Illuminate\Database\Eloquent\Builder|Language newModelQuery() + * @method static \Illuminate\Database\Eloquent\Builder|Language newQuery() + * @method static \Illuminate\Database\Eloquent\Builder|Language query() + * @method static \Illuminate\Database\Eloquent\Builder|Language whereCreatedAt($value) + * @method static \Illuminate\Database\Eloquent\Builder|Language whereId($value) + * @method static \Illuminate\Database\Eloquent\Builder|Language whereName($value) + * @method static \Illuminate\Database\Eloquent\Builder|Language whereSlug($value) + * @method static \Illuminate\Database\Eloquent\Builder|Language whereUpdatedAt($value) + * @mixin \Eloquent + */ class Language extends Model { protected $fillable = [ 'name', 'slug' ]; diff --git a/app/Models/Level.php b/app/Models/Level.php new file mode 100644 index 0000000..d205d45 --- /dev/null +++ b/app/Models/Level.php @@ -0,0 +1,26 @@ +|Level newModelQuery() + * @method static \Illuminate\Database\Eloquent\Builder|Level newQuery() + * @method static \Illuminate\Database\Eloquent\Builder|Level query() + * @method static \Illuminate\Database\Eloquent\Builder|Level whereCreatedAt($value) + * @method static \Illuminate\Database\Eloquent\Builder|Level whereId($value) + * @method static \Illuminate\Database\Eloquent\Builder|Level whereName($value) + * @method static \Illuminate\Database\Eloquent\Builder|Level whereSlug($value) + * @method static \Illuminate\Database\Eloquent\Builder|Level whereUpdatedAt($value) + * @mixin \Eloquent + */ +class Level extends Model +{ + protected $fillable = ['name', 'slug']; +} diff --git a/app/Models/Modification.php b/app/Models/Modification.php index fd7054f..d2596d1 100644 --- a/app/Models/Modification.php +++ b/app/Models/Modification.php @@ -4,6 +4,22 @@ namespace App\Models; use Illuminate\Database\Eloquent\Model; +/** + * @property int $id + * @property string $name + * @property string $slug + * @property \Illuminate\Support\Carbon|null $created_at + * @property \Illuminate\Support\Carbon|null $updated_at + * @method static \Illuminate\Database\Eloquent\Builder|Modification newModelQuery() + * @method static \Illuminate\Database\Eloquent\Builder|Modification newQuery() + * @method static \Illuminate\Database\Eloquent\Builder|Modification query() + * @method static \Illuminate\Database\Eloquent\Builder|Modification whereCreatedAt($value) + * @method static \Illuminate\Database\Eloquent\Builder|Modification whereId($value) + * @method static \Illuminate\Database\Eloquent\Builder|Modification whereName($value) + * @method static \Illuminate\Database\Eloquent\Builder|Modification whereSlug($value) + * @method static \Illuminate\Database\Eloquent\Builder|Modification whereUpdatedAt($value) + * @mixin \Eloquent + */ class Modification extends Model { protected $fillable = [ 'name', 'slug' ]; diff --git a/app/Models/Platform.php b/app/Models/Platform.php index cac5bf3..8fc5269 100644 --- a/app/Models/Platform.php +++ b/app/Models/Platform.php @@ -5,6 +5,28 @@ namespace App\Models; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\HasMany; +/** + * @property int $id + * @property string $name + * @property string $slug + * @property string|null $short_name + * @property \Illuminate\Support\Carbon|null $created_at + * @property \Illuminate\Support\Carbon|null $updated_at + * @property-read \Illuminate\Database\Eloquent\Collection $entries + * @property-read int|null $entries_count + * @property-read \Illuminate\Database\Eloquent\Collection $games + * @property-read int|null $games_count + * @method static \Illuminate\Database\Eloquent\Builder|Platform newModelQuery() + * @method static \Illuminate\Database\Eloquent\Builder|Platform newQuery() + * @method static \Illuminate\Database\Eloquent\Builder|Platform query() + * @method static \Illuminate\Database\Eloquent\Builder|Platform whereCreatedAt($value) + * @method static \Illuminate\Database\Eloquent\Builder|Platform whereId($value) + * @method static \Illuminate\Database\Eloquent\Builder|Platform whereName($value) + * @method static \Illuminate\Database\Eloquent\Builder|Platform whereShortName($value) + * @method static \Illuminate\Database\Eloquent\Builder|Platform whereSlug($value) + * @method static \Illuminate\Database\Eloquent\Builder|Platform whereUpdatedAt($value) + * @mixin \Eloquent + */ class Platform extends Model { diff --git a/app/Models/Status.php b/app/Models/Status.php index b6276a9..8dc7094 100644 --- a/app/Models/Status.php +++ b/app/Models/Status.php @@ -4,6 +4,22 @@ namespace App\Models; use Illuminate\Database\Eloquent\Model; +/** + * @property int $id + * @property string $name + * @property string $slug + * @property \Illuminate\Support\Carbon|null $created_at + * @property \Illuminate\Support\Carbon|null $updated_at + * @method static \Illuminate\Database\Eloquent\Builder|Status newModelQuery() + * @method static \Illuminate\Database\Eloquent\Builder|Status newQuery() + * @method static \Illuminate\Database\Eloquent\Builder|Status query() + * @method static \Illuminate\Database\Eloquent\Builder|Status whereCreatedAt($value) + * @method static \Illuminate\Database\Eloquent\Builder|Status whereId($value) + * @method static \Illuminate\Database\Eloquent\Builder|Status whereName($value) + * @method static \Illuminate\Database\Eloquent\Builder|Status whereSlug($value) + * @method static \Illuminate\Database\Eloquent\Builder|Status whereUpdatedAt($value) + * @mixin \Eloquent + */ class Status extends Model { protected $fillable = ['name', 'slug']; diff --git a/app/Models/System.php b/app/Models/System.php new file mode 100644 index 0000000..65d86e3 --- /dev/null +++ b/app/Models/System.php @@ -0,0 +1,26 @@ +|System newModelQuery() + * @method static \Illuminate\Database\Eloquent\Builder|System newQuery() + * @method static \Illuminate\Database\Eloquent\Builder|System query() + * @method static \Illuminate\Database\Eloquent\Builder|System whereCreatedAt($value) + * @method static \Illuminate\Database\Eloquent\Builder|System whereId($value) + * @method static \Illuminate\Database\Eloquent\Builder|System whereName($value) + * @method static \Illuminate\Database\Eloquent\Builder|System whereSlug($value) + * @method static \Illuminate\Database\Eloquent\Builder|System whereUpdatedAt($value) + * @mixin \Eloquent + */ +class System extends Model +{ + protected $fillable = ['name', 'slug']; +} diff --git a/app/Models/User.php b/app/Models/User.php index f6ba1d2..e46ea30 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -10,6 +10,31 @@ use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Notifications\Notifiable; +/** + * @property int $id + * @property string $name + * @property string $email + * @property \Illuminate\Support\Carbon|null $email_verified_at + * @property string $password + * @property string|null $remember_token + * @property \Illuminate\Support\Carbon|null $created_at + * @property \Illuminate\Support\Carbon|null $updated_at + * @property-read \Illuminate\Notifications\DatabaseNotificationCollection $notifications + * @property-read int|null $notifications_count + * @method static \Database\Factories\UserFactory factory($count = null, $state = []) + * @method static \Illuminate\Database\Eloquent\Builder|User newModelQuery() + * @method static \Illuminate\Database\Eloquent\Builder|User newQuery() + * @method static \Illuminate\Database\Eloquent\Builder|User query() + * @method static \Illuminate\Database\Eloquent\Builder|User whereCreatedAt($value) + * @method static \Illuminate\Database\Eloquent\Builder|User whereEmail($value) + * @method static \Illuminate\Database\Eloquent\Builder|User whereEmailVerifiedAt($value) + * @method static \Illuminate\Database\Eloquent\Builder|User whereId($value) + * @method static \Illuminate\Database\Eloquent\Builder|User whereName($value) + * @method static \Illuminate\Database\Eloquent\Builder|User wherePassword($value) + * @method static \Illuminate\Database\Eloquent\Builder|User whereRememberToken($value) + * @method static \Illuminate\Database\Eloquent\Builder|User whereUpdatedAt($value) + * @mixin \Eloquent + */ #[Fillable(['name', 'email', 'password'])] #[Hidden(['password', 'remember_token'])] class User extends Authenticatable diff --git a/app/Services/SubmissionsService.php b/app/Services/SubmissionsService.php index c397316..b7df22b 100644 --- a/app/Services/SubmissionsService.php +++ b/app/Services/SubmissionsService.php @@ -8,15 +8,17 @@ use App\Helpers\XenForoHelpers; use App\Http\Requests\StoreEntryRequest; use App\Jobs\CreateXenForoCommentsThread; use App\Models\Author; +use App\Models\Category; use App\Models\Entry; use App\Models\EntryFile; -use App\Models\EntryGallery; +use App\Models\Gallery; use App\Models\EntryHash; use App\Models\Game; use App\Models\Genre; use App\Models\Language; use App\Models\Modification; use App\Models\Platform; +use App\Models\System; use Illuminate\Http\Request; use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\DB; @@ -118,11 +120,7 @@ class SubmissionsService { $entry = DB::transaction(function () use ( $user_id ) { // STEP 2 : Create game. - - $gameId = null; - if( section_must_be( ['romhacks', 'translations'], $this->section ) ){ - $gameId = $this->Step2_CreateAndReturnGameId(); - } + $gameId = $this->Step2_CreateAndReturnGameId(); // STEP 3 : Create Complete title. $completeTitle = $this->Step3_BuildCompleteTitle( $gameId ); @@ -132,7 +130,7 @@ class SubmissionsService { if( section_must_be( 'translations', $this->section ) && !$this->request->input('entry_title') ){ - $entryTitle = Game::find($gameId)->name; + $entryTitle = Game::find($gameId)->name ?? ""; } else { $entryTitle = $this->request->input('entry_title'); } @@ -149,6 +147,7 @@ class SubmissionsService { 'main_image' => $this->request->input('main-image'), 'state' => $this->request->input('submit-state'), 'game_id' => $gameId, + 'platform_id' => $this->request->input('platform_only_id'), 'status_id' => $this->request->input('status'), 'version' => $this->request->input('version'), 'release_date' => $this->request->input('release-date'), @@ -157,6 +156,7 @@ class SubmissionsService { 'youtube_link' => $this->request->input('youtube_video'), 'user_id' => $user_id, 'complete_title' => $completeTitle, + 'level_id' => $this->request->input('level') ]; $entry = Entry::create( $fields ); @@ -165,19 +165,28 @@ class SubmissionsService { $this->Step7_SaveEntryFiles( $entry ); // STEP 8 : Save hashes. - $this->Step8_SaveHashes( $entry->id ); + if( section_must_be( ['translations', 'romhacks' ], $this->section ) ) + $this->Step8_SaveHashes( $entry->id ); // STEP 9 : Save Authors. $this->Step9_SaveAuthors( $entry ); // STEP 10 : Save Modifications. - if( section_must_be( 'romhacks', $this->section ) ){ + if( section_must_be( ['romhacks','lua-scripts'], $this->section ) ){ $this->Step10_SaveRomhacksModifications( $entry ); } + if( section_must_be( 'utilities', $this->section ) ){ + $this->Step10_SaveUtilitiesSystems( $entry ); + } // STEP 11 : Save Languages $this->Step11_SaveLanguages( $entry ); + // STEP 11.5 : Save Categories + if( section_must_be( 'utilities', $this->section ) ) { + $this->Step11_5_SaveCategories($entry); + } + // STEP 12 : Prepare Gallery images. $this->Step12a_PrepareGalleryImages( $entry ); @@ -207,6 +216,10 @@ class SubmissionsService { */ private function Step2_CreateAndReturnGameId(): ?int { + $mode = $this->request->input('game_selection_mode', 'game'); + if( $mode !== 'game' ) + return null; + // Already existing game. if( $this->request->input('game_id') ) return $this->request->input('game_id'); @@ -261,9 +274,9 @@ class SubmissionsService { if( section_must_be( 'translations', $this->section ) ) { $fields['languages_string'] = Language::whereIn('id', $this->request->input('languages', []))->pluck('name')->implode(', '); } - if( section_must_be(['romhacks', 'translations', 'homebrew', 'lua-scripts', 'tutorials'], $this->section ) ) { + if( section_must_be(['romhacks', 'translations', 'homebrew', 'lua-scripts'], $this->section ) ) { // TODO: Add single platform ID compatibility. - $fields['platform_name'] = $gameId ? Game::find( $gameId )->platform->name : null; + $fields['platform_name'] = $gameId ? Game::find( $gameId )->platform->name : Platform::find( $this->request->input('platform_only_id') )?->name ?? null; } return EntryHelpers::buildCompleteTitle( $this->section, $fields ); @@ -381,6 +394,24 @@ class SubmissionsService { } } + /** + * @param Entry $entry + * + * @return void + * @throws SubmissionException + */ + private function Step10_SaveUtilitiesSystems( Entry $entry ): void + { + // TODO: Replace by edit version + + foreach ( $this->request->input('systems', [] ) ?? [] as $systemId ) { + $system = System::find( $systemId ); + if( !$system ) + throw new SubmissionException( "System {$systemId} does not exist." ); + $entry->systems()->attach( $system->id ); + } + } + /** * @param Entry $entry * @@ -400,11 +431,29 @@ class SubmissionsService { } + /** + * @param Entry $entry + * + * @return void + * @throws SubmissionException + */ + private function Step11_5_SaveCategories( Entry $entry ): void + { + // TODO: Replace by edit version. + + foreach ( $this->request->input('categories', [] ) ?? [] as $categoryId ) { + $category = Category::find( $categoryId ); + if( !$category ) + throw new SubmissionException( "Category {$categoryId} does not exist." ); + $entry->categories()->attach( $category->id ); + } + + } + private function Step12a_PrepareGalleryImages( Entry $entry ): void { foreach ( $this->request->input('gallery', [] ) ?? [] as $imagePath ) { - EntryGallery::create([ - 'entry_id' => $entry->id, + $entry->gallery()->create([ 'image' => $imagePath, ]); } @@ -464,9 +513,7 @@ class SubmissionsService { // STEP 2: Create game if different. $gameId = null; - if( section_must_be( ['romhacks', 'translations' ], $this->section ) ){ - $gameId = $this->eStep2_VerifyCreateAndEditGameId(); - } + $gameId = $this->eStep2_VerifyCreateAndEditGameId(); // STEP 3: Recreate complete title and refresh slug if needed. $completeTitle = $this->Step3_BuildCompleteTitle( $gameId ); @@ -494,6 +541,7 @@ class SubmissionsService { 'main_image' => $this->request->input('main-image'), 'state' => $this->request->input('submit-state'), 'game_id' => $gameId, + 'platform_id' => $this->request->input('platform_only_id'), 'status_id' => $this->request->input('status'), 'version' => $this->request->input('version'), 'release_date' => $this->request->input('release-date'), @@ -502,12 +550,13 @@ class SubmissionsService { 'youtube_link' => $this->request->input('youtube_video'), 'user_id' => $user_id, 'complete_title' => $completeTitle, - 'comments_thread_id' => $this->request->input('comments_thread_id'), - 'featured' => $this->request->input('featured') ?? false, + 'level_id' => $this->request->input('level'), ]; if( \Auth::user()->can('moderate', $this->entry) ){ $fields['staff_comment'] = $this->request->input('staff_comment'); + $fields['featured'] = $this->request->input('featured') ?? false; + $fields['comments_thread_id'] = $this->request->input('comments_thread_id'); } $this->entry->update( $fields ); @@ -516,19 +565,27 @@ class SubmissionsService { $this->eStep6_UpdateEntryFiles( $this->entry->id ); // STEP 7: Update hashes. - $this->eStep7_UpdateHashes( $this->entry->id ); + if( section_must_be( ['translations', 'romhacks' ], $this->section ) ) + $this->eStep7_UpdateHashes( $this->entry->id ); // STEP 8: Update Authors. $this->eStep8_UpdateAuthors(); // STEP 9: Update romhacks modifications. - if( section_must_be( 'romhacks', $this->section ) ) { + if( section_must_be( ['romhacks', 'lua-scripts'], $this->section ) ) { $this->eStep9_UpdateRomhacksModifications(); } + if( section_must_be( 'utilities', $this->section ) ) { + $this->eStep9_UpdateUtilitiesSystems(); + } // STEP 10: Update Languages. $this->eStep10_UpdateLanguages(); + // STEP 10.5 : Update categories + if( section_must_be( 'utilities', $this->section ) ) + $this->eStep10_5_UpdateCategories(); + // STEP 11: Prepare new gallery images and prepare deletion of others ones. $galleryPaths = $this->eStep11a_UpdateGalleryImages(); @@ -558,8 +615,14 @@ class SubmissionsService { /** * @throws SubmissionException */ - private function eStep2_VerifyCreateAndEditGameId(): int + private function eStep2_VerifyCreateAndEditGameId(): ?int { + + $mode = $this->request->input('game_selection_mode', 'game'); + if ($mode !== 'game') { + return null; + } + // Already existing game. if( $this->request->input('game_id') ){ @@ -721,6 +784,27 @@ class SubmissionsService { } + /** + * @return void + * @throws SubmissionException + */ + private function eStep9_UpdateUtilitiesSystems(): void + { + $requestSystems = $this->request->input('systems', [] ) ?? []; + if( !empty( $requestSystems ) ){ + $valid = System::whereIn( 'id', $requestSystems )->pluck('id')->toArray(); + + if( count( $valid ) !== count( $requestSystems ) ){ + throw new SubmissionException( "One of the systems doesn't exist." ); + } + + + } + + $this->entry->systems()->sync( $requestSystems ); + + } + /** * @return void * @throws SubmissionException @@ -739,6 +823,24 @@ class SubmissionsService { $this->entry->languages()->sync( $requestLanguages ); } + /** + * @return void + * @throws SubmissionException + */ + private function eStep10_5_UpdateCategories(): void + { + $requestCategories = $this->request->input('categories', [] ) ?? []; + if( !empty( $requestCategories ) ){ + $valid = Category::whereIn( 'id', $requestCategories )->pluck('id')->toArray(); + if( count( $valid ) !== count( $requestCategories ) ){ + throw new SubmissionException( "One of the categories doesn't exist." ); + } + + } + + $this->entry->categories()->sync( $requestCategories ); + } + private function eStep11a_UpdateGalleryImages(): array { $requestGallery = $this->request->input('gallery', [] ) ?? []; @@ -747,14 +849,13 @@ class SubmissionsService { $needDeletion = array_diff( $existingGalleryPaths, $requestGallery ); if( !empty( $needDeletion ) ){ - EntryGallery::where('entry_id', $this->entry->id)->whereIn('image', $needDeletion )->delete(); + $this->entry->gallery()->whereIn('image', $needDeletion )->delete(); } $needAddition = array_diff( $requestGallery, $existingGalleryPaths ); $images = []; foreach( $needAddition as $imagePath ){ - $images[] = EntryGallery::create([ - 'entry_id' => $this->entry->id, + $images[] = $this->entry->gallery()->create([ 'image' => $imagePath, ]); } diff --git a/app/Traits/HasGallery.php b/app/Traits/HasGallery.php new file mode 100644 index 0000000..c775d50 --- /dev/null +++ b/app/Traits/HasGallery.php @@ -0,0 +1,14 @@ +morphMany(Gallery::class, 'galleryable')->orderBy('id'); + } +} diff --git a/app/View/Components/CategorySelector.php b/app/View/Components/CategorySelector.php new file mode 100644 index 0000000..44c7fdb --- /dev/null +++ b/app/View/Components/CategorySelector.php @@ -0,0 +1,41 @@ +categories = Category::query() + ->where(function ($query) { + $query->whereJsonContains('restricted_to', $this->section) + ->orWhereNull('restricted_to'); + }) + ->orderBy('name') + ->get(); + } + + /** + * Get the view / contents that represent the component. + */ + public function render(): View|Closure|string + { + return view('components.category-selector'); + } +} diff --git a/composer.json b/composer.json index dbd334c..cf73412 100644 --- a/composer.json +++ b/composer.json @@ -18,6 +18,7 @@ }, "require-dev": { "barryvdh/laravel-debugbar": "^4.2", + "barryvdh/laravel-ide-helper": "^3.7", "fakerphp/faker": "^1.23", "larastan/larastan": "^3.9", "laravel/pail": "^1.2.5", diff --git a/composer.lock b/composer.lock index 953b57e..a713774 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "a1c836758aaea45cc09c0c7786ea366f", + "content-hash": "79ce85f4c121f30d964f9322cb7120a4", "packages": [ { "name": "blade-ui-kit/blade-heroicons", @@ -8316,6 +8316,301 @@ ], "time": "2026-04-20T13:31:29+00:00" }, + { + "name": "barryvdh/laravel-ide-helper", + "version": "v3.7.0", + "source": { + "type": "git", + "url": "https://github.com/barryvdh/laravel-ide-helper.git", + "reference": "ad7e37676f1ff985d55ef1b6b96a0c0a40f2609a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/barryvdh/laravel-ide-helper/zipball/ad7e37676f1ff985d55ef1b6b96a0c0a40f2609a", + "reference": "ad7e37676f1ff985d55ef1b6b96a0c0a40f2609a", + "shasum": "" + }, + "require": { + "barryvdh/reflection-docblock": "^2.4", + "composer/class-map-generator": "^1.0", + "ext-json": "*", + "illuminate/console": "^11.15 || ^12 || ^13.0", + "illuminate/database": "^11.15 || ^12 || ^13.0", + "illuminate/filesystem": "^11.15 || ^12 || ^13.0", + "illuminate/support": "^11.15 || ^12 || ^13.0", + "php": "^8.2" + }, + "require-dev": { + "ext-pdo_sqlite": "*", + "friendsofphp/php-cs-fixer": "^3", + "illuminate/config": "^11.15 || ^12 || ^13.0", + "illuminate/view": "^11.15 || ^12 || ^13.0", + "larastan/larastan": "^3.1", + "mockery/mockery": "^1.4", + "orchestra/testbench": "^9.2 || ^10 || ^11.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpunit/phpunit": "^10.5 || ^11.5.3 || ^12.5.12", + "spatie/phpunit-snapshot-assertions": "^4 || ^5", + "vlucas/phpdotenv": "^5" + }, + "suggest": { + "illuminate/events": "Required for automatic helper generation (^6|^7|^8|^9|^10|^11)." + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Barryvdh\\LaravelIdeHelper\\IdeHelperServiceProvider" + ] + }, + "branch-alias": { + "dev-master": "3.6-dev" + } + }, + "autoload": { + "psr-4": { + "Barryvdh\\LaravelIdeHelper\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Barry vd. Heuvel", + "email": "barryvdh@gmail.com" + } + ], + "description": "Laravel IDE Helper, generates correct PHPDocs for all Facade classes, to improve auto-completion.", + "keywords": [ + "autocomplete", + "codeintel", + "dev", + "helper", + "ide", + "laravel", + "netbeans", + "phpdoc", + "phpstorm", + "sublime" + ], + "support": { + "issues": "https://github.com/barryvdh/laravel-ide-helper/issues", + "source": "https://github.com/barryvdh/laravel-ide-helper/tree/v3.7.0" + }, + "funding": [ + { + "url": "https://fruitcake.nl", + "type": "custom" + }, + { + "url": "https://github.com/barryvdh", + "type": "github" + } + ], + "time": "2026-03-17T14:12:51+00:00" + }, + { + "name": "barryvdh/reflection-docblock", + "version": "v2.4.1", + "source": { + "type": "git", + "url": "https://github.com/barryvdh/ReflectionDocBlock.git", + "reference": "4f5ba70c30c81f2ce03a16a9965832cfcc31ed3b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/barryvdh/ReflectionDocBlock/zipball/4f5ba70c30c81f2ce03a16a9965832cfcc31ed3b", + "reference": "4f5ba70c30c81f2ce03a16a9965832cfcc31ed3b", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "require-dev": { + "phpunit/phpunit": "^8.5.14|^9" + }, + "suggest": { + "dflydev/markdown": "~1.0", + "erusev/parsedown": "~1.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.3.x-dev" + } + }, + "autoload": { + "psr-0": { + "Barryvdh": [ + "src/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mike van Riel", + "email": "mike.vanriel@naenius.com" + } + ], + "support": { + "source": "https://github.com/barryvdh/ReflectionDocBlock/tree/v2.4.1" + }, + "time": "2026-03-05T20:09:01+00:00" + }, + { + "name": "composer/class-map-generator", + "version": "1.7.3", + "source": { + "type": "git", + "url": "https://github.com/composer/class-map-generator.git", + "reference": "86d8208fc3c649a3a999daf1a63c25201be2990f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/class-map-generator/zipball/86d8208fc3c649a3a999daf1a63c25201be2990f", + "reference": "86d8208fc3c649a3a999daf1a63c25201be2990f", + "shasum": "" + }, + "require": { + "composer/pcre": "^2.1 || ^3.1", + "php": "^7.2 || ^8.0", + "symfony/finder": "^4.4 || ^5.3 || ^6 || ^7 || ^8" + }, + "require-dev": { + "phpstan/phpstan": "^1.12 || ^2", + "phpstan/phpstan-deprecation-rules": "^1 || ^2", + "phpstan/phpstan-phpunit": "^1 || ^2", + "phpstan/phpstan-strict-rules": "^1.1 || ^2", + "phpunit/phpunit": "^8", + "symfony/filesystem": "^5.4 || ^6 || ^7 || ^8" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\ClassMapGenerator\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "https://seld.be" + } + ], + "description": "Utilities to scan PHP code and generate class maps.", + "keywords": [ + "classmap" + ], + "support": { + "issues": "https://github.com/composer/class-map-generator/issues", + "source": "https://github.com/composer/class-map-generator/tree/1.7.3" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + } + ], + "time": "2026-05-05T09:17:07+00:00" + }, + { + "name": "composer/pcre", + "version": "3.3.2", + "source": { + "type": "git", + "url": "https://github.com/composer/pcre.git", + "reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/pcre/zipball/b2bed4734f0cc156ee1fe9c0da2550420d99a21e", + "reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0" + }, + "conflict": { + "phpstan/phpstan": "<1.11.10" + }, + "require-dev": { + "phpstan/phpstan": "^1.12 || ^2", + "phpstan/phpstan-strict-rules": "^1 || ^2", + "phpunit/phpunit": "^8 || ^9" + }, + "type": "library", + "extra": { + "phpstan": { + "includes": [ + "extension.neon" + ] + }, + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\Pcre\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + } + ], + "description": "PCRE wrapping library that offers type-safe preg_* replacements.", + "keywords": [ + "PCRE", + "preg", + "regex", + "regular expression" + ], + "support": { + "issues": "https://github.com/composer/pcre/issues", + "source": "https://github.com/composer/pcre/tree/3.3.2" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2024-11-12T16:29:46+00:00" + }, { "name": "fakerphp/faker", "version": "v1.24.1", @@ -10945,7 +11240,9 @@ "prefer-stable": true, "prefer-lowest": false, "platform": { - "php": "^8.4" + "php": "^8.4", + "ext-xmlreader": "*", + "ext-simplexml": "*" }, "platform-dev": {}, "plugin-api-version": "2.9.0" diff --git a/database/migrations/2026_06_09_122425_create_category_table.php b/database/migrations/2026_06_09_122425_create_category_table.php new file mode 100644 index 0000000..4fe1bd3 --- /dev/null +++ b/database/migrations/2026_06_09_122425_create_category_table.php @@ -0,0 +1,29 @@ +id(); + $table->string('name'); + $table->string('slug')->unique(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('categories'); + } +}; diff --git a/database/migrations/2026_06_09_122655_create_os_table.php b/database/migrations/2026_06_09_122655_create_os_table.php new file mode 100644 index 0000000..7e119f4 --- /dev/null +++ b/database/migrations/2026_06_09_122655_create_os_table.php @@ -0,0 +1,29 @@ +id(); + $table->string('name'); + $table->string('slug')->unique(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('systems'); + } +}; diff --git a/database/migrations/2026_06_09_122817_create_level_table.php b/database/migrations/2026_06_09_122817_create_level_table.php new file mode 100644 index 0000000..13d321f --- /dev/null +++ b/database/migrations/2026_06_09_122817_create_level_table.php @@ -0,0 +1,29 @@ +id(); + $table->string('name'); + $table->string('slug')->unique(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('levels'); + } +}; diff --git a/database/migrations/2026_06_09_123242_add_entry_level_id_field.php b/database/migrations/2026_06_09_123242_add_entry_level_id_field.php new file mode 100644 index 0000000..a263bb9 --- /dev/null +++ b/database/migrations/2026_06_09_123242_add_entry_level_id_field.php @@ -0,0 +1,28 @@ +foreignId('level_id')->nullable()->constrained('levels')->nullOnDelete(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('entries', function (Blueprint $table) { + $table->dropColumn('level_id'); + }); + } +}; diff --git a/database/migrations/2026_06_09_123458_create_entry_systems_table.php b/database/migrations/2026_06_09_123458_create_entry_systems_table.php new file mode 100644 index 0000000..a4e3fbf --- /dev/null +++ b/database/migrations/2026_06_09_123458_create_entry_systems_table.php @@ -0,0 +1,28 @@ +foreignId('entry_id')->constrained('entries')->cascadeOnDelete(); + $table->foreignId('system_id')->constrained('systems')->cascadeOnDelete(); + $table->primary(['entry_id', 'system_id']); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('entry_systems'); + } +}; diff --git a/database/migrations/2026_06_09_123533_create_entry_categories_table.php b/database/migrations/2026_06_09_123533_create_entry_categories_table.php new file mode 100644 index 0000000..fce71b1 --- /dev/null +++ b/database/migrations/2026_06_09_123533_create_entry_categories_table.php @@ -0,0 +1,28 @@ +foreignId('entry_id')->constrained('entries')->cascadeOnDelete(); + $table->foreignId('category_id')->constrained('categories')->cascadeOnDelete(); + $table->primary(['entry_id', 'category_id']); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('entry_categories'); + } +}; diff --git a/database/migrations/2026_06_09_124832_add_restricted_field_to_categories.php b/database/migrations/2026_06_09_124832_add_restricted_field_to_categories.php new file mode 100644 index 0000000..f63e4a6 --- /dev/null +++ b/database/migrations/2026_06_09_124832_add_restricted_field_to_categories.php @@ -0,0 +1,28 @@ +json('restricted_to')->nullable(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('categories', function (Blueprint $table) { + $table->dropColumn('restricted_to'); + }); + } +}; diff --git a/database/migrations/2026_06_10_084936_make_entry_galleries_polymorphic.php b/database/migrations/2026_06_10_084936_make_entry_galleries_polymorphic.php new file mode 100644 index 0000000..391bf73 --- /dev/null +++ b/database/migrations/2026_06_10_084936_make_entry_galleries_polymorphic.php @@ -0,0 +1,40 @@ +string('galleryable_type')->default('App\\Models\\Entry') + ->after('id'); + + $table->renameColumn('entry_id', 'galleryable_id'); + + $table->index(['galleryable_type', 'galleryable_id']); + + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('galleries', function (Blueprint $table) { + $table->dropColumn('galleryable_type'); + $table->renameColumn('galleryable_id', 'entry_id'); + $table->dropIndex(['galleryable_type', 'galleryable_id']); + }); + + Schema::rename('galleries', 'entry_galleries'); + } +}; diff --git a/database/migrations/2026_06_10_090320_create_news_table.php b/database/migrations/2026_06_10_090320_create_news_table.php new file mode 100644 index 0000000..dab2a32 --- /dev/null +++ b/database/migrations/2026_06_10_090320_create_news_table.php @@ -0,0 +1,27 @@ +id(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('news'); + } +}; diff --git a/extra.less b/extra.less index ea5932a..62e665d 100644 --- a/extra.less +++ b/extra.less @@ -63,7 +63,7 @@ ul { --error: #e57373; --info: #1976d2; --success: #81c784; - --success2: #388e3c; + --success2: #fdeb0f; /* Typo */ --typography: 'Segoe UI', 'San Francisco', 'Helvetica Neue', sans-serif; diff --git a/resources/css/components/common.css b/resources/css/components/common.css index c74aa62..e4e392e 100644 --- a/resources/css/components/common.css +++ b/resources/css/components/common.css @@ -104,6 +104,11 @@ color: var(--text); border-color: var(--success2); } +.badge.yellow, .badge.utilities { + background-color: #fdeb0f; + color: #000; + border-color: #fdeb0f; +} .topbar-badge { position: absolute; diff --git a/resources/css/components/forms.css b/resources/css/components/forms.css index f1fa4ff..11a7587 100644 --- a/resources/css/components/forms.css +++ b/resources/css/components/forms.css @@ -528,3 +528,44 @@ } .author-search-item:hover { background-color: var(--bg3); } + +.game-selector-mode { + display: flex; + gap: 0; + margin-bottom: 15px; + border: 1px solid var(--border); +} + +.game-selector-mode-btn { + display: flex; + align-items: center; + gap: 7px; + padding: 8px 14px; + background: none; + border: none; + border-right: 1px solid var(--border); + color: var(--text2); + font-size: 0.85rem; + cursor: pointer; + font-family: var(--font-family); + transition: background-color 0.1s, color 0.1s; +} + +.game-selector-mode-btn:last-child { + border-right: none; +} + +.game-selector-mode-btn:hover { + background-color: var(--bg3); + color: var(--text); +} + +.game-selector-mode-btn.active { + background-color: var(--bg3); + color: var(--rhpz-orange); + border-bottom: 2px solid var(--rhpz-orange); +} + +.game-selector-platform-only { + grid-column: span 1; +} diff --git a/resources/js/submissions.js b/resources/js/submissions.js index e9b0a67..7931261 100644 --- a/resources/js/submissions.js +++ b/resources/js/submissions.js @@ -22,6 +22,7 @@ const ERROR_TABLE = { uploadError: "One or more files failed to upload.", notAllFilesDone: "Not all the files have finished uploading yet.", noModifications: "Please select at least a type of hack.", + noSystems: "Please select at least a system.", noDescription: "Please provide a description.", noGame: "Please provide a game or create a new one and fill all the required fields.", noLanguages: "Please select at least a language.", @@ -117,6 +118,10 @@ window.SubmissionVerifications = { return verifyCheckboxes( document.querySelector( '#modifications-group' ) ); }, + step5_UtilitiesSystemsCheckboxes: function(){ + return verifyCheckboxes( document.querySelector( '#systems-group' ) ); + }, + /** * Verify if the description field has at least one character. * @returns {boolean} @@ -132,6 +137,10 @@ window.SubmissionVerifications = { */ step7_VerifyGame: function( element ){ + const GAME_SELECTOR_MODE = document.querySelector('input[name="game_selection_mode"]')?.value ?? "game"; + if( GAME_SELECTOR_MODE === 'platform' || GAME_SELECTOR_MODE === 'none' ) + return true; + // Check if we have an already existent selected game. const GAME_ID_INPUT = document.querySelector('input[name="game_id"]'); if( GAME_ID_INPUT ){ @@ -283,12 +292,18 @@ window.Submission = function(){ return false; } - if( SECTION() === "romhacks" ){ + if( SECTION() === "romhacks" || SECTION() === "lua-scripts" ){ console.log( "Step 5" ); if( !SubmissionVerifications.step5_RomhacksModificationsCheckboxes()){ this.errorKey = "noModifications"; return false; } + } else if( SECTION() === "utilities" ){ + console.log( "Step 5" ); + if( !SubmissionVerifications.step5_UtilitiesSystemsCheckboxes()){ + this.errorKey = "noSystems"; + return false; + } } console.log( "Step 6" ); @@ -341,6 +356,7 @@ window.Submission = function(){ notAllFilesDone: 'uploadTarget', uploadError: 'uploadTarget', noModifications: 'modificationsGroup', + noSystems: 'systemsGroup', noDescription: 'descriptionField', noGame: 'gameSelector', noLanguages: 'languagesGroup', diff --git a/resources/views/components/category-selector.blade.php b/resources/views/components/category-selector.blade.php new file mode 100644 index 0000000..2a2193f --- /dev/null +++ b/resources/views/components/category-selector.blade.php @@ -0,0 +1,29 @@ + +
+ +
+ @foreach( $categories as $category ) + + @endforeach +
+ +
diff --git a/resources/views/components/entry-card.blade.php b/resources/views/components/entry-card.blade.php index f1d991d..28ba16c 100644 --- a/resources/views/components/entry-card.blade.php +++ b/resources/views/components/entry-card.blade.php @@ -24,13 +24,26 @@ @foreach( $entry->modifications as $modif ) {{ $modif->name }} @endforeach + @elseif( section_must_be( 'translations', $entry->type ) ) + @foreach( $entry->languages as $lang ) + {{ $lang->name }} + @endforeach + @elseif( section_must_be( 'utilities', $entry->type ) ) + @foreach( $entry->categories as $category ) + {{ $category->name }} + @endforeach @endif @if( $entry->status_id ) {{ $entry->status->name }} @endif - @foreach( $entry->languages as $lang ) - {{ $lang->name }} - @endforeach + @if( $entry->level_id ) + {{ $entry->level->name }} + @endif + @if( section_must_not_be( 'translations', $entry->type ) ) + @foreach( $entry->languages as $lang ) + {{ $lang->name }} + @endforeach + @endif
x diff --git a/resources/views/entries/show.blade.php b/resources/views/entries/show.blade.php index 11599e9..102ae5f 100644 --- a/resources/views/entries/show.blade.php +++ b/resources/views/entries/show.blade.php @@ -1,4 +1,4 @@ - + @extends('layouts.app') @section('page-title', $entry->title . " - " . config('app.name') ) @@ -54,9 +54,13 @@
@if($entry->state === 'pending') @can('approve', $entry)
-
+ @csrf @method('PATCH') -
- @@ -145,7 +174,7 @@ @csrf @method('PATCH')
- + +
+
+ + +
+
+
+
+ + +
+
+
+ +
+
+
+ @endcan + @cannot('moderate', $news) + + @endcannot + @endif + + @csrf + +
+ + +
+ + +
diff --git a/resources/views/news/index.blade.php b/resources/views/news/index.blade.php new file mode 100644 index 0000000..b587b5b --- /dev/null +++ b/resources/views/news/index.blade.php @@ -0,0 +1,11 @@ +@extends('layouts.app') + +@section('page-title', "News - " . config('app.name') ) + +@section('content') + {{ \Diglactic\Breadcrumbs\Breadcrumbs::render() }} +
+ News +
+ @livewire('news-database') +@endsection diff --git a/resources/views/news/show.blade.php b/resources/views/news/show.blade.php new file mode 100644 index 0000000..75f6a9e --- /dev/null +++ b/resources/views/news/show.blade.php @@ -0,0 +1,225 @@ +@extends('layouts.app') + +@section('page-title', $news->title . " - " . config('app.name') ) + +@section('content') + {{ Diglactic\Breadcrumbs\Breadcrumbs::render() }} +
+
+
+

+ {{ $news->title }} + @if( $news->state === 'pending' ) +
+ - + + Pending approval +
+ @elseif( $news->state === 'rejected' ) +
+ - + + Rejected +
+ @elseif( $news->state === 'locked' ) +
+ - + + Locked +
+ @elseif( $news->state === 'draft' ) +
+ - + + Draft +
+ @elseif( $news->state === 'hidden' ) +
+ - + + Hidden +
+ @endif +

+
+ @if($news->category_id) + + + {{ $news->category->name }} + + @endif + @if($news->user_id) + + + Posted by + + @endif + + + {{ $news->created_at->format('M d, Y') }} + + @if($news->updated_at && $news->updated_at->gt($news->created_at)) + + + Updated {{ $news->updated_at->diffForHumans() }} + + @endif +
+
+ + @if($news->state === 'pending') + @can('approve', $news) +
+
+ @csrf @method('PATCH') + +
+ + + +
+ @endcan + @endif + + @can('update', $news) + + Edit + + @endcan + + @auth + id}/report") }}" class="btn"> + Report + + @endauth + +
+
+
+ +
+
+ @if( $news->description ) +
+ {{ $news->description }} +
+ @endif + @if( $news->gallery->isNotEmpty() ) +
+ +
+ @foreach( $news->gallery as $galleryItem ) +
+ @endforeach +
+ +
+ @endif +
+ +
+
+ @include('entries.comments') +@endsection diff --git a/resources/views/queue/index.blade.php b/resources/views/queue/index.blade.php index 36554e6..4884db0 100644 --- a/resources/views/queue/index.blade.php +++ b/resources/views/queue/index.blade.php @@ -3,6 +3,7 @@ @section('page-title', "Submissions Queue - " . config('app.name') ) @section('content') + {{ \Diglactic\Breadcrumbs\Breadcrumbs::render() }}
Submissions Queue
diff --git a/resources/views/queue/item.blade.php b/resources/views/queue/item.blade.php index 791199e..69f6f29 100644 --- a/resources/views/queue/item.blade.php +++ b/resources/views/queue/item.blade.php @@ -7,7 +7,7 @@ >
-

{{ $entry->complete_title }}

+

{{ $entry->complete_title ?? $entry->title }}

@if($entry->state === 'rejected') @@ -23,20 +23,35 @@
Submitted by on {{ $entry->created_at->format('Y-m-d') }} - {{ \App\View\Components\EntryCard::ENTRY_TYPES_BADGE[$entry->type] ?? $entry->type }} + @if($entry->queue_type === 'entry' ) + {{ \App\View\Components\EntryCard::ENTRY_TYPES_BADGE[$entry->type] ?? $entry->type }} + @else + News + @endif
@can('manageButtonsInQueue',$entry)
- - - View Entry - - - - Edit - + @if( $entry->queue_type === 'entry' ) + + + View Entry + + + + Edit + + @else + + + View Entry + + + + Edit + + @endif
@endcan
@@ -97,7 +112,7 @@ @can('approve',$entry) @if($entry->state === 'pending')
-
+ @csrf @method('PATCH')
@@ -114,7 +129,7 @@
- + @csrf @method('PATCH')
- + @csrf @method('PATCH')
diff --git a/resources/views/submissions/fs-upload.blade.php b/resources/views/submissions/fs-upload.blade.php index 4d9bd2a..d6c7ed0 100644 --- a/resources/views/submissions/fs-upload.blade.php +++ b/resources/views/submissions/fs-upload.blade.php @@ -28,7 +28,7 @@ {{-- File listing. Used for editions or server-side errors. --}} -
+
@endif diff --git a/resources/views/submissions/index.blade.php b/resources/views/submissions/index.blade.php new file mode 100644 index 0000000..1be0954 --- /dev/null +++ b/resources/views/submissions/index.blade.php @@ -0,0 +1,73 @@ +@extends('layouts.app') + +@section('page-title', 'Submit - ' . config('app.name')) + +@section('content') + +{{ \Diglactic\Breadcrumbs\Breadcrumbs::render() }} +
Submit content
+ +
+ + + +
+ + @foreach($entryTypes as $type) + +
+
+ +
+
+
{{ $type['label'] }}
+
+
+
+ + Submit + +
+
+ @endforeach + +
+ + + + + + + +
+
+ 1 +

One entry per submission. Do not bundle multiple hacks or versions in a single form.

+
+
+ 2 +

You must be the author or have explicit permission from the original creator.

+
+
+ 3 +

Patch files only. Never upload ROM files — attach an IPS, BPS or xdelta patch.

+
+
+ +
+ +@endsection diff --git a/resources/views/submissions/play-online-core-select.blade.php b/resources/views/submissions/play-online-core-select.blade.php new file mode 100644 index 0000000..243e041 --- /dev/null +++ b/resources/views/submissions/play-online-core-select.blade.php @@ -0,0 +1,8 @@ +
+ + +
diff --git a/resources/views/tools/patcher.blade.php b/resources/views/tools/patcher.blade.php index 2bf120a..0982fa5 100644 --- a/resources/views/tools/patcher.blade.php +++ b/resources/views/tools/patcher.blade.php @@ -4,6 +4,7 @@ @push('scripts') + @vite('resources/js/RomPatcher.js') @endpush @section('content') @@ -88,5 +89,9 @@ Apply patch
+ +
@endsection diff --git a/resources/views/tools/play.blade.php b/resources/views/tools/play.blade.php new file mode 100644 index 0000000..a7baac7 --- /dev/null +++ b/resources/views/tools/play.blade.php @@ -0,0 +1,102 @@ +@extends('layouts.app') + +@section('page-title', "Play Online - " . config('app.name')) + +@push('scripts') + + @vite('resources/js/PlayOnlineAndPatcher.js') +@endpush + +@section('content') +
+ Play Online +
+ +
+ +
+ +
+
+ +
+ + + + +
+
+ +
+ + +
+ + + + +
+ +
+
+
+ +
+
+
Patch file
+
Select if there is multiple patch files
+
+
+ +
+ +
+
+
+
+ +
+
+
Checksums:
+
    +
  • CRC32:
  • +
  • MD5:
  • +
  • SHA-1:
  • +
+
+ +
+
Description:
+
+
+
+
ROM requirements:
+
+
+
+ +
+ +
+ + + +
+
+
+@endsection diff --git a/routes/api.php b/routes/api.php index 396b5c6..600b3e1 100644 --- a/routes/api.php +++ b/routes/api.php @@ -30,8 +30,17 @@ Route::get( '/dynamic/hovercard/{user_id}', [ \App\Http\Controllers\DynamicLoadC ->name('dynamic.hovercard') ->middleware('throttle:60,1') ; +Route::get('/dynamic/activity/feed', [ \App\Http\Controllers\DynamicLoadController::class, 'activityFeed' ] ) + ->name('dynamic.activity.feed') + ->middleware('throttle:60,1') +; Route::middleware('xf.auth')->controller(\App\Http\Controllers\DynamicLoadController::class)->name('dynamic.')->prefix('/dynamic/')->group(function(){ Route::get('/notifications', 'getNotifications' )->name('notifications'); Route::post('/notifications/mark-all-read', 'markAllRead' )->name('markallread'); Route::get('/conversations', 'getConversations' )->name('conversations'); }); + +// EntryFeaturedRequestController +Route::middleware('xf.auth')->name('featured.')->controller(\App\Http\Controllers\EntryFeaturedRequestController::class)->group(function(){ + Route::post('/entry/{entry:id}/featured', 'featuredRequest' )->name('request')->where(['entry' => '[0-9]+']); +}); diff --git a/routes/breadcrumbs.php b/routes/breadcrumbs.php index e89cd0c..0e20df6 100644 --- a/routes/breadcrumbs.php +++ b/routes/breadcrumbs.php @@ -15,7 +15,15 @@ Breadcrumbs::for('entries.show', function ( Generator $trail, string $section, \ $trail->parent('entries.index'); $trail->push($entry->title, route('entries.show', [ $section, $entry ] ) ); }); +Breadcrumbs::for('entries.drafts', function ( Generator $trail ) { + $trail->parent('home'); + $trail->push('Drafts', route('entries.drafts')); +}); +Breadcrumbs::for('submit.index', function ( Generator $trail ) { + $trail->parent('home'); + $trail->push('Submit', route('submit.index')); +}); Breadcrumbs::for('submit.create', function ( Generator $trail, string $section ) { $trail->parent('home'); $trail->push('Submit', route('submit.create', [ $section ] ) ); @@ -24,3 +32,25 @@ Breadcrumbs::for('submit.edit', function ( Generator $trail, string $section, \A $trail->parent('entries.show', $section, $entry); $trail->push('Edit', route('submit.edit', [ $section, $entry ] ) ); }); + +Breadcrumbs::for('news.index', function ( Generator $trail ) { + $trail->parent('home'); + $trail->push('News', route('news.index')); +}); +Breadcrumbs::for('news.show', function ( Generator $trail, \App\Models\News $news ) { + $trail->parent('news.index'); + $trail->push($news->title, route('news.show', [ $news ] ) ); +}); +Breadcrumbs::for('news.create', function ( Generator $trail ) { + $trail->parent('home'); + $trail->push('Submit', route('news.create') ); +}); +Breadcrumbs::for('news.edit', function ( Generator $trail, \App\Models\News $news ) { + $trail->parent('news.show', $news); + $trail->push('Edit', route('news.edit', [ $news ] ) ); +}); + +Breadcrumbs::for('queue.index', function ( Generator $trail ) { + $trail->parent('home'); + $trail->push('Submissions Queue', route('queue.index') ); +}); diff --git a/routes/console.php b/routes/console.php index b4b2ab6..87f95c9 100644 --- a/routes/console.php +++ b/routes/console.php @@ -8,3 +8,4 @@ Artisan::command('inspire', function () { })->purpose('Display an inspiring quote'); Schedule::command('entries:purge-rejected')->daily(); +Schedule::command('entries:purge-featured')->daily(); diff --git a/routes/web.php b/routes/web.php index b6db94d..9e25b22 100644 --- a/routes/web.php +++ b/routes/web.php @@ -29,9 +29,9 @@ Route::name('entries.')->controller(EntryController::class)->group(function () { // SubmissionController. Route::name('submit.')->prefix('/submit')->controller(\App\Http\Controllers\SubmissionController::class)->middleware(['xf.auth', 'can:create,\App\Models\Entry'])->group(function () { + Route::get('/', 'index')->name('index'); Route::get('/{section}', 'create' )->name('create') ->where([ 'section' => 'translations|romhacks|homebrew|utilities|documents|lua-scripts' ]); - Route::post('/{section}', 'store' )->name('store') ->where([ 'section' => 'translations|romhacks|homebrew|utilities|documents|lua-scripts' ]); }); @@ -44,6 +44,19 @@ Route::name('submit.')->prefix('/edit')->controller(\App\Http\Controllers\Submis ->where([ 'section' => 'translations|romhacks|homebrew|utilities|documents|lua-scripts', 'entry' => '[0-9\-]+' ]); }); +// NewsController +Route::name('news.')->controller(\App\Http\Controllers\NewsController::class)->group(function () { + Route::get('/news/', 'index' )->name('index'); + Route::get('/news/{news:slug}', 'show' )->name('show')->where([ 'news' => '[a-zA-Z0-9\-_]+']); + + Route::get('/submit/news', 'create' )->name('create')->middleware(['xf.auth','can:create,\App\Models\News']); + Route::post('/submit/news', 'store' )->name('store')->middleware(['xf.auth','can:create,\App\Models\News']); + + Route::get('/edit/news/{news:id}', 'edit')->name('edit')->where(['news' => '[0-9\-]+'])->middleware(['xf.auth','can:update,news']); + Route::post('/edit/news/{news:id}', 'update')->name('update')->where(['news' => '[0-9\-]+'])->middleware(['xf.auth','can:update,news']); + Route::delete('/edit/news/{news:id}', 'destroy')->name('destroy')->where(['news' => '[0-9\-]+'])->middleware(['xf.auth','can:update,news']); +}); + // QueueController Route::name('queue.')->prefix('/queue')->controller(\App\Http\Controllers\QueueController::class)->group(function () { Route::get('/', 'index' )->name('index'); @@ -62,6 +75,21 @@ Route::name('queue.')->prefix('/queue')->controller(\App\Http\Controllers\QueueC ->middleware(['xf.auth', 'can:reject,entry' ] ) ->where([ 'entry' => '[0-9\-]+' ]) ->name('reject'); + + Route::patch('/news/{news:id}/comment', 'updateComment_news' ) + ->middleware(['xf.auth', 'can:updateComment,news' ] ) + ->where([ 'news' => '[0-9\-]+' ]) + ->name('news.comment'); + + Route::patch('/news/{news:id}/approve', 'approve_news' ) + ->middleware(['xf.auth', 'can:approve,news' ] ) + ->where([ 'news' => '[0-9\-]+' ]) + ->name('news.approve'); + + Route::patch('/news/{news:id}/reject', 'reject_news' ) + ->middleware(['xf.auth', 'can:reject,news' ] ) + ->where([ 'news' => '[0-9\-]+' ]) + ->name('news.reject'); }); // ToolsController @@ -69,6 +97,8 @@ Route::name('tools.')->controller(\App\Http\Controllers\ToolsController::class)- Route::get('/patch', 'patcher' )->name('patcher'); Route::get( '/patch/{entry_id}/{file:file_uuid}', 'directPatch' )->name('direct-patch') ->where(['entry_id' => '[0-9]+']); + Route::get('/play/{entry_id}/{file:file_uuid}', 'play' )->name('play')->middleware('xf.auth') + ->where(['entry_id' => '[0-9]+']); }); // ModeratorCPController @@ -79,6 +109,7 @@ Route::name('modcp.')->prefix('/modcp')->controller(\App\Http\Controllers\ModCPC Route::get('/draft-entries', 'draft' )->name('draft')->middleware('can:is-admin'); Route::get('/hidden-entries', 'hidden' )->name('hidden')->middleware('can:is-admin'); Route::get('/deleted-entries', 'deleted' )->name('deleted')->middleware('can:is-admin'); + Route::get('/logs', 'logs' )->name('logs')->middleware('can:is-admin'); Route::patch('/restore/{entry}', 'restore' )->name('restore')->where(['entry' => '[0-9\-]+'])->withTrashed()->middleware('can:is-admin'); Route::delete('/purge/{entry}', 'destroy' )->name('destroy')->where(['entry' => '[0-9\-]+'])->withTrashed()->middleware('can:is-admin'); @@ -93,4 +124,5 @@ Route::name('modcp.')->prefix('/modcp')->controller(\App\Http\Controllers\ModCPC // RedirectController Route::name('redirect.')->controller(\App\Http\Controllers\RedirectController::class)->group(function () { Route::get('/entry/report_redirect', 'entryReportRedirect' )->name('entry_report'); + Route::get('/news/report_redirect', 'newsReportRedirect' )->name('news_report'); }); diff --git a/vite.config.js b/vite.config.js index 7b5ed4b..9f1c62d 100644 --- a/vite.config.js +++ b/vite.config.js @@ -6,7 +6,10 @@ import tailwindcss from '@tailwindcss/vite'; export default defineConfig({ plugins: [ laravel({ - input: ['resources/css/app.css', 'resources/js/app.js', 'resources/js/submissions.js'], + input: ['resources/css/app.css', 'resources/js/app.js', + 'resources/js/submissions.js', 'resources/js/news-submissions.js', + 'resources/js/RomPatcher.js', 'resources/js/PlayOnlineAndPatcher.js' + ], refresh: true, fonts: [ bunny('Instrument Sans', { -- 2.39.5 From 279160c1cb33e3e9dcbc81f03fae2a0cd36ee54b Mon Sep 17 00:00:00 2001 From: Benjamin Date: Tue, 16 Jun 2026 18:35:01 +0200 Subject: [PATCH 3/4] Added Download file, play for homebrews and ZIP explorer. --- app/Http/Controllers/FileServerController.php | 8 ++-- app/Http/Controllers/ToolsController.php | 21 +++++---- app/Models/EntryFile.php | 17 ++++++++ app/Services/FileServersService.php | 35 +++++++++++++-- app/Services/SubmissionsService.php | 14 ++++-- resources/js/PlayOnline.js | 43 +++++++++++++++++++ resources/js/SubmissionsClass/FSFileData.js | 11 ++++- resources/js/SubmissionsClass/FSUploader.js | 25 +++++++++++ .../livewire/entry-files-modal.blade.php | 7 ++- .../views/submissions/fs-upload.blade.php | 38 +++++++++++++++- resources/views/tools/play-homebrew.blade.php | 17 ++++++++ vite.config.js | 3 +- 12 files changed, 215 insertions(+), 24 deletions(-) create mode 100644 resources/js/PlayOnline.js create mode 100644 resources/views/tools/play-homebrew.blade.php diff --git a/app/Http/Controllers/FileServerController.php b/app/Http/Controllers/FileServerController.php index 5a03c77..2054018 100644 --- a/app/Http/Controllers/FileServerController.php +++ b/app/Http/Controllers/FileServerController.php @@ -77,9 +77,11 @@ class FileServerController extends Controller { abort(404); } - if( !EntryHelpers::fileAlreadyDownloaded($file) ) { - EntryHelpers::markFileAsDownloaded($file); - $file->increaseDownloadCount(); + if($request->input('count_download', true)) { + if (!EntryHelpers::fileAlreadyDownloaded($file)) { + EntryHelpers::markFileAsDownloaded($file); + $file->increaseDownloadCount(); + } } return redirect( $this->fs->getDownloadFileUrl( $file) ); diff --git a/app/Http/Controllers/ToolsController.php b/app/Http/Controllers/ToolsController.php index 9e54d91..36fa137 100644 --- a/app/Http/Controllers/ToolsController.php +++ b/app/Http/Controllers/ToolsController.php @@ -12,13 +12,12 @@ class ToolsController extends Controller public function patcher() { - return view('tools.patcher'); } public function directPatch( Request $request, int $entryId, EntryFile $file ) { - if( $file->entry_id != $entryId ) { + if( $file->entry_id != $entryId || $file->state === 'private' ) { abort(404); } @@ -38,22 +37,28 @@ class ToolsController extends Controller public function play( Request $request, int $entryId, EntryFile $file ) { - if( $file->entry_id != $entryId ) { + if( $file->entry_id != $entryId || $file->state === 'private' ) { abort(404); } $service = app(FileServersService::class); - $patches = [ - 'file' => $service->getDownloadFileUrl( $file ), - 'name' => $file->entry->title, - 'outputName' => $file->filename - ]; $emuConfig = [ 'core' => $file->playOnlineSetting?->core, 'threads' => $file->playOnlineSetting?->threads, ]; + if( $file->entry->type === 'homebrew' ){ + $filePath = $service->getDownloadFileUrl( $file ); + + return view('tools.play-homebrew', compact('filePath', 'emuConfig')); + } + $patches = [ + 'file' => $service->getDownloadFileUrl( $file ), + 'name' => $file->entry->title, + 'outputName' => $file->filename + ]; + return view('tools.play', compact('patches', 'emuConfig')); } } diff --git a/app/Models/EntryFile.php b/app/Models/EntryFile.php index a213637..69df869 100644 --- a/app/Models/EntryFile.php +++ b/app/Models/EntryFile.php @@ -62,6 +62,23 @@ class EntryFile extends Model return $this->hasOne(PlayOnlineSetting::class,'file_id'); } + public function prettyFileSize(): string + { + $bytes = $this->filesize; + + if ($bytes >= 1073741824) { + $bytes = number_format($bytes / 1073741824, 2) . ' GB'; + } elseif ($bytes >= 1048576) { + $bytes = number_format($bytes / 1048576, 2) . ' MB'; + } elseif ($bytes >= 1024) { + $bytes = number_format($bytes / 1024, 2) . ' KB'; + } else { + $bytes = ($bytes == 0) ? '0 bytes' : $bytes . ' bytes'; + } + + return $bytes; + } + public function increaseDownloadCount(): void { $this->download_count++; diff --git a/app/Services/FileServersService.php b/app/Services/FileServersService.php index 742db53..d798e5f 100644 --- a/app/Services/FileServersService.php +++ b/app/Services/FileServersService.php @@ -8,6 +8,8 @@ use Illuminate\Http\Client\ConnectionException; use Illuminate\Http\UploadedFile; use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Cache; +use Illuminate\Support\Facades\Log; +use Illuminate\Support\Str; class FileServersService { @@ -69,15 +71,38 @@ class FileServersService { * @param EntryFile $file * @return string */ - public function getDownloadFileUrl( EntryFile $file ): string + public function getDownloadFileUrl( EntryFile $file, bool $countDownload = true ): string { $serverKey = $this->getEntryFileServerKey( $file ); $url = $this->servers[$serverKey]['download'] ?? "#"; if( $url === "#" ) return $url; - return $url . "&" . http_build_query( [ 'filename' => $file->filename, 'filepath' => $file->filepath ] ); + $args = [ 'filename' => $file->filename, 'filepath' => $file->filepath ]; + if( !$countDownload ) + $args['count_download'] = false; + return $url . "&" . http_build_query( $args ); + + } + + + public function getArchiveExplorerUrl( EntryFile $file ): ?string + { + if( !Str::endsWith( $file->filename, ['zip', 'rar', '7z'] ) ) + return null; + + $serverKey = $this->getEntryFileServerKey( $file ); + $url = $this->servers[$serverKey]['file_explorer'] ?? "#"; + + if( $url === "#" ) + return null; + + $args = [ 'filename' => $file->filename, 'filepath' => $file->filepath, + 'zeus' => $this->generateZeusToken(\Auth::user()->user_id ?? 0, $this->servers[$serverKey]['base_url'], 'Fileexplorer') + ]; + + return $url . "&" . http_build_query( $args ); } /** @@ -137,11 +162,13 @@ class FileServersService { foreach( $this->servers as $serverKey => $server ){ - $response = Http::withHeaders([]) + $token = $this->generateZeusToken( $userId, $server['base_url'], "Deletefile" ); + + $response = Http::asForm()->withHeaders([]) ->post( $server['delete_file'], [ 'filepath' => $filePath, 'filename' => $fileName, - 'zeus' => $this->generateZeusToken( $userId, $server['base_url'], "Deletefile" ), + 'zeus' => $token, ]); if (!$response->successful()) { diff --git a/app/Services/SubmissionsService.php b/app/Services/SubmissionsService.php index 15130cf..e89c394 100644 --- a/app/Services/SubmissionsService.php +++ b/app/Services/SubmissionsService.php @@ -63,8 +63,10 @@ class SubmissionsService { if( $files === [] ) return []; + $service = app(FileServersService::class); + return array_map( - function( string $uuid ) { + function( string $uuid ) use ($service) { $file = EntryFile::where('file_uuid', $uuid)->first(); if( $file ) @@ -78,6 +80,9 @@ class SubmissionsService { 'error' => null, 'uuid' => $uuid, 'state' => $file->state, + 'file_explorer' => $service->getArchiveExplorerUrl($file), + 'file_explorer_files' => null, + 'download_url' => $service->getDownloadFileUrl($file, false), 'can_be_online_patched' => EntryHelpers::enableOnlinePatcherBasedOnExtension($file['filename']), 'meta_online_patcher' => $file->online_patcher, 'meta_secondary_online_patcher' => $file->secondary_online_patcher, @@ -98,6 +103,9 @@ class SubmissionsService { 'error' => null, 'uuid' => $uuid, 'state' => $file['state'], + 'file_explorer' => null, + 'file_explorer_files' => null, + 'download_url' => null, 'can_be_online_patched' => EntryHelpers::enableOnlinePatcherBasedOnExtension($file['filename']), 'meta_online_patcher' => false, 'meta_secondary_online_patcher' => false, @@ -707,10 +715,10 @@ class SubmissionsService { $needDeletion = array_diff( $existingUuids, $requestUuids ); if( !empty( $needDeletion ) ){ $userId = \Auth::user()->user_id; - EntryFile::where('entry_id', $entryId)->whereIn('file_uuid', $needDeletion)->get()->each( function ( $f ) use ( $userId ) { + EntryFile::where('entry_id', $entryId)->whereIn('file_uuid', $needDeletion)->whereNot('state', 'archived')->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)->whereNot('state', 'archived')->delete(); } $needAddition = array_diff( $requestUuids, $existingUuids ); diff --git a/resources/js/PlayOnline.js b/resources/js/PlayOnline.js new file mode 100644 index 0000000..4950b03 --- /dev/null +++ b/resources/js/PlayOnline.js @@ -0,0 +1,43 @@ +window.PlayOnline = function( filePath = "", emulatorJsConfig = {} ){ + + return { + + fileUrl: filePath, + emuConfig: emulatorJsConfig, + + init(){ + this.launchEmulatorJs(); + }, + + 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.filePath; + window.EJS_pathtodata = "https://cdn.emulatorjs.org/stable/data/"; + window.EJS_startOnLoaded = true; + window.EJS_threads = this.emuConfig.threads ?? false; + }, + + launchEmulatorJs(){ + + 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; + + } + + } + +} diff --git a/resources/js/SubmissionsClass/FSFileData.js b/resources/js/SubmissionsClass/FSFileData.js index 6c4fc13..623e885 100644 --- a/resources/js/SubmissionsClass/FSFileData.js +++ b/resources/js/SubmissionsClass/FSFileData.js @@ -1,6 +1,6 @@ /** @typedef { import('types/UploadchunkResponse.js').UploadchunkResponse} UploadchunkResponse */ -export const CHUNK_SIZE = 8192; +export const CHUNK_SIZE = 8192 * 1024; const PATCH_EXTENSIONS = new Set([ 'ips', 'bps', 'ups', 'aps', 'ppf', 'xdelta', "zip" @@ -72,6 +72,15 @@ export function FSFileData(name, totalChunks, rawFile ) { */ state: 'public', + file_explorer: null, + + file_explorer_files: null, + + /** + * For files already uploaded, download URL. + */ + download_url: null, + can_be_online_patched: PATCH_EXTENSIONS.has(extension), /** diff --git a/resources/js/SubmissionsClass/FSUploader.js b/resources/js/SubmissionsClass/FSUploader.js index 96af2ed..8c7a597 100644 --- a/resources/js/SubmissionsClass/FSUploader.js +++ b/resources/js/SubmissionsClass/FSUploader.js @@ -105,6 +105,29 @@ export function FSUploader(){ }, + handleDownloadFile( index ){ + let download_url = this.files[index].download_url; + window.location.href = download_url; + }, + + async handleFileExplorer( index ){ + + if( this.files[index].file_explorer_files !== null ) + return; + + let file_explorer_url = this.files[index].file_explorer; + + let response = await fetch(file_explorer_url, { method: 'GET', headers: { 'Content-Type': 'application/json' } }); + let json = await response.json(); + + if( !json.files ){ + this.files[index].file_explorer_files = [ "An error occurred during request" ]; + return; + } + + this.files[index].file_explorer_files = json.files; + }, + /** * Retry file uploading. * @@ -126,6 +149,8 @@ export function FSUploader(){ * @param {number} index FSFileData index in this.files. */ handleRemoveFile( index ){ + if( this.files[index].state === 'archived') + return; this.files.splice(index, 1); }, diff --git a/resources/views/livewire/entry-files-modal.blade.php b/resources/views/livewire/entry-files-modal.blade.php index dfb2817..14b3e2c 100644 --- a/resources/views/livewire/entry-files-modal.blade.php +++ b/resources/views/livewire/entry-files-modal.blade.php @@ -17,10 +17,13 @@