Download Dialog

This commit is contained in:
2025-09-04 21:46:39 +02:00
parent 75f66c791d
commit 81bde5c099
6 changed files with 148 additions and 14 deletions

View File

@@ -8,6 +8,7 @@ import MangaProvider from './contexts/MangaContext.tsx'
import MangaList from './Components/Mangas/MangaList.tsx'
import {Search} from './Search.tsx'
import MangaConnectorProvider from './contexts/MangaConnectorContext.tsx'
import LibraryProvider from "./contexts/FileLibraryContext.tsx";
export default function App() {
const [apiUri, setApiUri] = useState<string>(
@@ -27,17 +28,19 @@ export default function App() {
return (
<ApiProvider apiConfig={apiConfig}>
<MangaConnectorProvider>
<MangaProvider>
<Sheet className={'app'}>
<Header>
<Settings setApiUri={setApiUri} />
</Header>
<Sheet className={'app-content'}>
<MangaList openSearch={() => setSearchOpen(true)} />
<Search open={searchOpen} setOpen={setSearchOpen} />
<LibraryProvider>
<MangaProvider>
<Sheet className={'app'}>
<Header>
<Settings setApiUri={setApiUri} />
</Header>
<Sheet className={'app-content'}>
<MangaList openSearch={() => setSearchOpen(true)} />
<Search open={searchOpen} setOpen={setSearchOpen} />
</Sheet>
</Sheet>
</Sheet>
</MangaProvider>
</MangaProvider>
</LibraryProvider>
</MangaConnectorProvider>
</ApiProvider>
)

View File

@@ -35,6 +35,6 @@ export const TColor = (state: TState): ColorPaletteProp => {
export default interface TProps {
disabled?: boolean
completionAction?: (
value: string | number | readonly string[] | undefined
value?: string | number | readonly string[]
) => Promise<void>
}

View File

@@ -33,7 +33,7 @@ export default function MangaDetail (props: MangaDetailProps) : ReactNode {
<img src={manga ? `${Api.baseUrl}/v2/Manga/${manga.key}/Cover` : '/blahaj.png'} />
</CardCover>
</Card>
<Stack direction={'column'} gap={2} sx={{maxWidth: 'calc(100% - 230px)', margin: '0 5px'}}>
<Stack direction={'column'} gap={2} sx={{maxWidth: 'calc(100% - 230px)', margin: '5px'}}>
<Stack direction={'row'} gap={0.5} flexWrap={'wrap'}>
{manga?.tags.map(tag => <Chip key={tag} size={"sm"} sx={{backgroundColor: theme.palette.primary.plainColor}}>{tag}</Chip>)}
{manga?.authors.map(author => <Chip key={author.key} size={'sm'} sx={{backgroundColor: theme.palette.success.plainColor}}>{author.name}</Chip> )}
@@ -41,7 +41,7 @@ export default function MangaDetail (props: MangaDetailProps) : ReactNode {
</Stack>
<MarkdownPreview source={manga?.description} style={{backgroundColor: "transparent", color: theme.palette.text.primary, overflowY: "auto"}}/>
</Stack>
<Stack sx={{width: '100%'}} flexWrap={'nowrap'} gap={1}>
<Stack sx={{flexGrow: 1, flexBasis: 0, margin: '5px 0', alignItems: 'flex-end'}} flexWrap={'nowrap'} gap={1}>
{props.actions}
</Stack>
</div>

View File

@@ -0,0 +1,91 @@
import {Dispatch, ReactNode, useContext, useEffect, useState} from "react";
import {Box, Card, Checkbox, Drawer, List, ListItem, Option, Select, Stack, Typography} from "@mui/joy";
import ModalClose from "@mui/joy/ModalClose";
import {Manga, MangaConnectorId} from "./api/data-contracts.ts";
import {ApiContext} from "./contexts/ApiContext.tsx";
import {MangaContext} from "./contexts/MangaContext.tsx";
import {FileLibraryContext} from "./contexts/FileLibraryContext.tsx";
import MangaConnectorIcon from "./Components/Mangas/MangaConnectorIcon.tsx";
import TButton from "./Components/Inputs/TButton.tsx";
export default function MangaDownloadDrawer (props: MangaDownloadDrawerProps) : ReactNode{
const Api = useContext(ApiContext);
const Manga = useContext(MangaContext);
const Libraries = useContext(FileLibraryContext);
const [manga, setManga] = useState<Manga | undefined>(props.manga)
const [downloadFromMap, setDownloadFromMap] = useState<Map<MangaConnectorId, boolean>>(new Map())
useEffect(() => {
if (!props.open) return;
if (!props.mangaKey) return;
if (props.manga != undefined) return;
Manga.GetManga(props.mangaKey).then(setManga);
}, [Api, props]);
useEffect(() => {
const newMap = new Map();
manga?.mangaConnectorIds.forEach(id => {
newMap.set(id, id.useForDownload);
})
setDownloadFromMap(newMap);
}, [manga]);
const setDownload = () : Promise<void> => {
if (!manga) return Promise.reject();
downloadFromMap.forEach(async (download, id) => {
const result = await Api.mangaSetAsDownloadFromCreate(manga?.key, id.mangaConnectorName, download);
if (!result.ok)
return Promise.reject();
});
return Promise.resolve();
}
return (
<Drawer open={props.open}
onClose={() => props.setOpen(false)}
anchor="left"
size="md">
<Card sx={{flexGrow: 1, margin: '10px'}}>
<ModalClose />
<Typography level={"h3"}>Download</Typography>
<Typography level={"h4"}>{manga?.name}</Typography>
<Stack direction={'column'} gap={2} sx={{flexBasis: 0}}>
<Box>
<Typography>Select a Library to Download to:</Typography>
<Select placeholder={"Select a Library"}>
{Libraries.map(l => <Option key={l.key} value={l.key}>{l.libraryName} ({l.basePath})</Option>)}
</Select>
</Box>
<Box>
<Typography>Select which connectors you want to download this Manga from:</Typography>
<List>
{manga?.mangaConnectorIds.map(id => (
<ListItem key={id.key}>
<Checkbox
defaultChecked={id.useForDownload}
onChange={(c) => downloadFromMap.set(id, c.target.checked)}
label={
<div style={{display: 'flex', alignItems: 'center', gap: 5}}>
<MangaConnectorIcon mangaConnectorName={id.mangaConnectorName} />
<Typography>{id.mangaConnectorName}</Typography>
</div>
}
/>
</ListItem>
))}
</List>
</Box>
<TButton completionAction={setDownload}>Download All</TButton>
</Stack>
</Card>
</Drawer>
)
}
export interface MangaDownloadDrawerProps {
manga?: Manga;
mangaKey?: string;
open: boolean;
setOpen: Dispatch<boolean>;
}

View File

@@ -1,5 +1,6 @@
import {Dispatch, ReactNode, useContext, useEffect, useState} from 'react'
import {
Button,
List,
ListItem,
ListItemDecorator,
@@ -18,6 +19,7 @@ import { ApiContext } from './contexts/ApiContext.tsx'
import { MangaCardList } from './Components/Mangas/MangaList.tsx'
import {MangaConnector, MinimalManga} from './api/data-contracts.ts'
import MangaDetail from "./MangaDetail.tsx";
import MangaDownloadDrawer from "./MangaDownloadDrawer.tsx";
export function Search(props: SearchModalProps): ReactNode {
const Api = useContext(ApiContext)
@@ -64,12 +66,18 @@ export function Search(props: SearchModalProps): ReactNode {
const [selectedManga, setSelectedManga] = useState<MinimalManga | undefined>(undefined);
const [mangaDetailOpen, setMangaDetailOpen] = useState(false);
const [mangaDownloadDrawerOpen, setMangaDownloadDrawerOpen] = useState(false);
function openMangaDetail(manga: MinimalManga) {
setSelectedManga(manga);
setMangaDetailOpen(true);
}
function openMangaDownloadDrawer() {
setMangaDetailOpen(false);
setMangaDownloadDrawerOpen(true);
}
return (
<Modal open={props.open} onClose={() => props.setOpen(false)}>
<ModalDialog sx={{width: '90vw'}}>
@@ -120,7 +128,10 @@ export function Search(props: SearchModalProps): ReactNode {
</Step>
</Stepper>
<MangaCardList manga={searchResults} mangaOnClick={openMangaDetail}/>
<MangaDetail mangaKey={selectedManga?.key} open={mangaDetailOpen} setOpen={setMangaDetailOpen} />
<MangaDetail mangaKey={selectedManga?.key} open={mangaDetailOpen} setOpen={setMangaDetailOpen} actions={[
<Button onClick={openMangaDownloadDrawer}>Download</Button>
]} />
<MangaDownloadDrawer open={mangaDownloadDrawerOpen} setOpen={setMangaDownloadDrawerOpen} mangaKey={selectedManga?.key} />
</ModalDialog>
</Modal>
)

View File

@@ -0,0 +1,29 @@
import {
createContext,
ReactNode,
useContext,
useEffect,
useState,
} from 'react'
import {FileLibrary} from '../api/data-contracts.ts'
import { ApiContext } from './ApiContext.tsx'
export const FileLibraryContext = createContext<FileLibrary[]>([]);
export default function LibraryProvider({ children }: { children: ReactNode }) : ReactNode {
const Api = useContext(ApiContext)
const [state, setState] = useState<FileLibrary[]>([])
useEffect(() => {
Api.fileLibraryList().then((result) => {
if (result.ok) {
setState(result.data)
}
})
}, [Api])
return (
<FileLibraryContext value={state}>{children}</FileLibraryContext>
);
}