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

View File

@@ -35,6 +35,6 @@ export const TColor = (state: TState): ColorPaletteProp => {
export default interface TProps { export default interface TProps {
disabled?: boolean disabled?: boolean
completionAction?: ( completionAction?: (
value: string | number | readonly string[] | undefined value?: string | number | readonly string[]
) => Promise<void> ) => 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'} /> <img src={manga ? `${Api.baseUrl}/v2/Manga/${manga.key}/Cover` : '/blahaj.png'} />
</CardCover> </CardCover>
</Card> </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'}> <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?.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> )} {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> </Stack>
<MarkdownPreview source={manga?.description} style={{backgroundColor: "transparent", color: theme.palette.text.primary, overflowY: "auto"}}/> <MarkdownPreview source={manga?.description} style={{backgroundColor: "transparent", color: theme.palette.text.primary, overflowY: "auto"}}/>
</Stack> </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} {props.actions}
</Stack> </Stack>
</div> </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 {Dispatch, ReactNode, useContext, useEffect, useState} from 'react'
import { import {
Button,
List, List,
ListItem, ListItem,
ListItemDecorator, ListItemDecorator,
@@ -18,6 +19,7 @@ import { ApiContext } from './contexts/ApiContext.tsx'
import { MangaCardList } from './Components/Mangas/MangaList.tsx' import { MangaCardList } from './Components/Mangas/MangaList.tsx'
import {MangaConnector, MinimalManga} from './api/data-contracts.ts' import {MangaConnector, MinimalManga} from './api/data-contracts.ts'
import MangaDetail from "./MangaDetail.tsx"; import MangaDetail from "./MangaDetail.tsx";
import MangaDownloadDrawer from "./MangaDownloadDrawer.tsx";
export function Search(props: SearchModalProps): ReactNode { export function Search(props: SearchModalProps): ReactNode {
const Api = useContext(ApiContext) const Api = useContext(ApiContext)
@@ -64,11 +66,17 @@ export function Search(props: SearchModalProps): ReactNode {
const [selectedManga, setSelectedManga] = useState<MinimalManga | undefined>(undefined); const [selectedManga, setSelectedManga] = useState<MinimalManga | undefined>(undefined);
const [mangaDetailOpen, setMangaDetailOpen] = useState(false); const [mangaDetailOpen, setMangaDetailOpen] = useState(false);
const [mangaDownloadDrawerOpen, setMangaDownloadDrawerOpen] = useState(false);
function openMangaDetail(manga: MinimalManga) { function openMangaDetail(manga: MinimalManga) {
setSelectedManga(manga); setSelectedManga(manga);
setMangaDetailOpen(true); setMangaDetailOpen(true);
} }
function openMangaDownloadDrawer() {
setMangaDetailOpen(false);
setMangaDownloadDrawerOpen(true);
}
return ( return (
<Modal open={props.open} onClose={() => props.setOpen(false)}> <Modal open={props.open} onClose={() => props.setOpen(false)}>
@@ -120,7 +128,10 @@ export function Search(props: SearchModalProps): ReactNode {
</Step> </Step>
</Stepper> </Stepper>
<MangaCardList manga={searchResults} mangaOnClick={openMangaDetail}/> <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> </ModalDialog>
</Modal> </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>
);
}