From 14938f63dfe05ab3caf1bdd72691dce78bacc427 Mon Sep 17 00:00:00 2001 From: User Date: Sat, 18 Jan 2025 19:05:23 -0500 Subject: [PATCH] 0.1.3 --- app/Http/Controllers/ComicController.php | 170 ++++++++++-------- app/Jobs/ComicUpsert.php | 71 ++++++++ app/Jobs/ImageUpsert.php | 9 +- app/Jobs/RemotePrefetch.php | 29 ++- app/Remote/CopyManga.php | 4 +- app/Remote/ImageFetcher.php | 2 +- bootstrap/app.php | 5 + bun.lockb | Bin 170744 -> 171182 bytes composer.json | 1 + composer.lock | 124 ++++++++++--- config/queue-monitor.php | 60 +++++++ ...2_05_000000_create_queue_monitor_table.php | 55 ++++++ package.json | 6 +- public/vendor/queue-monitor/app.css | 1 + resources/js/Layouts/AppLayout.jsx | 44 ----- resources/js/Pages/Comic/Histories.jsx | 8 +- resources/js/Pages/Comic/HistoriesByComic.jsx | 92 ++++++++++ resources/js/Pages/Comic/Index.jsx | 24 ++- resources/js/Pages/Comic/Read.jsx | 54 +++++- resources/js/Pages/Pages/Installation.jsx | 1 + resources/js/Pages/Pages/Updates.jsx | 16 ++ resources/js/components/ui/app-sidebar.jsx | 53 +++++- resources/views/errors.blade.php | 0 23 files changed, 652 insertions(+), 177 deletions(-) create mode 100644 app/Jobs/ComicUpsert.php create mode 100644 config/queue-monitor.php create mode 100644 database/migrations/2018_02_05_000000_create_queue_monitor_table.php create mode 100644 public/vendor/queue-monitor/app.css create mode 100644 resources/js/Pages/Comic/HistoriesByComic.jsx create mode 100644 resources/views/errors.blade.php diff --git a/app/Http/Controllers/ComicController.php b/app/Http/Controllers/ComicController.php index c6c7f2b..b69fafd 100644 --- a/app/Http/Controllers/ComicController.php +++ b/app/Http/Controllers/ComicController.php @@ -3,11 +3,14 @@ namespace App\Http\Controllers; use App\Helper\ZhConversion; +use App\Jobs\ComicInsert; +use App\Jobs\ComicUpsert; use App\Jobs\ImageUpsert; use App\Jobs\RemotePrefetch; use App\Models\Author; use App\Models\Chapter; use App\Models\Comic; +use App\Models\ReadingHistory; use App\Remote\CopyManga; use App\Remote\ImageFetcher; use GuzzleHttp\Exception\GuzzleException; @@ -19,6 +22,7 @@ use Illuminate\Http\JsonResponse; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; use Illuminate\Http\Response as IlluminateHttpResponse; +use Illuminate\Support\Collection; use Illuminate\Support\Facades\Cache; use Inertia\Inertia; use Inertia\Response; @@ -30,12 +34,15 @@ class ComicController extends Controller { /** + * Autoload classes + * * @param CopyManga $copyManga * @param ZhConversion $zhConversion */ - public function __construct(private readonly CopyManga $copyManga, private readonly ZhConversion $zhConversion) - { - } + public function __construct( + private readonly CopyManga $copyManga, + private readonly ZhConversion $zhConversion + ) {} /** * Convert SC to ZH @@ -61,10 +68,8 @@ class ComicController extends Controller */ public function favourites(Request $request): Response { - $favourites = $this->scToZh($request->user()->favourites()->with(['authors'])->orderBy('upstream_updated_at', 'desc')->get()); - return Inertia::render('Comic/Favourites', [ - 'favourites' => $favourites, + 'favourites' => $this->scToZh($request->user()->favourites()->with(['authors'])->orderBy('upstream_updated_at', 'desc')->get()), ]); } @@ -84,16 +89,16 @@ class ComicController extends Controller // Fetch from remote $remoteComic = $this->copyManga->comic($request->pathword); - $comic = new Comic; - $comic->pathword = $remoteComic['comic']['path_word']; - $comic->name = $remoteComic['comic']['name']; - $comic->cover = $remoteComic['comic']['cover']; - $comic->upstream_updated_at = $remoteComic['comic']['datetime_updated']; - $comic->uuid = $remoteComic['comic']['uuid']; - $comic->alias = explode(',', $remoteComic['comic']['alias']); - $comic->description = $remoteComic['comic']['brief']; - $comic->metadata = $remoteComic; - $comic->save(); + $comic = Comic::create([ + 'pathword' => $remoteComic['comic']['path_word'], + 'name' => $remoteComic['comic']['name'], + 'cover' => $remoteComic['comic']['cover'], + 'upstream_updated_at' => $remoteComic['comic']['datetime_updated'], + 'uuid' => $remoteComic['comic']['uuid'], + 'alias' => explode(',', $remoteComic['comic']['alias']), + 'description' => $remoteComic['comic']['brief'], + 'metadata' => $remoteComic, + ]); } // Set favourite @@ -125,47 +130,6 @@ class ComicController extends Controller ]); } - // Internal function for Upsert comics to db - protected function comicsUpsert($comics): void - { - // Prep the array for upsert - $comicsUpsertArray = []; - $authorsUpsertArray = []; - - foreach ($comics['list'] as $comic) { - $comicsUpsertArray[] = [ - 'pathword' => $comic['path_word'], - 'uuid' => '', - 'name' => $comic['name'], - 'alias' => '{}', - 'description' => '', - 'cover' => $comic['cover'], - 'upstream_updated_at' => $comic['datetime_updated'] ?? null, - ]; - - foreach ($comic['author'] as $author) { - $authorsUpsertArray[] = [ - 'name' => $author['name'] - ]; - } - } - - // Do an upsert for comics - Comic::upsert($comicsUpsertArray, uniqueBy: 'pathword', update: ['upstream_updated_at']); - Author::upsert($authorsUpsertArray, uniqueBy: 'name'); - - // Had to do a second pass to insert the relationships - foreach ($comics['list'] as $comic) { - // Get the comic id - $comicObj = Comic::where('pathword', $comic['path_word'])->first(); - - foreach ($comic['author'] as $author) { - $authorObj = Author::where('name', $author['name'])->first(); - $comicObj->authors()->sync($authorObj); - } - } - } - /** * Index / Tags for comic listing * @@ -180,12 +144,23 @@ class ComicController extends Controller $params['theme'] = $request->get('tag'); } - $comics = $this->copyManga->comics(30, $request->header('offset', 0), $request->get('top', 'all'), $params); - $this->comicsUpsert($comics); + $offset = $request->header('offset', 0); + $comics = $this->copyManga->comics(30, $offset, $request->get('top', 'all'), $params); + + // Upsert into DB + ComicUpsert::dispatch($comics); + + // Prefetch next page + RemotePrefetch::dispatch('comics', [ + 'limit' => 30, + 'offset' => $offset + 30, + 'top' => $request->get('top', 'all'), + 'params' => $params + ])->delay(now()->addSecond()); return Inertia::render('Comic/Index', [ 'comics' => $this->scToZh($comics), - 'offset' => $request->header('offset', 0) + 'offset' => $offset ]); } @@ -199,15 +174,15 @@ class ComicController extends Controller */ public function author(Request $request, string $author): Response { - $params = []; - $params['author'] = $author; + $offset = $request->header('offset', 0); + $comics = $this->copyManga->comics(30, $offset, $request->get('top', 'all'), ['author' => $author]); - $comics = $this->copyManga->comics(30, $request->header('offset', 0), $request->get('top', 'all'), $params); - $this->comicsUpsert($comics); + // Upsert into DB + ComicUpsert::dispatch($comics); return Inertia::render('Comic/Index', [ 'comics' => $this->scToZh($comics), - 'offset' => $request->header('offset', 0) + 'offset' => $offset ]); } @@ -221,13 +196,14 @@ class ComicController extends Controller */ public function search(Request $request, string $search): Response { - $comics = $this->copyManga->search($search, 30, $request->header('offset', 0)); + $offset = $request->header('offset', 0); + $comics = $this->copyManga->search($search, 30, $offset); // Search API is limited, no upsert return Inertia::render('Comic/Index', [ 'comics' => $this->scToZh($comics), - 'offset' => $request->header('offset', 0) + 'offset' => $offset ]); } @@ -245,8 +221,10 @@ class ComicController extends Controller return to_route('comics.index'); } + $offset = $request->header('offset', 0); + $comic = $this->copyManga->comic($pathword); - $chapters = $this->copyManga->chapters($pathword, 200, $request->header('offset', 0), [], $request->get('group', 'default')); + $chapters = $this->copyManga->chapters($pathword, 200, $offset, [], $request->get('group', 'default')); // Get the comic object and fill other parameters try { @@ -297,15 +275,20 @@ class ComicController extends Controller // Do an upsert Chapter::upsert($arrayForUpsert, uniqueBy: 'chapter_uuid'); - // Get history + // Get user history $histories = $request->user()->readingHistories()->where('reading_histories.comic_id', $comicObject->id) ->distinct()->select('chapter_uuid')->get()->pluck('chapter_uuid'); + // Check if the first chapter is already fetched or not, if not, do a prefetch + if (!$histories->contains($chapters['list'][0]['uuid'])) { + RemotePrefetch::dispatch('chapter', ['pathword' => $pathword, 'uuid' => $chapters['list'][0]['uuid']]); + } + return Inertia::render('Comic/Chapters', [ 'comic' => $this->scToZh($comic), 'chapters' => $this->scToZh($chapters), 'histories' => $histories, - 'offset' => $request->header('offset', 0) + 'offset' => $offset ]); } @@ -351,7 +334,7 @@ class ComicController extends Controller try { Chapter::where('chapter_uuid', $chapter['chapter']['next'])->firstOrFail()->images()->firstOrFail(); } catch (ModelNotFoundException $e) { - RemotePrefetch::dispatch('chapter', ['pathword' => $pathword, 'uuid' => $chapter['chapter']['next']]); + RemotePrefetch::dispatch('chapter', ['pathword' => $pathword, 'uuid' => $chapter['chapter']['next']]); } } @@ -370,7 +353,48 @@ class ComicController extends Controller */ public function histories(Request $request): Response { - // Get history + // Group by comic + if ($request->get('group', '') === 'comic') { + $readingHistory = ReadingHistory::query() + ->join('comics', 'reading_histories.comic_id', '=', 'comics.id') + ->join('chapters', 'reading_histories.chapter_id', '=', 'chapters.id') + ->select( + 'comics.id as comic_id', + 'comics.name as comic_name', + 'comics.pathword as comic_pathword', + 'comics.upstream_updated_at as comic_upstream_updated_at', + 'chapters.name as chapter_name', + 'chapters.chapter_uuid as chapter_uuid', + 'reading_histories.created_at as read_at' + ) + ->where('reading_histories.user_id', $request->user()->id) + ->orderBy('comics.name')->orderByDesc('reading_histories.created_at')->paginate(2000); + + // Transform the paginated data into a 2D array grouped by comic_id, including comic data and histories + $groupedData = $readingHistory->getCollection() + ->groupBy('comic_id')->map(function (Collection $records, $comicId) { + $firstRecord = $records->first(); + return [ + 'comic' => [ + 'comic_id' => $comicId, + 'comic_name' => $firstRecord->comic_name, + 'comic_pathword' => $firstRecord->comic_pathword, + 'comic_upstream_updated_at' => $firstRecord->comic_upstream_updated_at, + ], + 'histories' => $records->map(fn($record) => [ + 'chapter_name' => $record->chapter_name, + 'chapter_uuid' => $record->chapter_uuid, + 'read_at' => $record->read_at, + ])->toArray(), + ]; + })->values()->toArray(); // Use values() to reset numeric keys after grouping + + return Inertia::render('Comic/HistoriesByComic', [ + 'histories' => $this->scToZh($groupedData) + ]); + } + + // Only order by chapter $histories = $request->user()->readingHistories()->with(['comic:id,name,pathword'])->orderByDesc('reading_histories.created_at') ->select(['reading_histories.id as hid', 'chapters.comic_id', 'chapters.name'])->paginate(50)->toArray(); @@ -425,7 +449,7 @@ class ComicController extends Controller */ public function destroyHistories(Request $request): RedirectResponse { - $result = $request->user()->cleanUpReadingHistories(); + $request->user()->cleanUpReadingHistories(); return redirect()->route('comics.histories'); } diff --git a/app/Jobs/ComicUpsert.php b/app/Jobs/ComicUpsert.php new file mode 100644 index 0000000..13f13d1 --- /dev/null +++ b/app/Jobs/ComicUpsert.php @@ -0,0 +1,71 @@ +queueProgress(0); + Log::info("JOB ComicUpsert START"); + + $comicsUpsertArray = []; + $authorsUpsertArray = []; + + foreach ($this->comic['list'] as $comic) { + $comicsUpsertArray[] = [ + 'pathword' => $comic['path_word'], + 'uuid' => '', + 'name' => $comic['name'], + 'alias' => '{}', + 'description' => '', + 'cover' => $comic['cover'], + 'upstream_updated_at' => $comic['datetime_updated'] ?? null, + ]; + + foreach ($comic['author'] as $author) { + $authorsUpsertArray[] = [ + 'name' => $author['name'] + ]; + } + } + + // Do an upsert for comics + Comic::upsert($comicsUpsertArray, uniqueBy: 'pathword', update: ['upstream_updated_at']); + Author::upsert($authorsUpsertArray, uniqueBy: 'name'); + + // Had to do a second pass to insert the relationships + foreach ($this->comic['list'] as $comic) { + // Get the comic id + $comicObj = Comic::where('pathword', $comic['path_word'])->first(); + + foreach ($comic['author'] as $author) { + $authorObj = Author::where('name', $author['name'])->first(); + $comicObj->authors()->sync($authorObj); + } + } + + Log::info('JOB ComicUpsert END'); + $this->queueProgress(100); + } +} diff --git a/app/Jobs/ImageUpsert.php b/app/Jobs/ImageUpsert.php index c708f8f..de29b90 100644 --- a/app/Jobs/ImageUpsert.php +++ b/app/Jobs/ImageUpsert.php @@ -6,10 +6,11 @@ use App\Models\Image; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Queue\Queueable; use Illuminate\Support\Facades\Log; +use romanzipp\QueueMonitor\Traits\IsMonitored; class ImageUpsert implements ShouldQueue { - use Queueable; + use IsMonitored, Queueable; /** * Create a new job instance. @@ -25,7 +26,12 @@ class ImageUpsert implements ShouldQueue */ public function handle(): void { + $this->queueProgress(0); Log::info("JOB ImageUpsert START, comicId: {$this->comicId}, chapterId: {$this->chapterId}"); + $this->queueData([ + 'comicId' => $this->comicId, + 'chapterId' => $this->chapterId, + ]); $arrayForUpsert = []; @@ -47,5 +53,6 @@ class ImageUpsert implements ShouldQueue Image::upsert($arrayForUpsert, uniqueBy: 'url'); Log::info('JOB ImageUpsert END'); + $this->queueProgress(100); } } diff --git a/app/Jobs/RemotePrefetch.php b/app/Jobs/RemotePrefetch.php index 70da749..4474150 100644 --- a/app/Jobs/RemotePrefetch.php +++ b/app/Jobs/RemotePrefetch.php @@ -6,10 +6,11 @@ use App\Remote\CopyManga; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Queue\Queueable; use Illuminate\Support\Facades\Log; +use romanzipp\QueueMonitor\Traits\IsMonitored; class RemotePrefetch implements ShouldQueue { - use Queueable; + use IsMonitored, Queueable; /** * Create a new job instance. @@ -24,11 +25,17 @@ class RemotePrefetch implements ShouldQueue */ public function handle(): void { + $this->queueProgress(0); $copyManga = new CopyManga(); switch ($this->action) { case 'chapter': Log::info("JOB RemotePrefetch START, action '{$this->action}', Pathword: {$this->parameters['pathword']}, UUID: {$this->parameters['uuid']}"); + $this->queueData([ + 'action' => $this->action, + 'pathword' => $this->parameters['pathword'], + 'uuid' => $this->parameters['uuid'] + ]); $copyManga->chapter($this->parameters['pathword'], $this->parameters['uuid']); @@ -37,15 +44,25 @@ class RemotePrefetch implements ShouldQueue case 'chapters': // TODO: break; - case 'index': - // TODO - break; - case 'tags': - // TODO + case 'comics': + Log::info("JOB RemotePrefetch START, action '{$this->action}', Offset: {$this->parameters['offset']}"); + $this->queueData([ + 'action' => $this->action, + 'offset' => $this->parameters['offset'], + 'limit' => $this->parameters['limit'], + 'top' => $this->parameters['top'], + 'params' => $this->parameters['params'] + ]); + + $copyManga->comics($this->parameters['offset'], $this->parameters['limit'], $this->parameters['top'], $this->parameters['params']); + + Log::info("JOB RemotePrefetch END, action '{$this->action}'"); break; default: Log::info("JOB RemotePrefetch Unknown action '{$this->action}'"); break; } + + $this->queueProgress(100); } } diff --git a/app/Remote/CopyManga.php b/app/Remote/CopyManga.php index 4ee88d6..911dfe2 100644 --- a/app/Remote/CopyManga.php +++ b/app/Remote/CopyManga.php @@ -296,7 +296,7 @@ class CopyManga public function legacyChapter(string $comic, string $chapter): array { $userAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36"; - $responses = $this->execute($this->legacyBuildUrl("comic/{$comic}/chapter/{$chapter}"), "GET", $userAgent, ttl: 24 * 60 * 60); + $responses = $this->execute($this->legacyBuildUrl("comic/{$comic}/chapter/{$chapter}"), "GET", $userAgent, ttl: 24 * 60 * 60 * 30); // Get Content Key $dom = new DOMDocument(); @@ -346,7 +346,7 @@ class CopyManga */ public function chapter(string $comic, string $chapter, array $parameters = []): array { - $responses = $this->execute($this->buildUrl("comic/{$comic}/chapter2/{$chapter}", $parameters), ttl: 24 * 60 * 60); + $responses = $this->execute($this->buildUrl("comic/{$comic}/chapter2/{$chapter}", $parameters), ttl: 24 * 60 * 60 * 30); if ($this->legacyImagesFetch) { $responses['sorted'] = $this->legacyChapter($comic, $chapter); diff --git a/app/Remote/ImageFetcher.php b/app/Remote/ImageFetcher.php index f6ed4a4..adb9155 100644 --- a/app/Remote/ImageFetcher.php +++ b/app/Remote/ImageFetcher.php @@ -14,7 +14,7 @@ class ImageFetcher 'cache' => [ 'enabled' => true, 'prefix' => 'image_fetcher_', - 'ttl' => 3600, + 'ttl' => 60 * 60 * 24 * 30, ], 'http' => [ 'timeout' => 60, diff --git a/bootstrap/app.php b/bootstrap/app.php index 682ced6..c3fcbff 100644 --- a/bootstrap/app.php +++ b/bootstrap/app.php @@ -1,6 +1,7 @@ withExceptions(function (Exceptions $exceptions) { + $exceptions->render(function (ServerException $e, Request $request) { + return response()->view('errors', status: 500); + }); + Integration::handles($exceptions); })->create(); diff --git a/bun.lockb b/bun.lockb index d95ba2f57aa5d2308101702eb953ec0c00324e94..5514e1a6cecde44045812aad248664c06d7f154d 100755 GIT binary patch delta 8622 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 delta 8483 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 VSym?$7_*+OLUfD$PGP&zzX3+F{FVR! diff --git a/composer.json b/composer.json index 2f82cfa..1794416 100644 --- a/composer.json +++ b/composer.json @@ -15,6 +15,7 @@ "laravel/tinker": "^2.9", "plesk/ext-laravel-integration": "^7.0", "predis/predis": "^2.0", + "romanzipp/laravel-queue-monitor": "^5.3", "sentry/sentry-laravel": "^4.10", "tightenco/ziggy": "^2.0" }, diff --git a/composer.lock b/composer.lock index ba3b1e5..c6ee78f 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": "641b7b21cd3231c89ab305cd615eeb6f", + "content-hash": "6f784bba21b3b875b87a164afa37ff36", "packages": [ { "name": "brick/math", @@ -1189,16 +1189,16 @@ }, { "name": "laravel/framework", - "version": "v11.37.0", + "version": "v11.38.2", "source": { "type": "git", "url": "https://github.com/laravel/framework.git", - "reference": "6cb103d2024b087eae207654b3f4b26646119ba5" + "reference": "9d290aa90fcad44048bedca5219d2b872e98772a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/framework/zipball/6cb103d2024b087eae207654b3f4b26646119ba5", - "reference": "6cb103d2024b087eae207654b3f4b26646119ba5", + "url": "https://api.github.com/repos/laravel/framework/zipball/9d290aa90fcad44048bedca5219d2b872e98772a", + "reference": "9d290aa90fcad44048bedca5219d2b872e98772a", "shasum": "" }, "require": { @@ -1399,20 +1399,20 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2025-01-02T20:10:21+00:00" + "time": "2025-01-15T00:06:46+00:00" }, { "name": "laravel/prompts", - "version": "v0.3.2", + "version": "v0.3.3", "source": { "type": "git", "url": "https://github.com/laravel/prompts.git", - "reference": "0e0535747c6b8d6d10adca8b68293cf4517abb0f" + "reference": "749395fcd5f8f7530fe1f00dfa84eb22c83d94ea" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/prompts/zipball/0e0535747c6b8d6d10adca8b68293cf4517abb0f", - "reference": "0e0535747c6b8d6d10adca8b68293cf4517abb0f", + "url": "https://api.github.com/repos/laravel/prompts/zipball/749395fcd5f8f7530fe1f00dfa84eb22c83d94ea", + "reference": "749395fcd5f8f7530fe1f00dfa84eb22c83d94ea", "shasum": "" }, "require": { @@ -1456,9 +1456,9 @@ "description": "Add beautiful and user-friendly forms to your command-line applications.", "support": { "issues": "https://github.com/laravel/prompts/issues", - "source": "https://github.com/laravel/prompts/tree/v0.3.2" + "source": "https://github.com/laravel/prompts/tree/v0.3.3" }, - "time": "2024-11-12T14:59:47+00:00" + "time": "2024-12-30T15:53:31+00:00" }, { "name": "laravel/sanctum", @@ -3681,6 +3681,76 @@ ], "time": "2024-04-27T21:32:50+00:00" }, + { + "name": "romanzipp/laravel-queue-monitor", + "version": "5.3.7", + "source": { + "type": "git", + "url": "https://github.com/romanzipp/Laravel-Queue-Monitor.git", + "reference": "7412f5315bbb2fa5ea4a05743d615e56b397a96e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/romanzipp/Laravel-Queue-Monitor/zipball/7412f5315bbb2fa5ea4a05743d615e56b397a96e", + "reference": "7412f5315bbb2fa5ea4a05743d615e56b397a96e", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-mbstring": "*", + "illuminate/database": "^5.5|^6.0|^7.0|^8.0|^9.0|^10.0|^11.0", + "illuminate/queue": "^5.5|^6.0|^7.0|^8.0|^9.0|^10.0|^11.0", + "illuminate/support": "^5.5|^6.0|^7.0|^8.0|^9.0|^10.0|^11.0", + "nesbot/carbon": "^2.0|^3.0", + "php": "^8.0" + }, + "require-dev": { + "doctrine/dbal": "^3.1", + "friendsofphp/php-cs-fixer": "^3.0", + "laravel/framework": "^5.5|^6.0|^7.0|^8.0|^9.0|^10.0|^11.0", + "mockery/mockery": "^1.3.2", + "orchestra/testbench": ">=3.8", + "phpstan/phpstan": "^0.12.99|^1.0", + "phpunit/phpunit": "^8.5.23|^9.0|^10.5", + "romanzipp/php-cs-fixer-config": "^3.0" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "romanzipp\\QueueMonitor\\Providers\\QueueMonitorProvider" + ] + } + }, + "autoload": { + "psr-4": { + "romanzipp\\QueueMonitor\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "romanzipp", + "email": "ich@ich.wtf", + "homepage": "https://ich.wtf" + } + ], + "description": "Queue Monitoring for Laravel Database Job Queue", + "support": { + "issues": "https://github.com/romanzipp/Laravel-Queue-Monitor/issues", + "source": "https://github.com/romanzipp/Laravel-Queue-Monitor/tree/5.3.7" + }, + "funding": [ + { + "url": "https://github.com/romanzipp", + "type": "github" + } + ], + "time": "2024-12-05T14:46:27+00:00" + }, { "name": "sentry/sentry", "version": "4.10.0", @@ -6966,16 +7036,16 @@ }, { "name": "laravel/breeze", - "version": "v2.3.0", + "version": "v2.3.1", "source": { "type": "git", "url": "https://github.com/laravel/breeze.git", - "reference": "d59702967b9ae21879df905d691a50132966c4ff" + "reference": "60ac80abfa08c3c2dbc61e4b16f02230b843cfd3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/breeze/zipball/d59702967b9ae21879df905d691a50132966c4ff", - "reference": "d59702967b9ae21879df905d691a50132966c4ff", + "url": "https://api.github.com/repos/laravel/breeze/zipball/60ac80abfa08c3c2dbc61e4b16f02230b843cfd3", + "reference": "60ac80abfa08c3c2dbc61e4b16f02230b843cfd3", "shasum": "" }, "require": { @@ -7023,7 +7093,7 @@ "issues": "https://github.com/laravel/breeze/issues", "source": "https://github.com/laravel/breeze" }, - "time": "2024-12-14T21:21:42+00:00" + "time": "2025-01-13T16:52:29+00:00" }, { "name": "laravel/pail", @@ -7105,16 +7175,16 @@ }, { "name": "laravel/pint", - "version": "v1.19.0", + "version": "v1.20.0", "source": { "type": "git", "url": "https://github.com/laravel/pint.git", - "reference": "8169513746e1bac70c85d6ea1524d9225d4886f0" + "reference": "53072e8ea22213a7ed168a8a15b96fbb8b82d44b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/pint/zipball/8169513746e1bac70c85d6ea1524d9225d4886f0", - "reference": "8169513746e1bac70c85d6ea1524d9225d4886f0", + "url": "https://api.github.com/repos/laravel/pint/zipball/53072e8ea22213a7ed168a8a15b96fbb8b82d44b", + "reference": "53072e8ea22213a7ed168a8a15b96fbb8b82d44b", "shasum": "" }, "require": { @@ -7167,20 +7237,20 @@ "issues": "https://github.com/laravel/pint/issues", "source": "https://github.com/laravel/pint" }, - "time": "2024-12-30T16:20:10+00:00" + "time": "2025-01-14T16:20:53+00:00" }, { "name": "laravel/sail", - "version": "v1.39.1", + "version": "v1.40.0", "source": { "type": "git", "url": "https://github.com/laravel/sail.git", - "reference": "1a3c7291bc88de983b66688919a4d298d68ddec7" + "reference": "237e70656d8eface4839de51d101284bd5d0cf71" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/sail/zipball/1a3c7291bc88de983b66688919a4d298d68ddec7", - "reference": "1a3c7291bc88de983b66688919a4d298d68ddec7", + "url": "https://api.github.com/repos/laravel/sail/zipball/237e70656d8eface4839de51d101284bd5d0cf71", + "reference": "237e70656d8eface4839de51d101284bd5d0cf71", "shasum": "" }, "require": { @@ -7230,7 +7300,7 @@ "issues": "https://github.com/laravel/sail/issues", "source": "https://github.com/laravel/sail" }, - "time": "2024-11-27T15:42:28+00:00" + "time": "2025-01-13T16:57:11+00:00" }, { "name": "mockery/mockery", diff --git a/config/queue-monitor.php b/config/queue-monitor.php new file mode 100644 index 0000000..f7ec055 --- /dev/null +++ b/config/queue-monitor.php @@ -0,0 +1,60 @@ + 'queue_monitor', + 'connection' => null, + + /* + * Set the model used for monitoring. + * If using a custom model, be sure to implement the + * romanzipp\QueueMonitor\Models\Contracts\MonitorContract + * interface or extend the base model. + */ + 'model' => \romanzipp\QueueMonitor\Models\Monitor::class, + + // Determined if the queued jobs should be monitored + 'monitor_queued_jobs' => true, + + // Specify the max character length to use for storing exception backtraces. + 'db_max_length_exception' => 4294967295, + 'db_max_length_exception_message' => 65535, + + // The optional UI settings. + 'ui' => [ + // Enable the UI + 'enabled' => true, + + // Accepts route group configuration + 'route' => [ + 'prefix' => 'queues', + 'middleware' => [], + ], + + // Set the monitored jobs count to be displayed per page. + 'per_page' => 50, + + // Show custom data stored on model + 'show_custom_data' => true, + + // Allow the deletion of single monitor items. + 'allow_deletion' => true, + + // Allow retry for a single failed monitor item. + 'allow_retry' => true, + + // Allow purging all monitor entries. + 'allow_purge' => true, + + 'show_metrics' => true, + + // Time frame used to calculate metrics values (in days). + 'metrics_time_frame' => 14, + + // The interval before refreshing the dashboard (in seconds). + 'refresh_interval' => 5, + + // Order the queued but not started jobs first + 'order_queued_first' => false, + ], +]; diff --git a/database/migrations/2018_02_05_000000_create_queue_monitor_table.php b/database/migrations/2018_02_05_000000_create_queue_monitor_table.php new file mode 100644 index 0000000..d482e3a --- /dev/null +++ b/database/migrations/2018_02_05_000000_create_queue_monitor_table.php @@ -0,0 +1,55 @@ +increments('id'); + $table->uuid('job_uuid')->nullable(); + + $table->string('job_id')->index(); + $table->string('name')->nullable(); + $table->string('queue')->nullable(); + + $table->unsignedInteger('status')->default(MonitorStatus::RUNNING); + + $table->dateTime('queued_at')->nullable(); + $table->timestamp('started_at')->nullable()->index(); + $table->string('started_at_exact')->nullable(); + + $table->timestamp('finished_at')->nullable(); + $table->string('finished_at_exact')->nullable(); + + $table->integer('attempt')->default(0); + $table->boolean('retried')->default(false); + $table->integer('progress')->nullable(); + + $table->longText('exception')->nullable(); + $table->text('exception_message')->nullable(); + $table->text('exception_class')->nullable(); + + $table->longText('data')->nullable(); + }); + } + + public function down() + { + Schema::drop(config('queue-monitor.table')); + } +} diff --git a/package.json b/package.json index 632efc7..5f8bc7c 100644 --- a/package.json +++ b/package.json @@ -7,14 +7,14 @@ }, "devDependencies": { "@headlessui/react": "^2.2.0", - "@inertiajs/react": "^2.0.0", + "@inertiajs/react": "^2.0.2", "@tailwindcss/forms": "^0.5.10", "@vitejs/plugin-react": "^4.3.4", "autoprefixer": "^10.4.20", "axios": "^1.7.9", "concurrently": "^9.1.2", "laravel-vite-plugin": "^1.1.1", - "postcss": "^8.4.49", + "postcss": "^8.5.1", "react": "^18.3.1", "react-dom": "^18.3.1", "tailwindcss": "^3.4.17", @@ -35,7 +35,7 @@ "@radix-ui/react-tabs": "^1.1.2", "@radix-ui/react-toast": "^1.2.4", "@radix-ui/react-tooltip": "^1.1.6", - "@sentry/react": "^8.48.0", + "@sentry/react": "^8.50.0", "@sentry/vite-plugin": "^2.23.0", "@tanstack/react-table": "^8.20.6", "class-variance-authority": "^0.7.1", diff --git a/public/vendor/queue-monitor/app.css b/public/vendor/queue-monitor/app.css new file mode 100644 index 0000000..7c238fd --- /dev/null +++ b/public/vendor/queue-monitor/app.css @@ -0,0 +1 @@ +/*! tailwindcss v3.2.7 | MIT License | https://tailwindcss.com*/*,:after,:before{box-sizing:border-box;border:0 solid #e5e7eb}:after,:before{--tw-content:""}html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;font-feature-settings:normal}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:initial}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;font-weight:inherit;line-height:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button;background-color:initial;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:initial}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]{display:none}*,::backdrop,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:#3b82f680;--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }.-mx-2{margin-left:-.5rem;margin-right:-.5rem}.mx-1{margin-left:.25rem;margin-right:.25rem}.my-2{margin-top:.5rem;margin-bottom:.5rem}.my-6{margin-top:1.5rem;margin-bottom:1.5rem}.mb-1{margin-bottom:.25rem}.mb-4{margin-bottom:1rem}.mb-6{margin-bottom:1.5rem}.ml-1{margin-left:.25rem}.mt-1{margin-top:.25rem}.mt-12{margin-top:3rem}.mt-2{margin-top:.5rem}.mt-4{margin-top:1rem}.block{display:block}.flex{display:flex}.inline-flex{display:inline-flex}.table{display:table}.h-3{height:.75rem}.h-full{height:100%}.w-1\/4{width:25%}.w-32{width:8rem}.w-64{width:16rem}.w-\[24rem\]{width:24rem}.w-full{width:100%}.flex-1{flex:1 1 0%}.border-separate{border-collapse:initial}.border-spacing-0{--tw-border-spacing-x:0px;--tw-border-spacing-y:0px;border-spacing:var(--tw-border-spacing-x) var(--tw-border-spacing-y)}.cursor-not-allowed{cursor:not-allowed}.cursor-pointer{cursor:pointer}.appearance-none{-webkit-appearance:none;-moz-appearance:none;appearance:none}.flex-col{flex-direction:column}.items-center{align-items:center}.items-stretch{align-items:stretch}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-4{gap:1rem}.overflow-hidden{overflow:hidden}.rounded{border-radius:.25rem}.rounded-full{border-radius:9999px}.rounded-md{border-radius:.375rem}.rounded-t-md{border-top-left-radius:.375rem;border-top-right-radius:.375rem}.border{border-width:1px}.border-b{border-bottom-width:1px}.border-gray-100{--tw-border-opacity:1;border-color:rgb(243 244 246/var(--tw-border-opacity))}.border-gray-200{--tw-border-opacity:1;border-color:rgb(229 231 235/var(--tw-border-opacity))}.border-gray-300{--tw-border-opacity:1;border-color:rgb(209 213 219/var(--tw-border-opacity))}.bg-blue-200{--tw-bg-opacity:1;background-color:rgb(191 219 254/var(--tw-bg-opacity))}.bg-blue-50{--tw-bg-opacity:1;background-color:rgb(239 246 255/var(--tw-bg-opacity))}.bg-gray-100{--tw-bg-opacity:1;background-color:rgb(243 244 246/var(--tw-bg-opacity))}.bg-gray-200{--tw-bg-opacity:1;background-color:rgb(229 231 235/var(--tw-bg-opacity))}.bg-gray-300{--tw-bg-opacity:1;background-color:rgb(209 213 219/var(--tw-bg-opacity))}.bg-gray-50{--tw-bg-opacity:1;background-color:rgb(249 250 251/var(--tw-bg-opacity))}.bg-gray-700{--tw-bg-opacity:1;background-color:rgb(55 65 81/var(--tw-bg-opacity))}.bg-green-200{--tw-bg-opacity:1;background-color:rgb(187 247 208/var(--tw-bg-opacity))}.bg-green-500{--tw-bg-opacity:1;background-color:rgb(34 197 94/var(--tw-bg-opacity))}.bg-red-200{--tw-bg-opacity:1;background-color:rgb(254 202 202/var(--tw-bg-opacity))}.bg-red-50{--tw-bg-opacity:1;background-color:rgb(254 242 242/var(--tw-bg-opacity))}.bg-transparent{background-color:initial}.bg-white{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity))}.p-1{padding:.25rem}.p-4{padding:1rem}.p-6{padding:1.5rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.py-4{padding-top:1rem;padding-bottom:1rem}.pb-64{padding-bottom:16rem}.pl-2{padding-left:.5rem}.pl-4{padding-left:1rem}.text-left{text-align:left}.text-center{text-align:center}.font-sans{font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji}.text-3xl{font-size:1.875rem;line-height:2.25rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xs{font-size:.75rem;line-height:1rem}.font-light{font-weight:300}.font-medium{font-weight:500}.font-semibold{font-weight:600}.uppercase{text-transform:uppercase}.leading-5{line-height:1.25rem}.leading-relaxed{line-height:1.625}.text-blue-800{--tw-text-opacity:1;color:rgb(30 64 175/var(--tw-text-opacity))}.text-gray-200{--tw-text-opacity:1;color:rgb(229 231 235/var(--tw-text-opacity))}.text-gray-500{--tw-text-opacity:1;color:rgb(107 114 128/var(--tw-text-opacity))}.text-gray-600{--tw-text-opacity:1;color:rgb(75 85 99/var(--tw-text-opacity))}.text-gray-700{--tw-text-opacity:1;color:rgb(55 65 81/var(--tw-text-opacity))}.text-gray-800{--tw-text-opacity:1;color:rgb(31 41 55/var(--tw-text-opacity))}.text-green-700{--tw-text-opacity:1;color:rgb(21 128 61/var(--tw-text-opacity))}.text-green-800{--tw-text-opacity:1;color:rgb(22 101 52/var(--tw-text-opacity))}.text-red-800{--tw-text-opacity:1;color:rgb(153 27 27/var(--tw-text-opacity))}.shadow-sm{--tw-shadow:0 1px 2px 0 #0000000d;--tw-shadow-colored:0 1px 2px 0 var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.transition-colors{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1)}.duration-150,.transition-colors{transition-duration:.15s}.hover\:bg-blue-100:hover{--tw-bg-opacity:1;background-color:rgb(219 234 254/var(--tw-bg-opacity))}.hover\:bg-gray-100:hover{--tw-bg-opacity:1;background-color:rgb(243 244 246/var(--tw-bg-opacity))}.hover\:bg-gray-300:hover{--tw-bg-opacity:1;background-color:rgb(209 213 219/var(--tw-bg-opacity))}.hover\:bg-red-100:hover{--tw-bg-opacity:1;background-color:rgb(254 226 226/var(--tw-bg-opacity))}@media (prefers-color-scheme:dark){.dark\:border-gray-500{--tw-border-opacity:1;border-color:rgb(107 114 128/var(--tw-border-opacity))}.dark\:border-gray-600{--tw-border-opacity:1;border-color:rgb(75 85 99/var(--tw-border-opacity))}.dark\:bg-black{--tw-bg-opacity:1;background-color:rgb(0 0 0/var(--tw-bg-opacity))}.dark\:bg-blue-200{--tw-bg-opacity:1;background-color:rgb(191 219 254/var(--tw-bg-opacity))}.dark\:bg-blue-600{--tw-bg-opacity:1;background-color:rgb(37 99 235/var(--tw-bg-opacity))}.dark\:bg-gray-200{--tw-bg-opacity:1;background-color:rgb(229 231 235/var(--tw-bg-opacity))}.dark\:bg-gray-600{--tw-bg-opacity:1;background-color:rgb(75 85 99/var(--tw-bg-opacity))}.dark\:bg-gray-700{--tw-bg-opacity:1;background-color:rgb(55 65 81/var(--tw-bg-opacity))}.dark\:bg-gray-800{--tw-bg-opacity:1;background-color:rgb(31 41 55/var(--tw-bg-opacity))}.dark\:bg-green-600{--tw-bg-opacity:1;background-color:rgb(22 163 74/var(--tw-bg-opacity))}.dark\:bg-red-200{--tw-bg-opacity:1;background-color:rgb(254 202 202/var(--tw-bg-opacity))}.dark\:bg-red-600{--tw-bg-opacity:1;background-color:rgb(220 38 38/var(--tw-bg-opacity))}.dark\:bg-transparent{background-color:initial}.dark\:text-blue-50{--tw-text-opacity:1;color:rgb(239 246 255/var(--tw-text-opacity))}.dark\:text-gray-300{--tw-text-opacity:1;color:rgb(209 213 219/var(--tw-text-opacity))}.dark\:text-gray-400{--tw-text-opacity:1;color:rgb(156 163 175/var(--tw-text-opacity))}.dark\:text-gray-600{--tw-text-opacity:1;color:rgb(75 85 99/var(--tw-text-opacity))}.dark\:text-green-50{--tw-text-opacity:1;color:rgb(240 253 244/var(--tw-text-opacity))}.dark\:text-green-500{--tw-text-opacity:1;color:rgb(34 197 94/var(--tw-text-opacity))}.dark\:text-red-50{--tw-text-opacity:1;color:rgb(254 242 242/var(--tw-text-opacity))}.dark\:text-red-500{--tw-text-opacity:1;color:rgb(239 68 68/var(--tw-text-opacity))}.dark\:text-red-600{--tw-text-opacity:1;color:rgb(220 38 38/var(--tw-text-opacity))}.dark\:text-white{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}.dark\:hover\:bg-blue-300:hover{--tw-bg-opacity:1;background-color:rgb(147 197 253/var(--tw-bg-opacity))}.dark\:hover\:bg-gray-300:hover{--tw-bg-opacity:1;background-color:rgb(209 213 219/var(--tw-bg-opacity))}.dark\:hover\:bg-red-800:hover{--tw-bg-opacity:1;background-color:rgb(153 27 27/var(--tw-bg-opacity))}.hover\:dark\:bg-red-300:hover{--tw-bg-opacity:1;background-color:rgb(252 165 165/var(--tw-bg-opacity))}.dark\:hover\:text-red-200:hover{--tw-text-opacity:1;color:rgb(254 202 202/var(--tw-text-opacity))}} \ No newline at end of file diff --git a/resources/js/Layouts/AppLayout.jsx b/resources/js/Layouts/AppLayout.jsx index e6184ce..610bccd 100644 --- a/resources/js/Layouts/AppLayout.jsx +++ b/resources/js/Layouts/AppLayout.jsx @@ -1,43 +1,13 @@ -import { useEffect, useState } from 'react'; import { Link } from '@inertiajs/react'; -import { Moon, Sun } from 'lucide-react'; import { Separator } from '@radix-ui/react-separator'; -import { Tooltip, TooltipProvider, TooltipTrigger } from '@radix-ui/react-tooltip'; import { AppSidebar } from '@/components/ui/app-sidebar'; import { Breadcrumb, BreadcrumbItem, BreadcrumbLink, BreadcrumbList } from '@/components/ui/breadcrumb'; -import { Button } from '@/components/ui/button'; import { SidebarInset, SidebarProvider, SidebarTrigger } from '@/components/ui/sidebar'; -import { TooltipContent } from '@/components/ui/tooltip'; import { Toaster } from '@/components/ui/toaster'; export default function AppLayout({ auth, header, children, toolbar }) { - - const getTheme = () => { - if (localStorage.getItem('theme')) { - return localStorage.getItem('theme'); - } - - return 'light'; - } - - const themeButtonOnclickHandler = (theme) => { - // Set local storage - localStorage.setItem('theme', theme); - setTheme(theme); - - const root = window.document.documentElement; - root.classList.remove("light", "dark"); - root.classList.add(theme); - } - - const [theme, setTheme] = useState(getTheme()); - - useEffect(() => { - setTheme(getTheme()); - }, []); - return ( @@ -56,20 +26,6 @@ export default function AppLayout({ auth, header, children, toolbar }) { - - - - { theme === 'dark' ? () : () } - - -

Toggle day / night mode

-
-
-
{ toolbar }
diff --git a/resources/js/Pages/Comic/Histories.jsx b/resources/js/Pages/Comic/Histories.jsx index 9e7e7f6..6014864 100644 --- a/resources/js/Pages/Comic/Histories.jsx +++ b/resources/js/Pages/Comic/Histories.jsx @@ -79,6 +79,12 @@ export default function Histories({ auth, histories }) { + + @@ -135,5 +141,5 @@ export default function Histories({ auth, histories }) { -); + ); } diff --git a/resources/js/Pages/Comic/HistoriesByComic.jsx b/resources/js/Pages/Comic/HistoriesByComic.jsx new file mode 100644 index 0000000..a783854 --- /dev/null +++ b/resources/js/Pages/Comic/HistoriesByComic.jsx @@ -0,0 +1,92 @@ +import { useState } from 'react'; +import { Head, Link, router } from '@inertiajs/react'; +import AppLayout from '@/Layouts/AppLayout.jsx'; + +import { DateTime } from "luxon"; + +import { BreadcrumbItem, BreadcrumbPage, BreadcrumbSeparator } from "@/components/ui/breadcrumb"; +import { Table, TableBody, TableCaption, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; +import { Button } from "@/components/ui/button.jsx"; + +export default function Histories({ auth, histories }) { + + const deleteButtonOnClickHandler = () => { + router.visit(route('comics.patchHistories'), { + data: (ids.length > 0) ? { ids: ids } : { ids: 'all' }, + method: "PATCH", + only: ['histories'], + onSuccess: data => { + toast({ + title: "All set", + description: `The histories has been deleted.`, + }); + } + }); + } + + const removeDuplicatedButtonOnClickHandler = () => { + router.visit(route('comics.destroyHistories'), { + data: (ids.length > 0) ? { ids: ids } : { ids: 'all' }, + method: "DELETE", + only: ['histories'], + onSuccess: data => { + toast({ + title: "All set", + description: `The duplicated records has been deleted.`, + }); + } + }); + } + + return ( + + + + Histories + + + }> + + Histories + +
+
+ +
+
+ + + Comic + Chapter + Read at + + + + { histories.map((comic, i) => + comic.histories.map((record, j) => ( + + { (j === 0) ? + + { comic.comic.comic_name } + + : null } + + + { record.chapter_name } + + + { record.read_at } + + ) ) + ) } + +
+ + + ); +} diff --git a/resources/js/Pages/Comic/Index.jsx b/resources/js/Pages/Comic/Index.jsx index 0528571..6c37d82 100644 --- a/resources/js/Pages/Comic/Index.jsx +++ b/resources/js/Pages/Comic/Index.jsx @@ -12,11 +12,17 @@ import { useToast } from '@/hooks/use-toast.js'; export default function Index({ comics, offset, auth }) { - const url = new URL(window.location); //searchParams + const url = new URL(window.location); // searchParams const [favourites, setFavourites] = useState(auth.user?.favourites ?? []); const { toast } = useToast(); + /** + * On click handler for the star + * Do posting and make a toast + * + * @param pathword + */ const favouriteOnClickHandler = (pathword) => { axios.post(route('comics.postFavourite'), { pathword: pathword }).then(res => { setFavourites(res.data); @@ -28,6 +34,12 @@ export default function Index({ comics, offset, auth }) { }); } + /** + * Generate info card for comics + * @param props + * @returns {JSX.Element} + * @constructor + */ const ComicCard = (props) => ( @@ -41,7 +53,9 @@ export default function Index({ comics, offset, auth }) { - { props.name } + + { props.name } + { props.author && props.author.map(a => ( @@ -53,6 +67,12 @@ export default function Index({ comics, offset, auth }) { ); + /** + * Loop and return all info cards + * @param comics + * @returns {*} + * @constructor + */ const ComicCards = (comics) => { return comics.list.map((comic, i) => ); } diff --git a/resources/js/Pages/Comic/Read.jsx b/resources/js/Pages/Comic/Read.jsx index 46d9bde..b382778 100644 --- a/resources/js/Pages/Comic/Read.jsx +++ b/resources/js/Pages/Comic/Read.jsx @@ -1,6 +1,6 @@ +import React, { useEffect, useLayoutEffect, useRef, useState } from 'react'; import { Head, Link, router } from '@inertiajs/react'; import AppLayout from '@/Layouts/AppLayout.jsx'; -import React, { useEffect, useLayoutEffect, useRef, useState } from 'react'; import { ChevronFirst, ChevronLast, Rows3 } from 'lucide-react'; import { Tooltip, TooltipProvider, TooltipTrigger } from '@radix-ui/react-tooltip'; @@ -19,6 +19,7 @@ export default function Read({ auth, comic, chapter, chapters }) { const validReadingModes = ['rtl', 'utd']; const [readingMode, setReadingMode] = useState('rtl'); // rtl, utd + const [isInvertClickingZone, setIsInvertClickingZone] = useState(false); const [isTwoPagesPerScreen, setIsTwoPagePerScreen] = useState(false); // TODO const [currentImage, setCurrentImage] = useState(1); @@ -35,9 +36,17 @@ export default function Read({ auth, comic, chapter, chapters }) { return "rtl"; } + const getLocalStorageIsInvertClickingZone = () => { + if (window.localStorage.getItem('invertClickingZone') !== null && window.localStorage.getItem('invertClickingZone')) { + return window.localStorage.getItem('invertClickingZone') === 'true'; + } + + return false; + } + const getLocalStorageIsTwoPagePerScreen = () => { - if (window.localStorage.getItem('twoPagesPerScreen') !== null && validReadingModes.includes(window.localStorage.getItem('twoPagesPerScreen'))) { - return window.localStorage.getItem('twoPagesPerScreen'); + if (window.localStorage.getItem('twoPagesPerScreen') !== null && window.localStorage.getItem('twoPagesPerScreen')) { + return window.localStorage.getItem('twoPagesPerScreen') === 'true'; } return false; @@ -68,6 +77,16 @@ export default function Read({ auth, comic, chapter, chapters }) { } } + const toggleInvertClickingZone = (e) => { + if (e) { + window.localStorage.setItem('invertClickingZone', true); + setIsInvertClickingZone(true); + } else { + window.localStorage.setItem('invertClickingZone', false); + setIsInvertClickingZone(false); + } + } + const toggleTwoPagesPerScreen = (e) => { if (e) { window.localStorage.setItem('twoPagesPerScreen', true); @@ -114,24 +133,32 @@ export default function Read({ auth, comic, chapter, chapters }) { const bounds = imgRef.current.getBoundingClientRect(); const percentage = (e.pageX - bounds.left) / imgRef.current.offsetWidth; - if (percentage < 0.45) { + const prevHandler = () => { if (img.innerKey === 0 && chapter.chapter.prev) { router.visit(route('comics.read', [comic.comic.path_word, chapter.chapter.prev])); } else { document.getElementById(`image-${img.innerKey - 1}`)?.scrollIntoView(); } - } else if (percentage > 0.55) { + } + + const nextHandler = () => { if (img.innerKey >= chapter.sorted.length - 1 && chapter.chapter.next) { router.visit(route('comics.read', [comic.comic.path_word, chapter.chapter.next])); } else { document.getElementById(`image-${img.innerKey + 1}`)?.scrollIntoView(); } } + + // InvertClickingZone + if (percentage < 0.45 || percentage > 0.55) { + (percentage < 0.45) ^ isInvertClickingZone ? prevHandler() : nextHandler(); + } }; return (
- {
); @@ -166,7 +193,17 @@ export default function Read({ auth, comic, chapter, chapters }) {
- + +

Turn on for clicking image on right side to previous one

+
+ toggleInvertClickingZone(!isInvertClickingZone) } /> +
+
+
+
+
+

Only applicable to RTL mode

{ setReadingMode(getLocalStorageReadingMode()); + setIsInvertClickingZone(getLocalStorageIsInvertClickingZone()); setIsTwoPagePerScreen(getLocalStorageIsTwoPagePerScreen()); if (!ref.current) return; @@ -333,7 +371,7 @@ export default function Read({ auth, comic, chapter, chapters }) { { chapter.chapter.name.concat(" - ", comic.comic.name) }
+ style={{ overflowAnchor: "none", height: "calc(100dvh - 90px)", overflowY: "scroll" }}> { chapter.sorted.map((img, j) => ) }
diff --git a/resources/js/Pages/Pages/Installation.jsx b/resources/js/Pages/Pages/Installation.jsx index b6a4fe6..4e6335f 100644 --- a/resources/js/Pages/Pages/Installation.jsx +++ b/resources/js/Pages/Pages/Installation.jsx @@ -38,6 +38,7 @@ export default function Installation({ auth }) {
  • Generate Database tables php artisan migrate
  • Install JS dependencies bun install
  • Build frontend JS files bun run build
  • +
  • Run background queue process php artisan queue:listen
  • Visit http://[url]/tags to fetch initial dataset
  • It should be running?
  • diff --git a/resources/js/Pages/Pages/Updates.jsx b/resources/js/Pages/Pages/Updates.jsx index 67cb0dd..93f40f2 100644 --- a/resources/js/Pages/Pages/Updates.jsx +++ b/resources/js/Pages/Pages/Updates.jsx @@ -11,6 +11,22 @@ export default function Updates({ auth }) { Updates
    + + + 0.1.3 + Release: 18 Jan 2025 + + +
      +
    • Fixed: Theme initialization
    • +
    • Toggle clicking spot invert in reading page
    • +
    • Beta: Histories group by comics
    • +
    • Beta: Prefetch first chapter if unread comic clicked
    • +
    • Updated cache TTL
    • +
    • Moved theme toggle to sidebar
    • +
    +
    +
    0.1.2 diff --git a/resources/js/components/ui/app-sidebar.jsx b/resources/js/components/ui/app-sidebar.jsx index 5012e30..347f6e6 100644 --- a/resources/js/components/ui/app-sidebar.jsx +++ b/resources/js/components/ui/app-sidebar.jsx @@ -1,7 +1,7 @@ -import { useState } from 'react'; +import { useEffect, useState } from 'react'; import { Link, router, usePage } from '@inertiajs/react'; -import { BadgeCheck, ChevronsUpDown, Star, History, ChevronDown, LogOut, Search, Book, TableOfContents, House, HardDriveDownload } from 'lucide-react'; +import { BadgeCheck, ChevronsUpDown, Star, History, ChevronDown, LogOut, Search, Book, TableOfContents, House, HardDriveDownload, Moon, Sun, Layers, SquarePower } from 'lucide-react'; import { Avatar, AvatarFallback } from '@/components/ui/avatar'; import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible'; @@ -9,16 +9,15 @@ import { DropdownMenu, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, import { Sidebar, SidebarContent, SidebarFooter, SidebarGroup, SidebarGroupContent, SidebarGroupLabel, SidebarHeader, SidebarInput, SidebarMenu, SidebarMenuBadge, SidebarMenuButton, SidebarMenuItem } from '@/components/ui/sidebar'; export function AppSidebar({ auth }) { - const { tags } = usePage().props; + const { tags } = usePage().props; const [search, setSearch] = useState(''); const searchOnSubmitHandler = (e) => { e.preventDefault(); - // Visit the search page - router.get(route('comics.search', [search]), {}, { - }); + // Visit the search page + router.get(route('comics.search', [search])); } const SidebarItem = (props) => { @@ -44,6 +43,36 @@ export function AppSidebar({ auth }) { ); }; + const getTheme = () => { + if (localStorage.getItem('theme')) { + return localStorage.getItem('theme'); + } + + return 'light'; + } + + const themeButtonOnclickHandler = () => { + const t = (theme === 'light') ? 'dark' : 'light'; + + // Set local storage + localStorage.setItem('theme', t); + setTheme(t); + setThemeInHtml(t); + } + + const setThemeInHtml = (theme) => { + const root = window.document.documentElement; + root.classList.remove("light", "dark"); + root.classList.add(theme); + } + + const [theme, setTheme] = useState(getTheme()); + + useEffect(() => { + setTheme(getTheme()); + setThemeInHtml(getTheme()); + }, []); + return ( @@ -57,7 +86,7 @@ export function AppSidebar({ auth }) {
    Comic - 0.1.2 + 0.1.3
    @@ -147,8 +176,7 @@ export function AppSidebar({ auth }) { - @@ -186,6 +214,13 @@ export function AppSidebar({ auth }) { + + History On + + themeButtonOnclickHandler() }> + <>{ theme === 'dark' ? () : () } Toggle theme + + Queues Profile Favourites History diff --git a/resources/views/errors.blade.php b/resources/views/errors.blade.php new file mode 100644 index 0000000..e69de29