zhConversion::ZH_TO_HANT), array_values($this->zhConversion::ZH_TO_HANT), $string)); } return str_replace(array_keys($this->zhConversion::ZH_TO_HANT), array_values($this->zhConversion::ZH_TO_HANT), $string); } public function toggleHistory(Request $request): JsonResponse { if ($request->session()->has('historyDisabled')) { $newValue = !$request->session()->get('historyDisabled'); } else { $newValue = true; } // Put Session $request->session()->put('historyDisabled', $newValue); // Return to frontend return response()->json(['historyDisabled' => $newValue]); } /** * Show user favourites * * @param Request $request * @return Response */ public function favourites(Request $request): Response { return Inertia::render('Comic/Favourites', [ 'favourites' => $this->scToZh($request->user()->favourites()->with(['authors'])->orderBy('upstream_updated_at', 'desc')->get()), ]); } /** * Toggle user favourites * * @param Request $request * @return JsonResponse * @throws GuzzleException */ public function postFavourite(Request $request): JsonResponse { try { // Get pathname to comic_id, if metadata is null, also do fetching $comic = Comic::where('pathword', $request->pathword)->whereNotNull('metadata')->firstOrFail(); } catch (ModelNotFoundException $e) { // Fetch from remote $remoteComic = $this->copyManga->comic($request->pathword); $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 if ($request->user()->favourites()->where('comic_id', $comic->id)->exists()) { $request->user()->favourites()->detach($comic->id); } else { $request->user()->favourites()->attach($comic->id); } return response()->json($request->user()->favourites()->get(['pathword'])->pluck('pathword')); } /** * Show image via proxy * * @param Request $request * @param string $url * @return ResponseFactory|Application|IlluminateHttpResponse * @throws ConnectionException */ public function image(Request $request, string $url): ResponseFactory|Application|IlluminateHttpResponse { // TODO: Ref check and make it require auth $fetcher = new ImageFetcher(base64_decode($url)); return response($fetcher->fetch())->withHeaders([ 'Content-Type' => $fetcher->getMimeType()->value, 'Cache-Control' => 'max-age=604800', ]); } /** * Index / Tags for comic listing * * @param Request $request * @return Response * @throws GuzzleException */ public function index(Request $request): Response { $params = []; if ($request->has('tag')) { $params['theme'] = $request->get('tag'); } $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' => $offset ]); } /** * Author for comic listing * * @param Request $request * @param string $author * @return Response * @throws GuzzleException */ public function author(Request $request, string $author): Response { $offset = $request->header('offset', 0); $comics = $this->copyManga->comics(30, $offset, $request->get('top', 'all'), ['author' => $author]); // Upsert into DB ComicUpsert::dispatch($comics); return Inertia::render('Comic/Index', [ 'comics' => $this->scToZh($comics), 'offset' => $offset ]); } /** * Search for comic listing * * @param Request $request * @param string $search * @return Response * @throws GuzzleException */ public function search(Request $request, string $search): Response { $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' => $offset ]); } /** * Show comic details and chapters * * @param Request $request * @param string $pathword * @return RedirectResponse|Response * @throws GuzzleException */ public function chapters(Request $request, string $pathword = ''): RedirectResponse|Response { if ($pathword === 'installHook.js.map') { return to_route('comics.index'); } $offset = $request->header('offset', 0); $comic = $this->copyManga->comic($pathword); $chapters = $this->copyManga->chapters($pathword, 200, $offset, [], $request->get('group', 'default'), $request->get('reload', false)); // Get the comic object and fill other parameters try { $comicObject = Comic::where('pathword', $pathword)->firstOrFail(); $comicObject->uuid = $comic['comic']['uuid']; $comicObject->alias = explode(',', $comic['comic']['alias']); $comicObject->description = $comic['comic']['brief']; $comicObject->metadata = $comic; $comicObject->save(); } catch (ModelNotFoundException $e) { $comicObject = Comic::create([ 'name' => $comic['comic']['name'], 'pathword' => $comic['comic']['path_word'], 'cover' => $comic['comic']['cover'], 'upstream_updated_at' => $comic['comic']['datetime_updated'], 'uuid' => $comic['comic']['uuid'], 'alias' => explode(',', $comic['comic']['alias']), 'description' => $comic['comic']['brief'], 'metadata' => $comic ]); } // Get the authors and update the pathword foreach ($comic['comic']['author'] as $author) { $authorObj = Author::where('name', $author['name'])->whereNull('pathword')->first(); if ($authorObj) { // Do nothing if pathword already exist $authorObj->pathword = $author['path_word']; $authorObj->save(); } } // Do the Chapters // Prep the array for upsert $arrayForUpsert = []; foreach ($chapters['list'] as $chapter) { $arrayForUpsert[] = [ 'comic_id' => $comicObject->id, 'chapter_uuid' => $chapter['uuid'], 'name' => $chapter['name'], 'order' => $chapter['index'], 'upstream_created_at' => $chapter['datetime_created'], 'metadata' => json_encode($chapter), ]; } // Do an upsert Chapter::upsert($arrayForUpsert, uniqueBy: 'chapter_uuid'); // 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' => $offset ]); } /** * Read a chapter, showing images * * @param Request $request * @param string $pathword * @param string $uuid * @return Response * @throws GuzzleException */ public function read(Request $request, string $pathword = '', string $uuid = ''): Response { $comic = $this->copyManga->comic($pathword); $chapter = $this->copyManga->chapter($pathword, $uuid); // Get the authors and update the pathword foreach ($comic['comic']['author'] as $author) { $authorObj = Author::where('name', $author['name'])->whereNull('pathword')->first(); if ($authorObj) { // Do nothing if pathword already exist $authorObj->pathword = $author['path_word']; $authorObj->save(); } } $chapterObj = Chapter::where('chapter_uuid', $chapter['chapter']['uuid'])->first(); $comicObj = Comic::where('pathword', $pathword)->first(); // Image Upsert ImageUpsert::dispatch($comicObj->id, $chapterObj->id, $chapter); // Update history if (!$request->session()->get('historyDisabled', false)) { $request->user()->readingHistories()->attach($chapterObj->id, ['comic_id' => $comicObj->id]); } // Get chapters from DB $chapters = $comicObj->chapters()->where('metadata->group_path_word', $chapter['chapter']['group_path_word'])->orderBy('order')->get(['name', 'chapter_uuid']); // Do remote prefetch if needed if ($chapter['chapter']['next'] !== null) { try { Chapter::where('chapter_uuid', $chapter['chapter']['next'])->firstOrFail()->images()->firstOrFail(); } catch (ModelNotFoundException $e) { RemotePrefetch::dispatch('chapter', ['pathword' => $pathword, 'uuid' => $chapter['chapter']['next']]); } } return Inertia::render('Comic/Read', [ 'comic' => $this->scToZh($comic), 'chapter' => $this->scToZh($chapter), 'chapters' => $this->scToZh($chapters), ]); } /** * Show user read histories * * @param Request $request * @return Response */ public function histories(Request $request): Response { // 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(); return Inertia::render('Comic/Histories', [ 'histories' => $this->scToZh($histories) ]); } /** * Delete user read histories by submitting ids * * @param Request $request * @return RedirectResponse */ public function patchHistories(Request $request): RedirectResponse { if (!is_array($request->get('ids')) && $request->get('ids') === 'all') { $histories = $request->user()->readingHistories()->delete(); } else { $histories = $request->user()->readingHistories()->whereIn('reading_histories.id', $request->get('ids'))->delete(); } return redirect()->route('comics.histories'); } /** * Remove histories for specified comic * * @param Request $request * @param string $pathword * @return Response */ public function destroyHistory(Request $request, string $pathword): Response { $comicId = Comic::where('pathword', $pathword)->firstOrFail(['id'])->id; $request->user()->readingHistories()->where('reading_histories.comic_id', $comicId)->delete(); // Get history $histories = $request->user()->readingHistories()->where('reading_histories.comic_id', $comicId) ->distinct()->select('chapter_uuid')->get()->pluck('chapter_uuid'); return Inertia::render('Comic/Chapters', [ 'histories' => $histories ]); } /** * Remove duplicated records * * @param Request $request * @return RedirectResponse */ public function destroyHistories(Request $request): RedirectResponse { $request->user()->cleanUpReadingHistories(); return redirect()->route('comics.histories'); } /** * Fetch tags * * @return JsonResponse * @throws GuzzleException */ public function tags() { // TODO $tags = $this->scToZh($this->copyManga->tags()); Cache::forever('tags', $tags); return response()->json($tags); } }