This commit is contained in:
User
2025-01-06 12:58:10 -05:00
parent 489e054614
commit 721192fce7
23 changed files with 227 additions and 197 deletions

View File

@@ -37,5 +37,4 @@ class Timezones
return $tzForSelect; return $tzForSelect;
} }
} }

View File

@@ -33,7 +33,7 @@ class AuthenticatedSessionController extends Controller
$request->session()->regenerate(); $request->session()->regenerate();
return redirect()->intended(route('dashboard', absolute: false)); return redirect()->intended(route('comics.index'));
} }
/** /**

View File

@@ -36,6 +36,6 @@ class ConfirmablePasswordController extends Controller
$request->session()->put('auth.password_confirmed_at', time()); $request->session()->put('auth.password_confirmed_at', time());
return redirect()->intended(route('dashboard', absolute: false)); return redirect()->intended(route('comics.index'));
} }
} }

View File

@@ -14,7 +14,7 @@ class EmailVerificationNotificationController extends Controller
public function store(Request $request): RedirectResponse public function store(Request $request): RedirectResponse
{ {
if ($request->user()->hasVerifiedEmail()) { if ($request->user()->hasVerifiedEmail()) {
return redirect()->intended(route('dashboard', absolute: false)); return redirect()->intended(route('comics.index'));
} }
$request->user()->sendEmailVerificationNotification(); $request->user()->sendEmailVerificationNotification();

View File

@@ -16,7 +16,7 @@ class EmailVerificationPromptController extends Controller
public function __invoke(Request $request): RedirectResponse|Response public function __invoke(Request $request): RedirectResponse|Response
{ {
return $request->user()->hasVerifiedEmail() return $request->user()->hasVerifiedEmail()
? redirect()->intended(route('dashboard', absolute: false)) ? redirect()->intended(route('comics.index'))
: Inertia::render('Auth/VerifyEmail', ['status' => session('status')]); : Inertia::render('Auth/VerifyEmail', ['status' => session('status')]);
} }
} }

View File

@@ -48,6 +48,6 @@ class RegisteredUserController extends Controller
Auth::login($user); Auth::login($user);
return redirect(route('dashboard', absolute: false)); return redirect(route('comics.index'));
} }
} }

View File

@@ -15,13 +15,13 @@ class VerifyEmailController extends Controller
public function __invoke(EmailVerificationRequest $request): RedirectResponse public function __invoke(EmailVerificationRequest $request): RedirectResponse
{ {
if ($request->user()->hasVerifiedEmail()) { if ($request->user()->hasVerifiedEmail()) {
return redirect()->intended(route('dashboard', absolute: false).'?verified=1'); return redirect()->intended(route('comics.index'));
} }
if ($request->user()->markEmailAsVerified()) { if ($request->user()->markEmailAsVerified()) {
event(new Verified($request->user())); event(new Verified($request->user()));
} }
return redirect()->intended(route('dashboard', absolute: false).'?verified=1'); return redirect()->intended(route('comics.index'));
} }
} }

View File

@@ -64,8 +64,8 @@ class ComicController extends Controller
public function postFavourite(Request $request): JsonResponse public function postFavourite(Request $request): JsonResponse
{ {
try { try {
// Get pathname to comic_id // Get pathname to comic_id, if metadata is null, also do fetching
$comic = Comic::where('pathword', $request->pathword)->firstOrFail(); $comic = Comic::where('pathword', $request->pathword)->whereNotNull('metadata')->firstOrFail();
} catch (ModelNotFoundException $e) { } catch (ModelNotFoundException $e) {
// Fetch from remote // Fetch from remote
$remoteComic = $this->copyManga->comic($request->pathword); $remoteComic = $this->copyManga->comic($request->pathword);
@@ -345,9 +345,13 @@ class ComicController extends Controller
// Update history // Update history
$request->user()->readingHistories()->attach($chapterObj->id, ['comic_id' => $comicObj->id]); $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']);
return Inertia::render('Comic/Read', [ return Inertia::render('Comic/Read', [
'comic' => $this->scToZh($comic), 'comic' => $this->scToZh($comic),
'chapter' => $this->scToZh($chapter), 'chapter' => $this->scToZh($chapter),
'chapters' => $this->scToZh($chapters),
]); ]);
} }
@@ -385,6 +389,13 @@ class ComicController extends Controller
return redirect()->route('comics.histories'); 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 public function destroyHistory(Request $request, string $pathword): Response
{ {
$comicId = Comic::where('pathword', $pathword)->firstOrFail(['id'])->id; $comicId = Comic::where('pathword', $pathword)->firstOrFail(['id'])->id;
@@ -399,6 +410,12 @@ class ComicController extends Controller
]); ]);
} }
/**
* Remove duplicated records
*
* @param Request $request
* @return RedirectResponse
*/
public function destroyHistories(Request $request): RedirectResponse public function destroyHistories(Request $request): RedirectResponse
{ {
$result = $request->user()->cleanUpReadingHistories(); $result = $request->user()->cleanUpReadingHistories();

View File

@@ -43,7 +43,6 @@ class ProfileController extends Controller
$settings = $request->user()->settings; $settings = $request->user()->settings;
$settings['timezone'] = $request->get('timezone'); $settings['timezone'] = $request->get('timezone');
$request->user()->settings = $settings; $request->user()->settings = $settings;
$request->user()->save();
$request->user()->save(); $request->user()->save();

View File

@@ -12,7 +12,7 @@ class ProfileUpdateRequest extends FormRequest
/** /**
* Get the validation rules that apply to the request. * Get the validation rules that apply to the request.
* *
* @return array<string, ValidationRule|array<mixed>|string> * @return array<string, ValidationRule|array|string>
*/ */
public function rules(): array public function rules(): array
{ {

View File

@@ -71,13 +71,11 @@ class User extends Authenticatable implements MustVerifyEmail
$userId = $this->id; $userId = $this->id;
// Step 1: Identify records to keep // Step 1: Identify records to keep
$idsToKeep = ReadingHistory::query() $idsToKeep = ReadingHistory::query()->select('reading_histories.id')
->select('reading_histories.id') // Specify table name explicitly
->joinSub( ->joinSub(
ReadingHistory::query() ReadingHistory::query()
->select('comic_id', 'user_id', 'chapter_id', DB::raw('MIN(created_at) as earliest_created_at')) ->select('comic_id', 'user_id', 'chapter_id', DB::raw('MIN(created_at) as earliest_created_at'))
->where('user_id', $userId) // Specify user_id with table name ->where('user_id', $userId)->groupBy('comic_id', 'user_id', 'chapter_id'),
->groupBy('comic_id', 'user_id', 'chapter_id'),
'b', 'b',
function (JoinClause $join) { function (JoinClause $join) {
$join->on('reading_histories.comic_id', '=', 'b.comic_id') $join->on('reading_histories.comic_id', '=', 'b.comic_id')
@@ -85,14 +83,10 @@ class User extends Authenticatable implements MustVerifyEmail
->on('reading_histories.chapter_id', '=', 'b.chapter_id') ->on('reading_histories.chapter_id', '=', 'b.chapter_id')
->on('reading_histories.created_at', '=', 'b.earliest_created_at'); ->on('reading_histories.created_at', '=', 'b.earliest_created_at');
} }
) )->where('reading_histories.user_id', $userId)->pluck('id');
->where('reading_histories.user_id', $userId) // Specify table name explicitly
->pluck('id');
// Step 2: Delete duplicates for the user // Step 2: Delete duplicates for the user
$deletedCount = ReadingHistory::where('user_id', $userId) $deletedCount = ReadingHistory::where('user_id', $userId)->whereNotIn('id', $idsToKeep)->delete();
->whereNotIn('id', $idsToKeep)
->delete();
// Return the result as an array // Return the result as an array
return [ return [

15
composer.lock generated
View File

@@ -8759,16 +8759,16 @@
}, },
{ {
"name": "sebastian/comparator", "name": "sebastian/comparator",
"version": "6.2.1", "version": "6.3.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/sebastianbergmann/comparator.git", "url": "https://github.com/sebastianbergmann/comparator.git",
"reference": "43d129d6a0f81c78bee378b46688293eb7ea3739" "reference": "d4e47a769525c4dd38cea90e5dcd435ddbbc7115"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/43d129d6a0f81c78bee378b46688293eb7ea3739", "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/d4e47a769525c4dd38cea90e5dcd435ddbbc7115",
"reference": "43d129d6a0f81c78bee378b46688293eb7ea3739", "reference": "d4e47a769525c4dd38cea90e5dcd435ddbbc7115",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@@ -8781,6 +8781,9 @@
"require-dev": { "require-dev": {
"phpunit/phpunit": "^11.4" "phpunit/phpunit": "^11.4"
}, },
"suggest": {
"ext-bcmath": "For comparing BcMath\\Number objects"
},
"type": "library", "type": "library",
"extra": { "extra": {
"branch-alias": { "branch-alias": {
@@ -8824,7 +8827,7 @@
"support": { "support": {
"issues": "https://github.com/sebastianbergmann/comparator/issues", "issues": "https://github.com/sebastianbergmann/comparator/issues",
"security": "https://github.com/sebastianbergmann/comparator/security/policy", "security": "https://github.com/sebastianbergmann/comparator/security/policy",
"source": "https://github.com/sebastianbergmann/comparator/tree/6.2.1" "source": "https://github.com/sebastianbergmann/comparator/tree/6.3.0"
}, },
"funding": [ "funding": [
{ {
@@ -8832,7 +8835,7 @@
"type": "github" "type": "github"
} }
], ],
"time": "2024-10-31T05:30:08+00:00" "time": "2025-01-06T10:28:19+00:00"
}, },
{ {
"name": "sebastian/complexity", "name": "sebastian/complexity",

View File

@@ -5,11 +5,11 @@ import { Moon, Sun } from 'lucide-react';
import { Separator } from '@radix-ui/react-separator'; import { Separator } from '@radix-ui/react-separator';
import { Tooltip, TooltipProvider, TooltipTrigger } from '@radix-ui/react-tooltip'; import { Tooltip, TooltipProvider, TooltipTrigger } from '@radix-ui/react-tooltip';
import { AppSidebar } from '@/components/ui/app-sidebar.jsx'; import { AppSidebar } from '@/components/ui/app-sidebar';
import { Breadcrumb, BreadcrumbItem, BreadcrumbLink, BreadcrumbList } from '@/components/ui/breadcrumb'; import { Breadcrumb, BreadcrumbItem, BreadcrumbLink, BreadcrumbList } from '@/components/ui/breadcrumb';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { SidebarInset, SidebarProvider, SidebarTrigger } from '@/components/ui/sidebar'; import { SidebarInset, SidebarProvider, SidebarTrigger } from '@/components/ui/sidebar';
import { TooltipContent } from '@/components/ui/tooltip.jsx'; import { TooltipContent } from '@/components/ui/tooltip';
import { Toaster } from '@/components/ui/toaster'; import { Toaster } from '@/components/ui/toaster';
export default function AppLayout({ auth, header, children, toolbar }) { export default function AppLayout({ auth, header, children, toolbar }) {
@@ -59,17 +59,12 @@ export default function AppLayout({ auth, header, children, toolbar }) {
<span className="flex gap-1 ml-auto justify-center content-center"> <span className="flex gap-1 ml-auto justify-center content-center">
<TooltipProvider> <TooltipProvider>
<Tooltip> <Tooltip>
<TooltipTrigger> <TooltipTrigger asChild>
{ theme === 'dark' && ( { theme === 'dark' ? (<Button variant="link" size="icon" onClick={ () => themeButtonOnclickHandler('light') }>
<Button variant="link" size="icon" onClick={ () => themeButtonOnclickHandler('light') }> <Sun />
<Sun /> </Button>) : (<Button variant="link" size="icon" onClick={ () => themeButtonOnclickHandler('dark') }>
</Button> <Moon />
) } </Button>) }
{ theme === 'light' && (
<Button variant="link" size="icon" onClick={ () => themeButtonOnclickHandler('dark') }>
<Moon />
</Button>
) }
</TooltipTrigger> </TooltipTrigger>
<TooltipContent> <TooltipContent>
<p>Toggle day / night mode</p> <p>Toggle day / night mode</p>

View File

@@ -50,10 +50,12 @@ export default function Chapters({ auth, comic, chapters, histories, offset }) {
const groupOnClickHandler = (pathword) => { const groupOnClickHandler = (pathword) => {
router.get(`/comic/${ comic.comic.path_word }?group=${ pathword }`, {}, { router.get(`/comic/${ comic.comic.path_word }?group=${ pathword }`, {}, {
only: ['chapters'], only: ['chapters'],
preserveState: true preserveState: true,
onSuccess: () => {
setGroup(pathword);
setAscending(true);
}
}); });
setGroup(pathword);
setAscending(true);
} }
const ComicChapterLink = (props) => { const ComicChapterLink = (props) => {
@@ -160,16 +162,17 @@ export default function Chapters({ auth, comic, chapters, histories, offset }) {
</div> </div>
</CardContent> </CardContent>
<CardContent> <CardContent>
<Tabs defaultValue={ group } className="w-full"> <Tabs defaultValue={ group } className="w-full" onValueChange={ (e) => groupOnClickHandler(e)} >
<div className="flex"> <div className="flex">
<TabsList className={ `grid w-full grid-cols-${ Object.entries(comic.groups).length } ` }> <TabsList className={ `grid w-full grid-cols-${ Object.entries(comic.groups).length } ` }>
{ Object.entries(comic.groups).map((g, i) => ( { Object.entries(comic.groups).map((g, i) => (
<TabsTrigger onClick={ () => groupOnClickHandler(g[1].path_word) } <TabsTrigger key={ g[1].path_word } value={ g[1].path_word }>
key={ g[1].path_word }
value={ g[1].path_word }>
{ g[1].name } { g[1].name }
<Badge key={ g[1].path_word } className="ml-2" variant="outline">
{ g[1].count }
</Badge>
</TabsTrigger> </TabsTrigger>
)) } ) ) }
</TabsList> </TabsList>
<div className="flex justify-end"> <div className="flex justify-end">
<TooltipProvider> <TooltipProvider>
@@ -200,7 +203,8 @@ export default function Chapters({ auth, comic, chapters, histories, offset }) {
</div> </div>
</div> </div>
<TabsContent value={ group }> <TabsContent value={ group }>
<div className="w-full grid grid-cols-1 md:grid-cols-2 lg:grid-cols-6 xl:grid-cols-12 gap-1"> <div
className="w-full grid grid-cols-1 md:grid-cols-2 lg:grid-cols-6 2xl:grid-cols-12 gap-1">
{ (chapters.total > chapters.limit && chapters.offset > 0) && ( { (chapters.total > chapters.limit && chapters.offset > 0) && (
<Button size="sm" variant="outline" asChild> <Button size="sm" variant="outline" asChild>
<Link href="?" only={['chapters', 'offset']} headers={{ offset: parseInt(chapters.offset) - chapters.limit }}> <Link href="?" only={['chapters', 'offset']} headers={{ offset: parseInt(chapters.offset) - chapters.limit }}>

View File

@@ -1,13 +1,15 @@
import { Head, Link, router } from '@inertiajs/react';
import { Button } from '@/components/ui/button';
import AppLayout from '@/Layouts/AppLayout.jsx';
import { Card, CardContent, CardFooter, CardHeader, CardTitle } from '@/components/ui/card';
import { Star } from 'lucide-react';
import { Badge } from '@/components/ui/badge';
import { BreadcrumbItem, BreadcrumbPage, BreadcrumbSeparator } from "@/components/ui/breadcrumb";
import { useToast } from "@/hooks/use-toast.js";
import { useState } from "react"; import { useState } from "react";
import { Head, Link, } from '@inertiajs/react';
import { Star } from 'lucide-react';
import AppLayout from '@/Layouts/AppLayout.jsx';
import { Badge } from '@/components/ui/badge';
import { BreadcrumbItem, BreadcrumbPage, BreadcrumbSeparator } from "@/components/ui/breadcrumb";
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardFooter, CardHeader, CardTitle } from '@/components/ui/card';
import { useToast } from '@/hooks/use-toast.js';
export default function Favourites({ auth, favourites }) { export default function Favourites({ auth, favourites }) {
@@ -45,9 +47,9 @@ export default function Favourites({ auth, favourites }) {
<CardHeader> <CardHeader>
<CardTitle><Link href={ `/comic/${ props.pathword }` }>{ props.name }</Link></CardTitle> <CardTitle><Link href={ `/comic/${ props.pathword }` }>{ props.name }</Link></CardTitle>
<div className="flex gap-2 items-end justify-between"> <div className="flex gap-2 items-end justify-between">
<p> <div>
{ props.authors.map(a => <Badge key={ a.path_word } variant="outline"><Link>{ a.name }</Link></Badge>) } { props.authors.map(a => <Badge key={ a.name } variant="outline">{ a.name }</Badge>) }
</p> </div>
<p className="text-right text-sm"> <p className="text-right text-sm">
Updated: { props.upstream_updated_at } Updated: { props.upstream_updated_at }
</p> </p>

View File

@@ -116,25 +116,21 @@ export default function Histories({ auth, histories }) {
</Table> </Table>
<div> <div>
<Pagination className="justify-end"> <Pagination className="justify-end">
<PaginationContent> <PaginationContent>
{ histories.current_page > 1 && ( { histories.current_page > 1 && (
<PaginationItem>
<PaginationPrevious href={ histories.prev_page_url } only={['histories']} />
</PaginationItem>
) }
<PaginationItem> <PaginationItem>
<PaginationPrevious href={ histories.prev_page_url } only={['histories']} /> <PaginationLink href="#">{ histories.current_page }</PaginationLink>
</PaginationItem> </PaginationItem>
) } { histories.current_page < histories.last_page && (
<PaginationItem> <PaginationItem>
<PaginationLink href="#">{ histories.current_page }</PaginationLink> <PaginationNext href={ histories.next_page_url } only={['histories']} />
</PaginationItem> </PaginationItem>
<PaginationItem> ) }
</PaginationItem> </PaginationContent>
<PaginationItem>
{ histories.current_page < histories.last_page && (
<PaginationItem>
<PaginationNext href={ histories.next_page_url } only={['histories']} />
</PaginationItem>
) }
</PaginationItem>
</PaginationContent>
</Pagination> </Pagination>
</div> </div>
</div> </div>

View File

@@ -14,7 +14,7 @@ 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();
const favouriteOnClickHandler = (pathword) => { const favouriteOnClickHandler = (pathword) => {

View File

@@ -1,7 +1,7 @@
import { Head, Link, router } from '@inertiajs/react'; import { Head, Link, router } from '@inertiajs/react';
import AppLayout from '@/Layouts/AppLayout.jsx'; import AppLayout from '@/Layouts/AppLayout.jsx';
import React, { useEffect, useLayoutEffect, useRef, useState } from 'react'; import React, { useEffect, useLayoutEffect, useRef, useState } from 'react';
import { ChevronFirst, ChevronLast, Rows3, Settings } from 'lucide-react'; import { ChevronFirst, ChevronLast, Rows3 } from 'lucide-react';
import { Tooltip, TooltipProvider, TooltipTrigger } from '@radix-ui/react-tooltip'; import { Tooltip, TooltipProvider, TooltipTrigger } from '@radix-ui/react-tooltip';
import { throttle } from 'lodash'; import { throttle } from 'lodash';
@@ -14,22 +14,18 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@
import { Switch } from '@/components/ui/switch'; import { Switch } from '@/components/ui/switch';
import { TooltipContent } from '@/components/ui/tooltip'; import { TooltipContent } from '@/components/ui/tooltip';
export default function Read({ auth, comic, chapter }) { export default function Read({ auth, comic, chapter, chapters }) {
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); // TODO const [isTwoPagesPerScreen, setIsTwoPagePerScreen] = useState(false); // TODO
const [currentImage, setCurrentImage] = useState(1); const [currentImage, setCurrentImage] = useState(1);
const [isJumpToPageOpen, setIsJumpToPageOpen] = useState(false);
const windowSize = useWindowSize();
const ref = useRef();
const [divDimensions, setDivDimensions] = useState([0, 0]); const [divDimensions, setDivDimensions] = useState([0, 0]);
const [loading, setLoading] = useState(true); const ref = useRef();
const windowSize = useWindowSize();
const getLocalStorageReadingMode = () => { const getLocalStorageReadingMode = () => {
if (window.localStorage.getItem('readingMode') !== null && validReadingModes.includes(window.localStorage.getItem('readingMode'))) { if (window.localStorage.getItem('readingMode') !== null && validReadingModes.includes(window.localStorage.getItem('readingMode'))) {
@@ -39,6 +35,14 @@ export default function Read({ auth, comic, chapter }) {
return "rtl"; return "rtl";
} }
const getLocalStorageIsTwoPagePerScreen = () => {
if (window.localStorage.getItem('twoPagesPerScreen') !== null && validReadingModes.includes(window.localStorage.getItem('twoPagesPerScreen'))) {
return window.localStorage.getItem('twoPagesPerScreen');
}
return false;
}
function useWindowSize() { function useWindowSize() {
const [size, setSize] = useState([0, 0]); const [size, setSize] = useState([0, 0]);
useLayoutEffect(() => { useLayoutEffect(() => {
@@ -64,10 +68,26 @@ export default function Read({ auth, comic, chapter }) {
} }
} }
const selectOnChangeHandler = (e) => { const toggleTwoPagesPerScreen = (e) => {
if (e) {
window.localStorage.setItem('twoPagesPerScreen', true);
setIsTwoPagePerScreen(true);
} else {
window.localStorage.setItem('twoPagesPerScreen', false);
setIsTwoPagePerScreen(false);
}
}
const imageSelectOnChangeHandler = (e) => {
document.getElementById(`image-${e - 1}`)?.scrollIntoView(); document.getElementById(`image-${e - 1}`)?.scrollIntoView();
setCurrentImage(e); setCurrentImage(e);
setIsJumpToPageOpen(false); }
const chapterSelectOnChangeHandler = (e) => {
router.visit(route('comics.read', {
pathword: comic.comic.path_word,
uuid: e
}));
} }
const setViewPort = (e) => { const setViewPort = (e) => {
@@ -109,36 +129,22 @@ export default function Read({ auth, comic, chapter }) {
} }
}; };
return (<div className="basis-full"> return (
<img <div className="basis-full">
id={ `image-${ img.innerKey }` } <img alt={ comic.comic.name } className={` m-auto comic-img `} id={ `image-${ img.innerKey }` } ref={ imgRef }
ref={ imgRef } onClick={ handleImageClick } src={ `/image/${ btoa(img.url) }` } style={ imgStyles } />
style={ imgStyles } </div>
className={` m-auto comic-img `} );
src={ `/image/${ btoa(img.url) }` }
onClick={ handleImageClick }
alt={ comic.comic.name }
/>
</div>);
} }
const Toolbar = () => { const Toolbar = () => {
return ( return (
<> <>
<Dialog> <Dialog>
<DialogTrigger> <DialogTrigger asChild>
<TooltipProvider> <Button variant="ghost">
<Tooltip> { currentImage } / { chapter.sorted.length }
<TooltipTrigger> </Button>
<Button variant="link" size="icon">
<Settings />
</Button>
</TooltipTrigger>
<TooltipContent>
<p>Options</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</DialogTrigger> </DialogTrigger>
<DialogContent className="max-w-[600px]"> <DialogContent className="max-w-[600px]">
<DialogHeader> <DialogHeader>
@@ -157,6 +163,54 @@ export default function Read({ auth, comic, chapter }) {
onCheckedChange={ (e) => toggleReadingMode(e) } /> onCheckedChange={ (e) => toggleReadingMode(e) } />
</div> </div>
</div> </div>
<div className="space-y-4">
<div className="flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
<div className="space-y-0.5">
<label>Two images per screen</label>
<p>Only applicable to RTL mode</p>
</div>
<Switch defaultChecked={ (isTwoPagesPerScreen) }
onCheckedChange={ (e) => toggleTwoPagesPerScreen(e) } />
</div>
</div>
<div className="space-y-4">
<div className="flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
<div className="space-y-0.5">
<label>Jump to Chapter</label>
</div>
<Select onValueChange={ (e) => chapterSelectOnChangeHandler(e) } defaultValue={ chapter.chapter.uuid }>
<SelectTrigger className="w-[50%]">
<SelectValue />
</SelectTrigger>
<SelectContent position="popper" sideOffset={ -100 }>
{ chapters.map(c => (
<SelectItem key={ c.chapter_uuid } value={ c.chapter_uuid }>
{ c.name }
</SelectItem>
) ) }
</SelectContent>
</Select>
</div>
</div>
<div className="space-y-4">
<div className="flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
<div className="space-y-0.5">
<label>Jump to Page</label>
</div>
<Select onValueChange={ (e) => imageSelectOnChangeHandler(e) } defaultValue={ currentImage }>
<SelectTrigger className="w-[50%]">
<SelectValue />
</SelectTrigger>
<SelectContent position="popper" sideOffset={ -100 }>
{ chapter.sorted.map((img, i) => (
<SelectItem key={ i + 1 } value={ i + 1 }>
{ i + 1 } / { chapter.sorted.length }
</SelectItem>
) ) }
</SelectContent>
</Select>
</div>
</div>
<DialogFooter> <DialogFooter>
<DialogClose asChild> <DialogClose asChild>
<PrimaryButton>Done</PrimaryButton> <PrimaryButton>Done</PrimaryButton>
@@ -165,67 +219,10 @@ export default function Read({ auth, comic, chapter }) {
</DialogContent> </DialogContent>
</Dialog> </Dialog>
<Dialog open={ isJumpToPageOpen }>
<DialogTrigger>
<TooltipProvider>
<Tooltip>
<TooltipTrigger>
<Button variant="ghost" onClick={ () => setIsJumpToPageOpen(!isJumpToPageOpen) }>
{ currentImage } / { chapter.sorted.length }
</Button>
</TooltipTrigger>
<TooltipContent>
<p>Jump To</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</DialogTrigger>
<DialogContent className="max-w-[600px]">
<DialogHeader>
<DialogTitle>Jump to</DialogTitle>
<DialogDescription>
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
<Select defaultOpen onValueChange={ (e) => selectOnChangeHandler(e) } defaultValue={ currentImage}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
{ chapter.sorted.map((img, i) => (
<SelectItem key={ i + 1 } value={ i + 1 }>{ i + 1 } / { chapter.sorted.length }</SelectItem>
))}
</SelectContent>
</Select>
</div>
<DialogFooter>
<DialogClose asChild>
<PrimaryButton onClick={ () => setIsJumpToPageOpen(false) }>Done</PrimaryButton>
</DialogClose>
</DialogFooter>
</DialogContent>
</Dialog>
<TooltipProvider>
<Tooltip>
<TooltipTrigger>
<Button variant="link" size="icon" asChild>
<Link href={ route('comics.chapters', [comic.comic.path_word]) }>
<Rows3 />
</Link>
</Button>
</TooltipTrigger>
<TooltipContent>
<p>TOC</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
{ chapter.chapter.prev && ( { chapter.chapter.prev && (
<TooltipProvider> <TooltipProvider>
<Tooltip> <Tooltip>
<TooltipTrigger> <TooltipTrigger asChild>
<Button variant="link" size="icon" asChild> <Button variant="link" size="icon" asChild>
<Link href={ route('comics.read', [comic.comic.path_word, chapter.chapter.prev]) }> <Link href={ route('comics.read', [comic.comic.path_word, chapter.chapter.prev]) }>
<ChevronFirst /> <ChevronFirst />
@@ -239,10 +236,25 @@ export default function Read({ auth, comic, chapter }) {
</TooltipProvider> </TooltipProvider>
) } ) }
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button variant="link" size="icon" asChild>
<Link href={ route('comics.chapters', [comic.comic.path_word]) }>
<Rows3 />
</Link>
</Button>
</TooltipTrigger>
<TooltipContent>
<p>TOC</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
{ chapter.chapter.next && ( { chapter.chapter.next && (
<TooltipProvider> <TooltipProvider>
<Tooltip> <Tooltip>
<TooltipTrigger> <TooltipTrigger asChild>
<Button variant="link" size="icon" asChild> <Button variant="link" size="icon" asChild>
<Link href={ route('comics.read', [comic.comic.path_word, chapter.chapter.next]) }> <Link href={ route('comics.read', [comic.comic.path_word, chapter.chapter.next]) }>
<ChevronLast /> <ChevronLast />
@@ -265,6 +277,7 @@ export default function Read({ auth, comic, chapter }) {
useEffect(() => { useEffect(() => {
setReadingMode(getLocalStorageReadingMode()); setReadingMode(getLocalStorageReadingMode());
setIsTwoPagePerScreen(getLocalStorageIsTwoPagePerScreen());
if (!ref.current) return; if (!ref.current) return;
@@ -280,7 +293,7 @@ export default function Read({ auth, comic, chapter }) {
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 + 76 >= imageTop && containerScrollTop < imageBottom) { // Magic number 76
visibleImageIndex = i; visibleImageIndex = i;
} }
}); });

View File

@@ -30,7 +30,7 @@ export default function Installation({ auth }) {
</ul> </ul>
<p className="pt-2">Steps for installation</p> <p className="pt-2">Steps for installation</p>
<ol className="list-decimal ml-6 pt-2"> <ol className="list-decimal ml-6 pt-2">
<li>Clone the git repo <span className="rounded-md border m-1 p-1 font-mono text-sm shadow-sm">https://ds19910209@bitbucket.org/pokebeacon/cv4.git</span></li> <li>Clone the git repo <span className="rounded-md border m-1 p-1 font-mono text-sm shadow-sm">https://bitbucket.org/pokebeacon/cv4.git</span></li>
<li>Install PHP dependencies <span className="rounded-md border m-1 p-1 font-mono text-sm shadow-sm">composer install</span></li> <li>Install PHP dependencies <span className="rounded-md border m-1 p-1 font-mono text-sm shadow-sm">composer install</span></li>
<li>Clone .env.example file to .env</li> <li>Clone .env.example file to .env</li>
<li>Edit any items in .env if needed</li> <li>Edit any items in .env if needed</li>

View File

@@ -11,6 +11,20 @@ export default function Updates({ auth }) {
<title>Updates</title> <title>Updates</title>
</Head> </Head>
<div className="p-3 pt-1"> <div className="p-3 pt-1">
<Card className="w-[90%] m-3 mx-auto">
<CardHeader>
<CardTitle>0.1.1</CardTitle>
<CardDescription>Release: 06 Jan 2025</CardDescription>
</CardHeader>
<CardContent>
<ul>
<li>Fixed: errors in frontend</li>
<li>Fixed: favourites w/o metadata return error in frontend</li>
<li>Removed gear button on reading page</li>
<li>Jump to any chapters directly in reading page</li>
</ul>
</CardContent>
</Card>
<Card className="w-[90%] m-3 mx-auto"> <Card className="w-[90%] m-3 mx-auto">
<CardHeader> <CardHeader>
<CardTitle>0.1.0</CardTitle> <CardTitle>0.1.0</CardTitle>

View File

@@ -10,8 +10,8 @@ export default function Edit({ auth, mustVerifyEmail, status, timezones }) {
return ( return (
<AppLayout auth={ auth } header={ <AppLayout auth={ auth } header={
<> <>
<BreadcrumbSeparator /> <span className="hidden lg:block"><BreadcrumbSeparator /></span>
<BreadcrumbItem> <BreadcrumbItem>
<BreadcrumbLink>Profile</BreadcrumbLink> <BreadcrumbLink>Profile</BreadcrumbLink>
</BreadcrumbItem> </BreadcrumbItem>
</> </>

View File

@@ -1,20 +1,14 @@
import { useRef } from 'react';
import { useForm } from '@inertiajs/react';
import DangerButton from '@/components/DangerButton'; import DangerButton from '@/components/DangerButton';
import InputError from '@/components/InputError'; import InputError from '@/components/InputError';
import SecondaryButton from '@/components/SecondaryButton'; import SecondaryButton from '@/components/SecondaryButton';
import TextInput from '@/components/TextInput'; import TextInput from '@/components/TextInput';
import { useForm } from '@inertiajs/react'; import { Card, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
import { useRef, useState } from 'react'; import { Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"
import {
Dialog, DialogClose,
DialogContent,
DialogDescription, DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog"
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card.jsx";
export default function DeleteUserForm({ className = '' }) { export default function DeleteUserForm() {
const passwordInput = useRef(); const passwordInput = useRef();
const { const {
@@ -54,12 +48,12 @@ export default function DeleteUserForm({ className = '' }) {
<DialogContent className="max-w-[800px]"> <DialogContent className="max-w-[800px]">
<DialogHeader> <DialogHeader>
<DialogTitle>Are you sure you want to delete your account?</DialogTitle> <DialogTitle>Are you sure you want to delete your account?</DialogTitle>
<DialogDescription className="pt-3"> <DialogDescription className="pt-3">
Once your account is deleted, all of its resources and Once your account is deleted, all of its resources and
data will be permanently deleted. Please enter your data will be permanently deleted. Please enter your
password to confirm you would like to permanently delete password to confirm you would like to permanently delete
your account. your account.
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<div className="items-center gap-4"> <div className="items-center gap-4">
<TextInput id="password" type="password" name="password" ref={ passwordInput } <TextInput id="password" type="password" name="password" ref={ passwordInput }

View File

@@ -57,7 +57,7 @@ export function AppSidebar({ auth }) {
</div> </div>
<div className="flex flex-col gap-0.5 leading-none"> <div className="flex flex-col gap-0.5 leading-none">
<span className="font-semibold">Comic</span> <span className="font-semibold">Comic</span>
<span>0.1.0</span> <span>0.1.1</span>
</div> </div>
</Link> </Link>
</SidebarMenuButton> </SidebarMenuButton>