Search
This commit is contained in:
@@ -77,15 +77,9 @@ class ComicController extends Controller
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function index(Request $request): Response
|
// Internal function for Upsert comics to db
|
||||||
|
protected function comicsUpsert($comics): void
|
||||||
{
|
{
|
||||||
$params = [];
|
|
||||||
if ($request->has('tag')) {
|
|
||||||
$params['theme'] = $request->get('tag');
|
|
||||||
}
|
|
||||||
|
|
||||||
$comics = $this->copyManga->comics(30, $request->header('offset', 0), $request->get('top', 'all'), $params);
|
|
||||||
|
|
||||||
// Prep the array for upsert
|
// Prep the array for upsert
|
||||||
$comicsUpsertArray = [];
|
$comicsUpsertArray = [];
|
||||||
$authorsUpsertArray = [];
|
$authorsUpsertArray = [];
|
||||||
@@ -98,7 +92,7 @@ class ComicController extends Controller
|
|||||||
'alias' => '{}',
|
'alias' => '{}',
|
||||||
'description' => '',
|
'description' => '',
|
||||||
'cover' => $comic['cover'],
|
'cover' => $comic['cover'],
|
||||||
'upstream_updated_at' => $comic['datetime_updated'],
|
'upstream_updated_at' => $comic['datetime_updated'] ?? null,
|
||||||
];
|
];
|
||||||
|
|
||||||
foreach ($comic['author'] as $author) {
|
foreach ($comic['author'] as $author) {
|
||||||
@@ -122,6 +116,43 @@ class ComicController extends Controller
|
|||||||
$comicObj->authors()->sync($authorObj);
|
$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,
|
||||||
|
'offset' => $request->header('offset', 0)
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
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', [
|
return Inertia::render('Comic/Index', [
|
||||||
'comics' => $comics,
|
'comics' => $comics,
|
||||||
@@ -135,12 +166,25 @@ class ComicController extends Controller
|
|||||||
$chapters = $this->copyManga->chapters($pathword, 200, 0, [], $request->get('group', 'default'));
|
$chapters = $this->copyManga->chapters($pathword, 200, 0, [], $request->get('group', 'default'));
|
||||||
|
|
||||||
// Get the comic object and fill other parameters
|
// Get the comic object and fill other parameters
|
||||||
$comicObject = Comic::where('pathword', $pathword)->first();
|
try {
|
||||||
|
$comicObject = Comic::where('pathword', $pathword)->firstOrFail();
|
||||||
$comicObject->uuid = $comic['comic']['uuid'];
|
$comicObject->uuid = $comic['comic']['uuid'];
|
||||||
$comicObject->alias = explode(',', $comic['comic']['alias']);
|
$comicObject->alias = explode(',', $comic['comic']['alias']);
|
||||||
$comicObject->description = $comic['comic']['brief'];
|
$comicObject->description = $comic['comic']['brief'];
|
||||||
$comicObject->metadata = $comic;
|
$comicObject->metadata = $comic;
|
||||||
$comicObject->save();
|
$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
|
// Get the authors and update the pathword
|
||||||
foreach ($comic['comic']['author'] as $author) {
|
foreach ($comic['comic']['author'] as $author) {
|
||||||
|
|||||||
@@ -8,6 +8,18 @@ use Illuminate\Database\Eloquent\Relations\HasMany;
|
|||||||
|
|
||||||
class Comic extends Model
|
class Comic extends Model
|
||||||
{
|
{
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'pathword',
|
||||||
|
'uuid',
|
||||||
|
'name',
|
||||||
|
'alias',
|
||||||
|
'description',
|
||||||
|
'cover',
|
||||||
|
'upstream_updated_at',
|
||||||
|
'metadata',
|
||||||
|
];
|
||||||
|
|
||||||
protected function casts(): array
|
protected function casts(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
|
|||||||
@@ -209,9 +209,33 @@ class CopyManga
|
|||||||
$parameters['limit'] = $limit;
|
$parameters['limit'] = $limit;
|
||||||
$parameters['offset'] = $offset;
|
$parameters['offset'] = $offset;
|
||||||
$parameters['top'] = $top;
|
$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));
|
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
|
* Get comic info
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -113,7 +113,11 @@ export default function Chapters({ auth, comic, chapters, histories }) {
|
|||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td className="text-right pr-3">Authors</td>
|
<td className="text-right pr-3">Authors</td>
|
||||||
<td>{ comic.comic.author.map(a => <Badge key={ a.path_word } className="m-2" variant="outline"><Link>{ a.name }</Link></Badge>) }</td>
|
<td>{ comic.comic.author.map(a => (
|
||||||
|
<Badge key={ a.path_word } className="m-2" variant="outline">
|
||||||
|
<Link href={ route('comics.author', [a.path_word]) }>{ a.name }</Link>
|
||||||
|
</Badge>
|
||||||
|
) ) }</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td className="text-right pr-3">Description</td>
|
<td className="text-right pr-3">Description</td>
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import { useToast } from '@/hooks/use-toast.js';
|
|||||||
|
|
||||||
export default function Index({ comics, offset, auth }) {
|
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 [favourites, setFavourites] = useState(auth.user.favourites);
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
@@ -43,7 +43,11 @@ export default function Index({ comics, offset, auth }) {
|
|||||||
<CardContent>
|
<CardContent>
|
||||||
<CardTitle><Link href={ `/comic/${ props.path_word }` }>{ props.name }</Link></CardTitle>
|
<CardTitle><Link href={ `/comic/${ props.path_word }` }>{ props.name }</Link></CardTitle>
|
||||||
<CardDescription className="pt-2">
|
<CardDescription className="pt-2">
|
||||||
{ props.author.map(a => <Badge className="m-1" key={ a.path_word } variant="outline"><Link>{ a.name }</Link></Badge>) }
|
{ props.author.map(a => (
|
||||||
|
<Badge className="m-1" key={ a.path_word } variant="outline">
|
||||||
|
{ a.name }
|
||||||
|
</Badge>)
|
||||||
|
) }
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -65,11 +69,11 @@ export default function Index({ comics, offset, auth }) {
|
|||||||
<PaginationContent>
|
<PaginationContent>
|
||||||
{ parseInt(offset) !== 0 &&
|
{ parseInt(offset) !== 0 &&
|
||||||
<PaginationItem>
|
<PaginationItem>
|
||||||
<PaginationPrevious href={ `/?${url}` } only={['comics', 'offset']} headers={{ offset: parseInt(offset) - 30 }} />
|
<PaginationPrevious href={ `${ url.pathname }?${ url.searchParams }` } only={['comics', 'offset']} headers={{ offset: parseInt(offset) - 30 }} />
|
||||||
</PaginationItem>
|
</PaginationItem>
|
||||||
}
|
}
|
||||||
<PaginationItem>
|
<PaginationItem>
|
||||||
<PaginationNext href={ `/?${url}` } only={['comics', 'offset']} headers={{ offset: parseInt(offset) + 30 }} />
|
<PaginationNext href={ `${ url.pathname }?${ url.searchParams }` } only={['comics', 'offset']} headers={{ offset: parseInt(offset) + 30 }} />
|
||||||
</PaginationItem>
|
</PaginationItem>
|
||||||
</PaginationContent>
|
</PaginationContent>
|
||||||
</Pagination>
|
</Pagination>
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ export default function Read({ auth, comic, chapter }) {
|
|||||||
const validReadingModes = ['rtl', 'utd'];
|
const validReadingModes = ['rtl', 'utd'];
|
||||||
|
|
||||||
const [readingMode, setReadingMode] = useState('rtl'); // 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 [currentImage, setCurrentImage] = useState(1);
|
||||||
|
|
||||||
const windowSize = useWindowSize();
|
const windowSize = useWindowSize();
|
||||||
@@ -228,13 +228,13 @@ export default function Read({ auth, comic, chapter }) {
|
|||||||
let visibleImageIndex = 0;
|
let visibleImageIndex = 0;
|
||||||
|
|
||||||
// Determine which image is visible based on scroll position
|
// 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 imageTop = image.offsetTop; // Distance from top of the container
|
||||||
const imageBottom = imageTop + image.offsetHeight;
|
const imageBottom = imageTop + image.offsetHeight;
|
||||||
|
|
||||||
// Check if the image is in the visible area
|
// Check if the image is in the visible area
|
||||||
if (containerScrollTop + 80 >= imageTop && containerScrollTop < imageBottom) {
|
if (containerScrollTop + 80 >= imageTop && containerScrollTop < imageBottom) {
|
||||||
visibleImageIndex = index;
|
visibleImageIndex = i;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -242,12 +242,9 @@ export default function Read({ auth, comic, chapter }) {
|
|||||||
setCurrentImage(visibleImageIndex + 1);
|
setCurrentImage(visibleImageIndex + 1);
|
||||||
};
|
};
|
||||||
|
|
||||||
const throttledHandleScroll = throttle(handleScroll, 100); // Throttle for performance
|
const throttledHandleScroll = throttle(handleScroll, 1000); // Throttle for performance
|
||||||
ref.current.addEventListener("scroll", throttledHandleScroll);
|
ref.current.addEventListener("scroll", throttledHandleScroll);
|
||||||
|
|
||||||
// Initial check for visible image
|
|
||||||
handleScroll();
|
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
if (ref.current) {
|
if (ref.current) {
|
||||||
ref.current.removeEventListener("scroll", throttledHandleScroll);
|
ref.current.removeEventListener("scroll", throttledHandleScroll);
|
||||||
|
|||||||
@@ -1,18 +1,26 @@
|
|||||||
import { Sidebar, SidebarContent, SidebarFooter, SidebarGroup, SidebarGroupContent, SidebarGroupLabel, SidebarMenu, SidebarMenuBadge, SidebarMenuButton, SidebarMenuItem, SidebarMenuSub, SidebarMenuSubButton, SidebarMenuSubItem } from '@/components/ui/sidebar';
|
import { useState } from 'react';
|
||||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger } from '@/components/ui/dropdown-menu';
|
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 { Avatar, AvatarFallback } from '@/components/ui/avatar';
|
||||||
import {
|
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible';
|
||||||
Collapsible,
|
import { DropdownMenu, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger } from '@/components/ui/dropdown-menu';
|
||||||
CollapsibleContent,
|
import { Sidebar, SidebarContent, SidebarFooter, SidebarGroup, SidebarGroupContent, SidebarGroupLabel, SidebarHeader, SidebarInput, SidebarMenu, SidebarMenuBadge, SidebarMenuButton, SidebarMenuItem } from '@/components/ui/sidebar';
|
||||||
CollapsibleTrigger,
|
|
||||||
} from "@/components/ui/collapsible"
|
|
||||||
|
|
||||||
export function AppSidebar({ auth }) {
|
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]), {}, {
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const SidebarItem = (props) => {
|
const SidebarItem = (props) => {
|
||||||
const searchParams = new URL(window.location).searchParams;
|
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);
|
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 (
|
return (
|
||||||
<SidebarMenuItem>
|
<SidebarMenuItem>
|
||||||
<SidebarMenuButton asChild isActive={ isActive }>
|
<SidebarMenuButton asChild isActive={ isActive }>
|
||||||
<Link
|
<Link href={ "/?" + searchParams.toString() } onClick={ (e) => setSearch("") }>
|
||||||
href={ "/?" + searchParams.toString() }><span>{ props.name }</span></Link>
|
<span>{ props.name }</span>
|
||||||
|
</Link>
|
||||||
</SidebarMenuButton>
|
</SidebarMenuButton>
|
||||||
{ props.count && <SidebarMenuBadge>{ props.count }</SidebarMenuBadge> }
|
{ props.count && <SidebarMenuBadge>{ props.count }</SidebarMenuBadge> }
|
||||||
</SidebarMenuItem>
|
</SidebarMenuItem>
|
||||||
@@ -38,6 +47,32 @@ export function AppSidebar({ auth }) {
|
|||||||
return (
|
return (
|
||||||
<Sidebar>
|
<Sidebar>
|
||||||
<SidebarContent>
|
<SidebarContent>
|
||||||
|
<SidebarHeader>
|
||||||
|
<SidebarMenu>
|
||||||
|
<SidebarMenuItem>
|
||||||
|
<SidebarMenuButton size="lg" asChild>
|
||||||
|
<Link href={ route('comics.index')}>
|
||||||
|
<div className="flex aspect-square size-8 items-center justify-center rounded-lg bg-sidebar-primary text-sidebar-primary-foreground">
|
||||||
|
<Book className="size-4" />
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-0.5 leading-none">
|
||||||
|
<span className="font-semibold">Comic</span>
|
||||||
|
<span>0.0.0</span>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
</SidebarMenuButton>
|
||||||
|
</SidebarMenuItem>
|
||||||
|
</SidebarMenu>
|
||||||
|
<SidebarGroup className="py-0">
|
||||||
|
<SidebarGroupContent className="relative">
|
||||||
|
<form onSubmit={ (e) => searchOnSubmitHandler(e)} >
|
||||||
|
<label htmlFor="search" className="sr-only">Search</label>
|
||||||
|
<SidebarInput onChange={ (e) => setSearch(e.target.value) } id="search" placeholder="Search" className="pl-8" />
|
||||||
|
<Search className="pointer-events-none absolute left-2 top-1/2 size-4 -translate-y-1/2 select-none opacity-50" />
|
||||||
|
</form>
|
||||||
|
</SidebarGroupContent>
|
||||||
|
</SidebarGroup>
|
||||||
|
</SidebarHeader>
|
||||||
<Collapsible defaultOpen className="group/collapsible">
|
<Collapsible defaultOpen className="group/collapsible">
|
||||||
<SidebarGroup>
|
<SidebarGroup>
|
||||||
<SidebarGroupLabel asChild>
|
<SidebarGroupLabel asChild>
|
||||||
|
|||||||
@@ -8,6 +8,9 @@ use Inertia\Inertia;
|
|||||||
// Auth protected routes
|
// Auth protected routes
|
||||||
Route::controller(ComicController::class)->middleware('auth')->name('comics.')->group(function () {
|
Route::controller(ComicController::class)->middleware('auth')->name('comics.')->group(function () {
|
||||||
Route::get('/', 'index')->name('index');
|
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}/{uuid}', 'read')->name('read');
|
||||||
Route::get('/comic/{pathword}', 'chapters')->name('chapters');
|
Route::get('/comic/{pathword}', 'chapters')->name('chapters');
|
||||||
Route::get('/tags', 'tags')->name('tags');
|
Route::get('/tags', 'tags')->name('tags');
|
||||||
|
|||||||
Reference in New Issue
Block a user