From 2b52f7f15acce25b67f435df26752b36bb0c6e05 Mon Sep 17 00:00:00 2001 From: User Date: Sat, 28 Dec 2024 22:41:16 -0500 Subject: [PATCH] Search --- app/Http/Controllers/ComicController.php | 76 +++++++++++++++++----- app/Models/Comic.php | 12 ++++ app/Remote/CopyManga.php | 24 +++++++ resources/js/Pages/Comic/Chapters.jsx | 6 +- resources/js/Pages/Comic/Index.jsx | 12 ++-- resources/js/Pages/Comic/Read.jsx | 11 ++-- resources/js/components/ui/app-sidebar.jsx | 59 +++++++++++++---- routes/web.php | 3 + 8 files changed, 163 insertions(+), 40 deletions(-) diff --git a/app/Http/Controllers/ComicController.php b/app/Http/Controllers/ComicController.php index bcd6971..625af43 100644 --- a/app/Http/Controllers/ComicController.php +++ b/app/Http/Controllers/ComicController.php @@ -77,15 +77,9 @@ class ComicController extends Controller ]); } - public function index(Request $request): Response - { - $params = []; - if ($request->has('tag')) { - $params['theme'] = $request->get('tag'); - } - - $comics = $this->copyManga->comics(30, $request->header('offset', 0), $request->get('top', 'all'), $params); - + // Internal function for Upsert comics to db + protected function comicsUpsert($comics): void + { // Prep the array for upsert $comicsUpsertArray = []; $authorsUpsertArray = []; @@ -98,7 +92,7 @@ class ComicController extends Controller 'alias' => '{}', 'description' => '', 'cover' => $comic['cover'], - 'upstream_updated_at' => $comic['datetime_updated'], + 'upstream_updated_at' => $comic['datetime_updated'] ?? null, ]; foreach ($comic['author'] as $author) { @@ -122,6 +116,17 @@ class ComicController extends Controller $comicObj->authors()->sync($authorObj); } } + } + + public function index(Request $request): Response + { + $params = []; + if ($request->has('tag')) { + $params['theme'] = $request->get('tag'); + } + + $comics = $this->copyManga->comics(30, $request->header('offset', 0), $request->get('top', 'all'), $params); + $this->comicsUpsert($comics); return Inertia::render('Comic/Index', [ 'comics' => $comics, @@ -129,18 +134,57 @@ class ComicController extends Controller ]); } + public function author(Request $request, string $author): Response + { + $params = []; + $params['author'] = $author; + + $comics = $this->copyManga->comics(30, $request->header('offset', 0), $request->get('top', 'all'), $params); + $this->comicsUpsert($comics); + + return Inertia::render('Comic/Index', [ + 'comics' => $comics, + 'offset' => $request->header('offset', 0) + ]); + } + + public function search(Request $request, string $search): Response + { + $comics = $this->copyManga->search($search, 30, $request->header('offset', 0)); + + // Seacrh API is limited, no upsert + + return Inertia::render('Comic/Index', [ + 'comics' => $comics, + 'offset' => $request->header('offset', 0) + ]); + } + public function chapters(Request $request, string $pathword = ''): Response { $comic = $this->copyManga->comic($pathword); $chapters = $this->copyManga->chapters($pathword, 200, 0, [], $request->get('group', 'default')); // Get the comic object and fill other parameters - $comicObject = Comic::where('pathword', $pathword)->first(); - $comicObject->uuid = $comic['comic']['uuid']; - $comicObject->alias = explode(',', $comic['comic']['alias']); - $comicObject->description = $comic['comic']['brief']; - $comicObject->metadata = $comic; - $comicObject->save(); + 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) { diff --git a/app/Models/Comic.php b/app/Models/Comic.php index f3c9c9f..0866af3 100644 --- a/app/Models/Comic.php +++ b/app/Models/Comic.php @@ -8,6 +8,18 @@ use Illuminate\Database\Eloquent\Relations\HasMany; class Comic extends Model { + + protected $fillable = [ + 'pathword', + 'uuid', + 'name', + 'alias', + 'description', + 'cover', + 'upstream_updated_at', + 'metadata', + ]; + protected function casts(): array { return [ diff --git a/app/Remote/CopyManga.php b/app/Remote/CopyManga.php index 8d2eb44..9b626c9 100644 --- a/app/Remote/CopyManga.php +++ b/app/Remote/CopyManga.php @@ -209,9 +209,33 @@ class CopyManga $parameters['limit'] = $limit; $parameters['offset'] = $offset; $parameters['top'] = $top; + //OPTIONS + // https://api.mangacopy.com/api/v3/comics?format=json&platform=1&q=x&limit=30&offset=0&top=all" + // https://api.mangacopy.com/api/v3/search/comic?platform=1&q=x&limit=20&offset=0&q_type=&_update=true + return $this->execute($this->buildUrl("comics", $parameters)); } + /** + * Search comic by name + * + * @param string $item + * @param int $limit + * @param int $offset + * @return mixed|string + * @throws GuzzleException + */ + public function search(string $item = '', int $limit = 28, int $offset = 0) + { + $parameters['q'] = $item; + $parameters['q_type'] = ""; + $parameters['_update'] = true; + $parameters['limit'] = $limit; + $parameters['offset'] = $offset; + + return $this->execute($this->buildUrl("search/comic", $parameters)); + } + /** * Get comic info * diff --git a/resources/js/Pages/Comic/Chapters.jsx b/resources/js/Pages/Comic/Chapters.jsx index dfcbf4a..74f0ea5 100644 --- a/resources/js/Pages/Comic/Chapters.jsx +++ b/resources/js/Pages/Comic/Chapters.jsx @@ -113,7 +113,11 @@ export default function Chapters({ auth, comic, chapters, histories }) { Authors - { comic.comic.author.map(a => { a.name }) } + { comic.comic.author.map(a => ( + + { a.name } + + ) ) } Description diff --git a/resources/js/Pages/Comic/Index.jsx b/resources/js/Pages/Comic/Index.jsx index 3330f87..d09b252 100644 --- a/resources/js/Pages/Comic/Index.jsx +++ b/resources/js/Pages/Comic/Index.jsx @@ -12,7 +12,7 @@ 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(); @@ -43,7 +43,11 @@ export default function Index({ comics, offset, auth }) { { props.name } - { props.author.map(a => { a.name }) } + { props.author.map(a => ( + + { a.name } + ) + ) } @@ -65,11 +69,11 @@ export default function Index({ comics, offset, auth }) { { parseInt(offset) !== 0 && - + } - + diff --git a/resources/js/Pages/Comic/Read.jsx b/resources/js/Pages/Comic/Read.jsx index c0758c4..b5ba5f2 100644 --- a/resources/js/Pages/Comic/Read.jsx +++ b/resources/js/Pages/Comic/Read.jsx @@ -18,7 +18,7 @@ export default function Read({ auth, comic, chapter }) { const validReadingModes = ['rtl', 'utd']; const [readingMode, setReadingMode] = useState('rtl'); // rtl, utd - const [isTwoPagesPerScreen, setIsTwoPagePerScreen] = useState(false); + const [isTwoPagesPerScreen, setIsTwoPagePerScreen] = useState(false); // TODO const [currentImage, setCurrentImage] = useState(1); const windowSize = useWindowSize(); @@ -228,13 +228,13 @@ export default function Read({ auth, comic, chapter }) { let visibleImageIndex = 0; // Determine which image is visible based on scroll position - images.forEach((image, index) => { + images.forEach((image, i) => { const imageTop = image.offsetTop; // Distance from top of the container const imageBottom = imageTop + image.offsetHeight; // Check if the image is in the visible area if (containerScrollTop + 80 >= imageTop && containerScrollTop < imageBottom) { - visibleImageIndex = index; + visibleImageIndex = i; } }); @@ -242,12 +242,9 @@ export default function Read({ auth, comic, chapter }) { setCurrentImage(visibleImageIndex + 1); }; - const throttledHandleScroll = throttle(handleScroll, 100); // Throttle for performance + const throttledHandleScroll = throttle(handleScroll, 1000); // Throttle for performance ref.current.addEventListener("scroll", throttledHandleScroll); - // Initial check for visible image - handleScroll(); - return () => { if (ref.current) { ref.current.removeEventListener("scroll", throttledHandleScroll); diff --git a/resources/js/components/ui/app-sidebar.jsx b/resources/js/components/ui/app-sidebar.jsx index 97e0916..558a891 100644 --- a/resources/js/components/ui/app-sidebar.jsx +++ b/resources/js/components/ui/app-sidebar.jsx @@ -1,18 +1,26 @@ -import { Sidebar, SidebarContent, SidebarFooter, SidebarGroup, SidebarGroupContent, SidebarGroupLabel, SidebarMenu, SidebarMenuBadge, SidebarMenuButton, SidebarMenuItem, SidebarMenuSub, SidebarMenuSubButton, SidebarMenuSubItem } from '@/components/ui/sidebar'; -import { DropdownMenu, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger } from '@/components/ui/dropdown-menu'; +import { useState } from 'react'; +import { Link, router, usePage } from '@inertiajs/react'; + +import { BadgeCheck, ChevronsUpDown, Star, History, ChevronDown, LogOut, Search, Book } from 'lucide-react'; -import { BadgeCheck, ChevronsUpDown, Star, History, ChevronDown, LogOut, ChevronRight } from 'lucide-react'; -import { Link, usePage } from '@inertiajs/react'; import { Avatar, AvatarFallback } from '@/components/ui/avatar'; -import { - Collapsible, - CollapsibleContent, - CollapsibleTrigger, -} from "@/components/ui/collapsible" +import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible'; +import { DropdownMenu, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger } from '@/components/ui/dropdown-menu'; +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 [search, setSearch] = useState(''); + + const searchOnSubmitHandler = (e) => { + e.preventDefault(); + // Visit the search page + router.get(route('comics.search', [search]), {}, { + + }); + } + const SidebarItem = (props) => { const searchParams = new URL(window.location).searchParams; const isActive = (!searchParams.has(props.query) && props.name === 'All') || (searchParams.has(props.query) && searchParams.get(props.query) === props.path_word); @@ -27,8 +35,9 @@ export function AppSidebar({ auth }) { return ( - { props.name } + setSearch("") }> + { props.name } + { props.count && { props.count } } @@ -38,6 +47,32 @@ export function AppSidebar({ auth }) { return ( + + + + + +
+ +
+
+ Comic + 0.0.0 +
+ +
+
+
+ + +
searchOnSubmitHandler(e)} > + + setSearch(e.target.value) } id="search" placeholder="Search" className="pl-8" /> + + +
+
+
@@ -48,7 +83,7 @@ export function AppSidebar({ auth }) { - + { tags.theme.map(item => ) } diff --git a/routes/web.php b/routes/web.php index 7996e8c..cf135d1 100644 --- a/routes/web.php +++ b/routes/web.php @@ -8,6 +8,9 @@ use Inertia\Inertia; // Auth protected routes Route::controller(ComicController::class)->middleware('auth')->name('comics.')->group(function () { Route::get('/', 'index')->name('index'); + Route::get('/author/{author}', 'author')->name('author'); + Route::get('/search/{search}', 'search')->name('search'); + Route::get('/comic/{pathword}/{uuid}', 'read')->name('read'); Route::get('/comic/{pathword}', 'chapters')->name('chapters'); Route::get('/tags', 'tags')->name('tags');