dave
This commit is contained in:
parent
ef0c922762
commit
d5087fc4e7
@ -7,7 +7,27 @@ const menuAside: MenuAsideItem[] = [
|
||||
icon: icon.mdiViewDashboardOutline,
|
||||
label: 'Dashboard',
|
||||
},
|
||||
|
||||
{
|
||||
href: '/browse-sources',
|
||||
icon: icon.mdiSourceBranch,
|
||||
label: 'Browse Extensions',
|
||||
},
|
||||
{
|
||||
href: '/library_entries/library_entries-list',
|
||||
label: 'My Library',
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
icon: 'mdiLibraryShelves' in icon ? icon['mdiLibraryShelves' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
||||
permissions: 'READ_LIBRARY_ENTRIES'
|
||||
},
|
||||
{
|
||||
href: '/series/series-list',
|
||||
label: 'Local Series',
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
icon: 'mdiBookOpenPageVariant' in icon ? icon['mdiBookOpenPageVariant' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
||||
permissions: 'READ_SERIES'
|
||||
},
|
||||
{
|
||||
href: '/users/users-list',
|
||||
label: 'Users',
|
||||
@ -34,7 +54,7 @@ const menuAside: MenuAsideItem[] = [
|
||||
},
|
||||
{
|
||||
href: '/sources/sources-list',
|
||||
label: 'Sources',
|
||||
label: 'Sources Admin',
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
icon: 'mdiSourceRepository' in icon ? icon['mdiSourceRepository' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
||||
@ -42,20 +62,12 @@ const menuAside: MenuAsideItem[] = [
|
||||
},
|
||||
{
|
||||
href: '/extensions/extensions-list',
|
||||
label: 'Extensions',
|
||||
label: 'Extensions Admin',
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
icon: 'mdiPuzzle' in icon ? icon['mdiPuzzle' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
||||
permissions: 'READ_EXTENSIONS'
|
||||
},
|
||||
{
|
||||
href: '/series/series-list',
|
||||
label: 'Series',
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
icon: 'mdiBookOpenPageVariant' in icon ? icon['mdiBookOpenPageVariant' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
||||
permissions: 'READ_SERIES'
|
||||
},
|
||||
{
|
||||
href: '/chapters/chapters-list',
|
||||
label: 'Chapters',
|
||||
@ -72,14 +84,6 @@ const menuAside: MenuAsideItem[] = [
|
||||
icon: 'mdiImageMultiple' in icon ? icon['mdiImageMultiple' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
||||
permissions: 'READ_CHAPTER_PAGES'
|
||||
},
|
||||
{
|
||||
href: '/library_entries/library_entries-list',
|
||||
label: 'Library entries',
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
icon: 'mdiLibraryShelves' in icon ? icon['mdiLibraryShelves' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
||||
permissions: 'READ_LIBRARY_ENTRIES'
|
||||
},
|
||||
{
|
||||
href: '/categories/categories-list',
|
||||
label: 'Categories',
|
||||
@ -208,4 +212,4 @@ const menuAside: MenuAsideItem[] = [
|
||||
},
|
||||
]
|
||||
|
||||
export default menuAside
|
||||
export default menuAside
|
||||
83
frontend/src/pages/browse-sources.tsx
Normal file
83
frontend/src/pages/browse-sources.tsx
Normal file
@ -0,0 +1,83 @@
|
||||
import React, { useEffect } from 'react'
|
||||
import Head from 'next/head'
|
||||
import { mdiOpenInApp, mdiSourceBranch, mdiRefresh } from '@mdi/js'
|
||||
import LayoutAuthenticated from '../layouts/Authenticated'
|
||||
import SectionMain from '../components/SectionMain'
|
||||
import SectionTitleLineWithButton from '../components/SectionTitleLineWithButton'
|
||||
import CardBox from '../components/CardBox'
|
||||
import { useAppDispatch, useAppSelector } from '../stores/hooks'
|
||||
import { fetch } from '../stores/sources/sourcesSlice'
|
||||
import BaseButton from '../components/BaseButton'
|
||||
import { getPageTitle } from '../config'
|
||||
import Link from 'next/link'
|
||||
|
||||
const BrowseSourcesPage = () => {
|
||||
const dispatch = useAppDispatch()
|
||||
const { sources, loading } = useAppSelector((state) => state.sources)
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetch({}))
|
||||
}, [dispatch])
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>{getPageTitle('Browse Sources')}</title>
|
||||
</Head>
|
||||
|
||||
<SectionMain>
|
||||
<SectionTitleLineWithButton icon={mdiSourceBranch} title="Manga Sources" main>
|
||||
<BaseButton
|
||||
icon={mdiRefresh}
|
||||
color="whiteDark"
|
||||
onClick={() => dispatch(fetch({}))}
|
||||
disabled={loading}
|
||||
/>
|
||||
</SectionTitleLineWithButton>
|
||||
|
||||
<div className="grid grid-cols-1 gap-6 lg:grid-cols-3 mb-6">
|
||||
{sources.map((source: any) => (
|
||||
<CardBox key={source.id} className="flex flex-col">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h2 className="text-xl font-bold">{source.name}</h2>
|
||||
<span className={`px-2 py-1 text-xs rounded ${source.enabled ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'}`}>
|
||||
{source.enabled ? 'Enabled' : 'Disabled'}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-gray-500 text-sm mb-4 flex-grow">
|
||||
{source.base_url}
|
||||
</p>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-xs text-gray-400 uppercase tracking-wider">
|
||||
{source.source_type} • {source.default_language}
|
||||
</span>
|
||||
<Link href={`/sources/browse/${source.id}`} passHref legacyBehavior>
|
||||
<BaseButton
|
||||
color="info"
|
||||
label="Browse"
|
||||
icon={mdiOpenInApp}
|
||||
small
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
</CardBox>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{sources.length === 0 && !loading && (
|
||||
<CardBox>
|
||||
<div className="text-center py-10">
|
||||
<p className="text-gray-500">No sources found. Add a source in the admin panel to get started.</p>
|
||||
</div>
|
||||
</CardBox>
|
||||
)}
|
||||
</SectionMain>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
BrowseSourcesPage.getLayout = function getLayout(page: React.ReactElement) {
|
||||
return <LayoutAuthenticated>{page}</LayoutAuthenticated>
|
||||
}
|
||||
|
||||
export default BrowseSourcesPage
|
||||
124
frontend/src/pages/manga/view/[sourceId]/[mangaId].tsx
Normal file
124
frontend/src/pages/manga/view/[sourceId]/[mangaId].tsx
Normal file
@ -0,0 +1,124 @@
|
||||
import React, { useEffect } from 'react'
|
||||
import Head from 'next/head'
|
||||
import { useRouter } from 'next/router'
|
||||
import { mdiBookOpenVariant, mdiArrowLeft, mdiInformation } from '@mdi/js'
|
||||
import LayoutAuthenticated from '../../../layouts/Authenticated'
|
||||
import SectionMain from '../../../components/SectionMain'
|
||||
import SectionTitleLineWithButton from '../../../components/SectionTitleLineWithButton'
|
||||
import CardBox from '../../../components/CardBox'
|
||||
import { useAppDispatch, useAppSelector } from '../../../stores/hooks'
|
||||
import { fetchMangaDetails, fetchChapters } from '../../../stores/mangaSlice'
|
||||
import BaseButton from '../../../components/BaseButton'
|
||||
import { getPageTitle } from '../../../config'
|
||||
import Link from 'next/link'
|
||||
|
||||
const MangaDetailsPage = () => {
|
||||
const router = useRouter()
|
||||
const { sourceId, mangaId } = router.query
|
||||
const dispatch = useAppDispatch()
|
||||
const { mangaDetails, chapters, loading } = useAppSelector((state) => state.manga)
|
||||
|
||||
useEffect(() => {
|
||||
if (sourceId && mangaId && typeof sourceId === 'string' && typeof mangaId === 'string') {
|
||||
dispatch(fetchMangaDetails({ sourceId, mangaId }))
|
||||
dispatch(fetchChapters({ sourceId, mangaId }))
|
||||
}
|
||||
}, [sourceId, mangaId, dispatch])
|
||||
|
||||
if (!mangaDetails && loading) {
|
||||
return <div className="text-center py-20">Loading...</div>
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>{getPageTitle(mangaDetails?.title || 'Manga Details')}</title>
|
||||
</Head>
|
||||
|
||||
<SectionMain>
|
||||
<SectionTitleLineWithButton
|
||||
icon={mdiInformation}
|
||||
title={mangaDetails?.title || 'Manga Details'}
|
||||
main
|
||||
>
|
||||
<Link href={`/sources/browse/${sourceId}`} passHref legacyBehavior>
|
||||
<BaseButton icon={mdiArrowLeft} label="Back to Source" color="whiteDark" />
|
||||
</Link>
|
||||
</SectionTitleLineWithButton>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-6 mb-6">
|
||||
<div className="md:col-span-1">
|
||||
<CardBox className="overflow-hidden p-0">
|
||||
<img
|
||||
src={mangaDetails?.coverUrl || 'https://via.placeholder.com/512x720'}
|
||||
alt={mangaDetails?.title}
|
||||
className="w-full h-auto"
|
||||
/>
|
||||
</CardBox>
|
||||
</div>
|
||||
<div className="md:col-span-3">
|
||||
<CardBox className="h-full">
|
||||
<h1 className="text-3xl font-bold mb-4 dark:text-white">{mangaDetails?.title}</h1>
|
||||
<div className="flex flex-wrap gap-2 mb-4">
|
||||
{mangaDetails?.author && (
|
||||
<span className="px-3 py-1 bg-blue-100 text-blue-800 rounded-full text-sm">
|
||||
Author: {mangaDetails.author}
|
||||
</span>
|
||||
)}
|
||||
{mangaDetails?.artist && (
|
||||
<span className="px-3 py-1 bg-purple-100 text-purple-800 rounded-full text-sm">
|
||||
Artist: {mangaDetails.artist}
|
||||
</span>
|
||||
)}
|
||||
<span className="px-3 py-1 bg-gray-100 text-gray-800 rounded-full text-sm">
|
||||
Source: {mangaDetails?.source}
|
||||
</span>
|
||||
<span className="px-3 py-1 bg-green-100 text-green-800 rounded-full text-sm">
|
||||
Status: {mangaDetails?.status}
|
||||
</span>
|
||||
</div>
|
||||
<div className="prose dark:prose-invert max-w-none">
|
||||
<p className="text-gray-600 dark:text-slate-300">
|
||||
{mangaDetails?.description || 'No description available.'}
|
||||
</p>
|
||||
</div>
|
||||
</CardBox>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<CardBox title="Chapters">
|
||||
<div className="divide-y dark:divide-slate-700 max-h-[600px] overflow-y-auto">
|
||||
{chapters.map((chapter: any) => (
|
||||
<div key={chapter.id} className="py-3 flex justify-between items-center group">
|
||||
<div>
|
||||
<span className="font-semibold dark:text-white">Chapter {chapter.chapter}</span>
|
||||
{chapter.title && <span className="ml-2 text-gray-500 text-sm">- {chapter.title}</span>}
|
||||
</div>
|
||||
<Link href={`/reader/${sourceId}/${chapter.id}`} passHref legacyBehavior>
|
||||
<BaseButton
|
||||
color="info"
|
||||
label="Read"
|
||||
icon={mdiBookOpenVariant}
|
||||
small
|
||||
outline
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
))}
|
||||
{chapters.length === 0 && (
|
||||
<div className="py-10 text-center text-gray-500">
|
||||
No chapters found for this manga.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</CardBox>
|
||||
</SectionMain>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
MangaDetailsPage.getLayout = function getLayout(page: React.ReactElement) {
|
||||
return <LayoutAuthenticated>{page}</LayoutAuthenticated>
|
||||
}
|
||||
|
||||
export default MangaDetailsPage
|
||||
111
frontend/src/pages/reader/[sourceId]/[chapterId].tsx
Normal file
111
frontend/src/pages/reader/[sourceId]/[chapterId].tsx
Normal file
@ -0,0 +1,111 @@
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import Head from 'next/head'
|
||||
import { useRouter } from 'next/router'
|
||||
import { mdiArrowLeft, mdiFormatVerticalAlignTop, mdiChevronLeft, mdiChevronRight } from '@mdi/js'
|
||||
import LayoutAuthenticated from '../../../layouts/Authenticated'
|
||||
import SectionMain from '../../../components/SectionMain'
|
||||
import { useAppDispatch, useAppSelector } from '../../../stores/hooks'
|
||||
import { fetchPages, clearReader } from '../../../stores/mangaSlice'
|
||||
import BaseButton from '../../../components/BaseButton'
|
||||
import { getPageTitle } from '../../../config'
|
||||
import Link from 'next/link'
|
||||
|
||||
const ReaderPage = () => {
|
||||
const router = useRouter()
|
||||
const { sourceId, chapterId } = router.query
|
||||
const dispatch = useAppDispatch()
|
||||
const { pages, loading } = useAppSelector((state) => state.manga)
|
||||
const [currentPage, setCurrentPage] = useState(0)
|
||||
|
||||
useEffect(() => {
|
||||
if (sourceId && chapterId && typeof sourceId === 'string' && typeof chapterId === 'string') {
|
||||
dispatch(fetchPages({ sourceId, chapterId }))
|
||||
}
|
||||
return () => {
|
||||
dispatch(clearReader())
|
||||
}
|
||||
}, [sourceId, chapterId, dispatch])
|
||||
|
||||
const scrollToTop = () => {
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' })
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>{getPageTitle('Reading Chapter')}</title>
|
||||
</Head>
|
||||
|
||||
<div className="bg-black min-h-screen text-white">
|
||||
<div className="sticky top-0 z-50 bg-slate-900 bg-opacity-90 px-4 py-2 flex justify-between items-center">
|
||||
<BaseButton
|
||||
icon={mdiArrowLeft}
|
||||
color="white"
|
||||
onClick={() => router.back()}
|
||||
small
|
||||
label="Exit"
|
||||
/>
|
||||
<div className="text-sm font-semibold truncate max-w-xs">
|
||||
Chapter {chapterId}
|
||||
</div>
|
||||
<BaseButton
|
||||
icon={mdiFormatVerticalAlignTop}
|
||||
color="white"
|
||||
onClick={scrollToTop}
|
||||
small
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="max-w-3xl mx-auto py-4">
|
||||
{loading ? (
|
||||
<div className="flex flex-col items-center justify-center py-40">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-info-500"></div>
|
||||
<p className="mt-4 text-gray-400">Loading pages...</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-1">
|
||||
{pages.map((url: string, index: number) => (
|
||||
<div key={index} className="relative w-full flex justify-center bg-slate-950">
|
||||
<img
|
||||
src={url}
|
||||
alt={`Page ${index + 1}`}
|
||||
className="max-w-full h-auto"
|
||||
loading="lazy"
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!loading && pages.length === 0 && (
|
||||
<div className="text-center py-40">
|
||||
<p className="text-gray-500">No pages found for this chapter.</p>
|
||||
<BaseButton
|
||||
label="Go Back"
|
||||
color="info"
|
||||
onClick={() => router.back()}
|
||||
className="mt-4"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="fixed bottom-4 right-4 space-x-2">
|
||||
<BaseButton
|
||||
icon={mdiFormatVerticalAlignTop}
|
||||
color="info"
|
||||
onClick={scrollToTop}
|
||||
rounded-full
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
// We use a blank layout for the reader or Guest layout if we want it fullscreen
|
||||
ReaderPage.getLayout = function getLayout(page: React.ReactElement) {
|
||||
return page // Custom layout included in component
|
||||
}
|
||||
|
||||
export default ReaderPage
|
||||
122
frontend/src/pages/sources/browse/[id].tsx
Normal file
122
frontend/src/pages/sources/browse/[id].tsx
Normal file
@ -0,0 +1,122 @@
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import Head from 'next/head'
|
||||
import { useRouter } from 'next/router'
|
||||
import { mdiMagnify, mdiArrowLeft, mdiViewList } from '@mdi/js'
|
||||
import LayoutAuthenticated from '../../../layouts/Authenticated'
|
||||
import SectionMain from '../../../components/SectionMain'
|
||||
import SectionTitleLineWithButton from '../../../components/SectionTitleLineWithButton'
|
||||
import CardBox from '../../../components/CardBox'
|
||||
import { useAppDispatch, useAppSelector } from '../../../stores/hooks'
|
||||
import { searchManga } from '../../../stores/mangaSlice'
|
||||
import BaseButton from '../../../components/BaseButton'
|
||||
import FormField from '../../../components/FormField'
|
||||
import { getPageTitle } from '../../../config'
|
||||
import Link from 'next/link'
|
||||
|
||||
const BrowseSourceIdPage = () => {
|
||||
const router = useRouter()
|
||||
const { id } = router.query
|
||||
const dispatch = useAppDispatch()
|
||||
const { searchResults, loading } = useAppSelector((state) => state.manga)
|
||||
const [query, setQuery] = useState('')
|
||||
|
||||
const handleSearch = (e?: React.FormEvent) => {
|
||||
if (e) e.preventDefault()
|
||||
if (id && typeof id === 'string') {
|
||||
dispatch(searchManga({ query, sourceId: id }))
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (id && typeof id === 'string' && searchResults.length === 0) {
|
||||
dispatch(searchManga({ query: '', sourceId: id }))
|
||||
}
|
||||
}, [id, dispatch])
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>{getPageTitle('Browse Source')}</title>
|
||||
</Head>
|
||||
|
||||
<SectionMain>
|
||||
<SectionTitleLineWithButton
|
||||
icon={mdiViewList}
|
||||
title={`Browse Source`}
|
||||
main
|
||||
>
|
||||
<Link href="/browse-sources" passHref legacyBehavior>
|
||||
<BaseButton icon={mdiArrowLeft} label="Back to Sources" color="whiteDark" />
|
||||
</Link>
|
||||
</SectionTitleLineWithButton>
|
||||
|
||||
<CardBox className="mb-6">
|
||||
<form onSubmit={handleSearch} className="flex gap-4">
|
||||
<div className="flex-grow">
|
||||
<FormField>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search manga in this source..."
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
className="w-full px-4 py-2 border rounded dark:bg-slate-800 dark:border-slate-700"
|
||||
/>
|
||||
</FormField>
|
||||
</div>
|
||||
<BaseButton
|
||||
type="submit"
|
||||
color="info"
|
||||
label="Search"
|
||||
icon={mdiMagnify}
|
||||
disabled={loading}
|
||||
/>
|
||||
</form>
|
||||
</CardBox>
|
||||
|
||||
{loading ? (
|
||||
<div className="text-center py-20">
|
||||
<div className="inline-block animate-spin rounded-full h-8 w-8 border-t-2 border-b-2 border-info-500"></div>
|
||||
<p className="mt-2 text-gray-500">Fetching manga...</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 gap-4">
|
||||
{searchResults.map((manga: any) => (
|
||||
<Link key={manga.id} href={`/manga/view/${id}/${manga.id}`} passHref legacyBehavior>
|
||||
<div className="cursor-pointer group">
|
||||
<div className="aspect-[3/4] overflow-hidden rounded-lg bg-gray-200 dark:bg-slate-800 relative">
|
||||
{manga.coverUrl ? (
|
||||
<img
|
||||
src={manga.coverUrl}
|
||||
alt={manga.title}
|
||||
className="h-full w-full object-cover transition-transform duration-300 group-hover:scale-110"
|
||||
/>
|
||||
) : (
|
||||
<div className="flex items-center justify-center h-full text-gray-400">
|
||||
No Cover
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<h3 className="mt-2 text-sm font-semibold line-clamp-2 dark:text-white">
|
||||
{manga.title}
|
||||
</h3>
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!loading && searchResults.length === 0 && (
|
||||
<div className="text-center py-20 text-gray-500">
|
||||
No results found. Try a different search query.
|
||||
</div>
|
||||
)}
|
||||
</SectionMain>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
BrowseSourceIdPage.getLayout = function getLayout(page: React.ReactElement) {
|
||||
return <LayoutAuthenticated>{page}</LayoutAuthenticated>
|
||||
}
|
||||
|
||||
export default BrowseSourceIdPage
|
||||
76
frontend/src/stores/mangaSlice.ts
Normal file
76
frontend/src/stores/mangaSlice.ts
Normal file
@ -0,0 +1,76 @@
|
||||
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'
|
||||
import axios from 'axios'
|
||||
|
||||
interface MangaState {
|
||||
searchResults: any[]
|
||||
mangaDetails: any | null
|
||||
chapters: any[]
|
||||
pages: any[]
|
||||
loading: boolean
|
||||
error: string | null
|
||||
}
|
||||
|
||||
const initialState: MangaState = {
|
||||
searchResults: [],
|
||||
mangaDetails: null,
|
||||
chapters: [],
|
||||
pages: [],
|
||||
loading: false,
|
||||
error: null,
|
||||
}
|
||||
|
||||
export const searchManga = createAsyncThunk('manga/search', async (data: { query: string, sourceId: string }) => {
|
||||
const response = await axios.get('/manga/search', { params: data });
|
||||
return response.data;
|
||||
})
|
||||
|
||||
export const fetchMangaDetails = createAsyncThunk('manga/fetchDetails', async (data: { mangaId: string, sourceId: string }) => {
|
||||
const response = await axios.get(`/manga/details/${data.sourceId}/${data.mangaId}`);
|
||||
return response.data;
|
||||
})
|
||||
|
||||
export const fetchChapters = createAsyncThunk('manga/fetchChapters', async (data: { mangaId: string, sourceId: string }) => {
|
||||
const response = await axios.get(`/manga/chapters/${data.sourceId}/${data.mangaId}`);
|
||||
return response.data;
|
||||
})
|
||||
|
||||
export const fetchPages = createAsyncThunk('manga/fetchPages', async (data: { chapterId: string, sourceId: string }) => {
|
||||
const response = await axios.get(`/manga/pages/${data.sourceId}/${data.chapterId}`);
|
||||
return response.data;
|
||||
})
|
||||
|
||||
export const mangaSlice = createSlice({
|
||||
name: 'manga',
|
||||
initialState,
|
||||
reducers: {
|
||||
clearReader: (state) => {
|
||||
state.pages = [];
|
||||
}
|
||||
},
|
||||
extraReducers: (builder) => {
|
||||
builder
|
||||
.addCase(searchManga.pending, (state) => { state.loading = true; })
|
||||
.addCase(searchManga.fulfilled, (state, action) => {
|
||||
state.loading = false;
|
||||
state.searchResults = action.payload;
|
||||
})
|
||||
.addCase(searchManga.rejected, (state, action) => {
|
||||
state.loading = false;
|
||||
state.error = action.error.message || 'Search failed';
|
||||
})
|
||||
.addCase(fetchMangaDetails.fulfilled, (state, action) => {
|
||||
state.mangaDetails = action.payload;
|
||||
})
|
||||
.addCase(fetchChapters.fulfilled, (state, action) => {
|
||||
state.chapters = action.payload;
|
||||
})
|
||||
.addCase(fetchPages.pending, (state) => { state.loading = true; })
|
||||
.addCase(fetchPages.fulfilled, (state, action) => {
|
||||
state.loading = false;
|
||||
state.pages = action.payload;
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
export const { clearReader } = mangaSlice.actions
|
||||
export default mangaSlice.reducer
|
||||
@ -3,6 +3,7 @@ import styleReducer from './styleSlice';
|
||||
import mainReducer from './mainSlice';
|
||||
import authSlice from './authSlice';
|
||||
import openAiSlice from './openAiSlice';
|
||||
import mangaReducer from './mangaSlice';
|
||||
|
||||
import usersSlice from "./users/usersSlice";
|
||||
import rolesSlice from "./roles/rolesSlice";
|
||||
@ -34,6 +35,7 @@ export const store = configureStore({
|
||||
main: mainReducer,
|
||||
auth: authSlice,
|
||||
openAi: openAiSlice,
|
||||
manga: mangaReducer,
|
||||
|
||||
users: usersSlice,
|
||||
roles: rolesSlice,
|
||||
@ -64,4 +66,4 @@ reading_progress: reading_progressSlice,
|
||||
// Infer the `RootState` and `AppDispatch` types from the store itself
|
||||
export type RootState = ReturnType<typeof store.getState>
|
||||
// Inferred type: {posts: PostsState, comments: CommentsState, users: UsersState}
|
||||
export type AppDispatch = typeof store.dispatch
|
||||
export type AppDispatch = typeof store.dispatch
|
||||
Loading…
x
Reference in New Issue
Block a user