lint and prettier

This commit is contained in:
2025-09-04 21:48:05 +02:00
parent 81bde5c099
commit 19920fb507
10 changed files with 329 additions and 161 deletions

View File

@@ -8,7 +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";
import LibraryProvider from './contexts/FileLibraryContext.tsx'
export default function App() {
const [apiUri, setApiUri] = useState<string>(
@@ -35,8 +35,13 @@ export default function App() {
<Settings setApiUri={setApiUri} />
</Header>
<Sheet className={'app-content'}>
<MangaList openSearch={() => setSearchOpen(true)} />
<Search open={searchOpen} setOpen={setSearchOpen} />
<MangaList
openSearch={() => setSearchOpen(true)}
/>
<Search
open={searchOpen}
setOpen={setSearchOpen}
/>
</Sheet>
</Sheet>
</MangaProvider>

View File

@@ -1,10 +1,13 @@
@keyframes spin {
from {transform: translate(-50%, -50%) rotate(0);}
to {transform: translate(-50%, -50%) rotate(360deg);}
from {
transform: translate(-50%, -50%) rotate(0);
}
to {
transform: translate(-50%, -50%) rotate(360deg);
}
}
.t-loadable[aria-disabled="true"] {
.t-loadable[aria-disabled='true'] {
--border-radius: 5px;
--border-size: 2px;
--border-bg: conic-gradient(red, yellow, lime, aqua, blue, magenta, red);
@@ -42,10 +45,10 @@
}
}
.t-loadable[aria-disabled="true"]:has(input)::after {
.t-loadable[aria-disabled='true']:has(input)::after {
background: black;
}
.t-loadable[aria-disabled="true"]:is(button) {
.t-loadable[aria-disabled='true']:is(button) {
color: var(--joy-palette-text-primary);
}

View File

@@ -13,7 +13,7 @@
.manga-card-cover-blur {
background: linear-gradient(
150deg,
rgba(245, 169, 184, 0.80) 50%,
rgba(245, 169, 184, 0.8) 50%,
rgba(91, 206, 250, 0.3)
);
box-shadow: 0 4px 30px rgba(0, 0, 0, 0.1);

View File

@@ -13,18 +13,26 @@ import { Manga, MinimalManga } from '../../api/data-contracts.ts'
import { ApiContext } from '../../contexts/ApiContext.tsx'
export default function MangaCard(props: MangaCardProps): ReactNode {
const Api = useContext(ApiContext);
const Api = useContext(ApiContext)
return (
<Badge
badgeContent={props.manga?.mangaConnectorIds.map((id) => (
<MangaConnectorIcon mangaConnectorName={id.mangaConnectorName} />
<MangaConnectorIcon
mangaConnectorName={id.mangaConnectorName}
/>
))}
className={'manga-card-badge'}
>
<Card className={'manga-card'} onClick={props.onClick}>
<CardCover className={'manga-card-cover'}>
<img src={props.manga && props.manga.key != "Search" ? `${Api.baseUrl}/v2/Manga/${props.manga?.key}/Cover` : '/blahaj.png'} />
<img
src={
props.manga && props.manga.key != 'Search'
? `${Api.baseUrl}/v2/Manga/${props.manga?.key}/Cover`
: '/blahaj.png'
}
/>
</CardCover>
<CardCover className={'manga-card-cover-blur'} />
<CardContent className={'manga-card-content'}>

View File

@@ -14,20 +14,20 @@ export default function MangaConnectorIcon({
const [connector, setConnector] = useState<MangaConnector | undefined>(
mangaConnector
);
)
useEffect(() => {
if (mangaConnector) {
setConnector(mangaConnector)
return;
return
}
if (!mangaConnectorName) return;
if (!mangaConnectorName) return
Api.mangaConnectorDetail(mangaConnectorName).then((result) => {
if (result.ok) {
setConnector(result.data)
}
});
}, [Api, mangaConnectorName, mangaConnector]);
})
}, [Api, mangaConnectorName, mangaConnector])
return (
<Tooltip title={connector?.name ?? 'loading'}>

View File

@@ -56,7 +56,13 @@ export function MangaCardList(props: MangaCardListProps): ReactNode {
>
{props.children}
{props.manga.map((m) => (
<MangaCard key={m.key} manga={m} onClick={() => { if(props.mangaOnClick) props.mangaOnClick(m); } } />
<MangaCard
key={m.key}
manga={m}
onClick={() => {
if (props.mangaOnClick) props.mangaOnClick(m)
}}
/>
))}
</Stack>
)

View File

@@ -1,59 +1,135 @@
import {Manga} from "./api/data-contracts.ts";
import {Dispatch, ReactNode, useContext, useEffect, useState} from "react";
import {Card, CardCover, Chip, Modal, ModalDialog, Stack, Typography, useTheme} from "@mui/joy";
import ModalClose from "@mui/joy/ModalClose";
import {ApiContext} from "./contexts/ApiContext.tsx";
import {MangaContext} from "./contexts/MangaContext.tsx";
import { Manga } from './api/data-contracts.ts'
import { Dispatch, ReactNode, useContext, useEffect, useState } from 'react'
import {
Card,
CardCover,
Chip,
Modal,
ModalDialog,
Stack,
Typography,
useTheme,
} from '@mui/joy'
import ModalClose from '@mui/joy/ModalClose'
import { ApiContext } from './contexts/ApiContext.tsx'
import { MangaContext } from './contexts/MangaContext.tsx'
import './Components/Mangas/MangaCard.css'
import MarkdownPreview from "@uiw/react-markdown-preview";
import MarkdownPreview from '@uiw/react-markdown-preview'
export default function MangaDetail(props: MangaDetailProps): ReactNode {
const Api = useContext(ApiContext);
const Manga = useContext(MangaContext);
const Api = useContext(ApiContext)
const Manga = useContext(MangaContext)
const [manga, setManga] = useState<Manga | undefined>(props.manga)
useEffect(() => {
if (!props.open) return;
if (!props.mangaKey) return;
if (props.manga != undefined) return;
Manga.GetManga(props.mangaKey).then(setManga);
}, [Api, props]);
if (!props.open) return
if (!props.mangaKey) return
if (props.manga != undefined) return
Manga.GetManga(props.mangaKey).then(setManga)
}, [Api, Manga, props])
const theme = useTheme();
const theme = useTheme()
return (
<Modal open={props.open} onClose={() => props.setOpen(false)}>
<ModalDialog>
<ModalClose />
<div style={{display: 'flex', flexWrap: 'wrap', flexDirection: 'row'}}>
<Typography level={"h3"} sx={{width: '100%'}}>{manga?.name}</Typography>
<div
style={{
display: 'flex',
flexWrap: 'wrap',
flexDirection: 'row',
}}
>
<Typography level={'h3'} sx={{ width: '100%' }}>
{manga?.name}
</Typography>
<Card className={'manga-card'}>
<CardCover className={'manga-card-cover'}>
<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>
</Card>
<Stack direction={'column'} gap={2} sx={{maxWidth: 'calc(100% - 230px)', margin: '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> )}
{manga?.links.map(link => <Chip key={link.provider} size={"sm"} sx={{backgroundColor: theme.palette.neutral.plainColor}}><a href={link.url}>{link.provider}</a></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?.links.map((link) => (
<Chip
key={link.provider}
size={'sm'}
sx={{
backgroundColor:
theme.palette.neutral.plainColor,
}}
>
<a href={link.url}>{link.provider}</a>
</Chip>
))}
</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 sx={{flexGrow: 1, flexBasis: 0, margin: '5px 0', alignItems: 'flex-end'}} flexWrap={'nowrap'} gap={1}>
<Stack
sx={{
flexGrow: 1,
flexBasis: 0,
margin: '5px 0',
alignItems: 'flex-end',
}}
flexWrap={'nowrap'}
gap={1}
>
{props.actions}
</Stack>
</div>
</ModalDialog>
</Modal>
);
)
}
export interface MangaDetailProps {
manga?: Manga;
mangaKey?: string;
open: boolean;
setOpen: Dispatch<boolean>;
actions?: ReactNode[];
manga?: Manga
mangaKey?: string
open: boolean
setOpen: Dispatch<boolean>
actions?: ReactNode[]
}

View File

@@ -1,74 +1,120 @@
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";
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);
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())
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]);
if (!props.open) return
if (!props.mangaKey) return
if (props.manga != undefined) return
Manga.GetManga(props.mangaKey).then(setManga)
}, [Api, Manga, props])
useEffect(() => {
const newMap = new Map();
manga?.mangaConnectorIds.forEach(id => {
newMap.set(id, id.useForDownload);
const newMap = new Map()
manga?.mangaConnectorIds.forEach((id) => {
newMap.set(id, id.useForDownload)
})
setDownloadFromMap(newMap);
}, [manga]);
setDownloadFromMap(newMap)
}, [manga])
const setDownload = (): Promise<void> => {
if (!manga) return Promise.reject();
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();
const result = await Api.mangaSetAsDownloadFromCreate(
manga?.key,
id.mangaConnectorName,
download
)
if (!result.ok) return Promise.reject()
})
return Promise.resolve()
}
return (
<Drawer open={props.open}
<Drawer
open={props.open}
onClose={() => props.setOpen(false)}
anchor="left"
size="md">
size="md"
>
<Card sx={{ flexGrow: 1, margin: '10px' }}>
<ModalClose />
<Typography level={"h3"}>Download</Typography>
<Typography level={"h4"}>{manga?.name}</Typography>
<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>)}
<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>
<Typography>
Select which connectors you want to download this
Manga from:
</Typography>
<List>
{manga?.mangaConnectorIds.map(id => (
{manga?.mangaConnectorIds.map((id) => (
<ListItem key={id.key}>
<Checkbox
defaultChecked={id.useForDownload}
onChange={(c) => downloadFromMap.set(id, c.target.checked)}
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
style={{
display: 'flex',
alignItems: 'center',
gap: 5,
}}
>
<MangaConnectorIcon
mangaConnectorName={
id.mangaConnectorName
}
/>
<Typography>
{id.mangaConnectorName}
</Typography>
</div>
}
/>
@@ -76,7 +122,9 @@ export default function MangaDownloadDrawer (props: MangaDownloadDrawerProps) :
))}
</List>
</Box>
<TButton completionAction={setDownload}>Download All</TButton>
<TButton completionAction={setDownload}>
Download All
</TButton>
</Stack>
</Card>
</Drawer>
@@ -84,8 +132,8 @@ export default function MangaDownloadDrawer (props: MangaDownloadDrawerProps) :
}
export interface MangaDownloadDrawerProps {
manga?: Manga;
mangaKey?: string;
open: boolean;
setOpen: Dispatch<boolean>;
manga?: Manga
mangaKey?: string
open: boolean
setOpen: Dispatch<boolean>
}

View File

@@ -18,8 +18,8 @@ import TInput from './Components/Inputs/TInput.tsx'
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";
import MangaDetail from './MangaDetail.tsx'
import MangaDownloadDrawer from './MangaDownloadDrawer.tsx'
export function Search(props: SearchModalProps): ReactNode {
const Api = useContext(ApiContext)
@@ -27,10 +27,10 @@ export function Search(props: SearchModalProps): ReactNode {
useEffect(() => {
if (props.open) {
setSelectedConnector(undefined);
setSearchResults([]);
setSelectedConnector(undefined)
setSearchResults([])
}
}, [open]);
}, [props])
const [selectedConnector, setSelectedConnector] = useState<MangaConnector>()
const [searchResults, setSearchResults] = useState<MinimalManga[]>([])
@@ -42,40 +42,46 @@ export function Search(props: SearchModalProps): ReactNode {
setSearchResults([])
if (isUrl(value)) {
try {
let result = await Api.searchUrlCreate(value);
const result = await Api.searchUrlCreate(value)
if (result.ok) {
setSearchResults([result.data])
return Promise.resolve()
} else return Promise.reject()
} catch (reason) {
return await Promise.reject(reason);
return await Promise.reject(reason)
}
} else {
if (!selectedConnector) return Promise.reject()
try {
let result2 = await Api.searchDetail(selectedConnector?.key, value);
const result2 = await Api.searchDetail(
selectedConnector?.key,
value
)
if (result2.ok) {
setSearchResults(result2.data)
return Promise.resolve()
} else return Promise.reject()
} catch (reason1) {
return await Promise.reject(reason1);
return await Promise.reject(reason1)
}
}
}
const [selectedManga, setSelectedManga] = useState<MinimalManga | undefined>(undefined);
const [mangaDetailOpen, setMangaDetailOpen] = useState(false);
const [mangaDownloadDrawerOpen, setMangaDownloadDrawerOpen] = useState(false);
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);
setSelectedManga(manga)
setMangaDetailOpen(true)
}
function openMangaDownloadDrawer() {
setMangaDetailOpen(false);
setMangaDownloadDrawerOpen(true);
setMangaDetailOpen(false)
setMangaDownloadDrawerOpen(true)
}
return (
@@ -127,19 +133,33 @@ export function Search(props: SearchModalProps): ReactNode {
/>
</Step>
</Stepper>
<MangaCardList manga={searchResults} mangaOnClick={openMangaDetail}/>
<MangaDetail mangaKey={selectedManga?.key} open={mangaDetailOpen} setOpen={setMangaDetailOpen} actions={[
<Button onClick={openMangaDownloadDrawer}>Download</Button>
]} />
<MangaDownloadDrawer open={mangaDownloadDrawerOpen} setOpen={setMangaDownloadDrawerOpen} mangaKey={selectedManga?.key} />
<MangaCardList
manga={searchResults}
mangaOnClick={openMangaDetail}
/>
<MangaDetail
mangaKey={selectedManga?.key}
open={mangaDetailOpen}
setOpen={setMangaDetailOpen}
actions={[
<Button onClick={openMangaDownloadDrawer}>
Download
</Button>,
]}
/>
<MangaDownloadDrawer
open={mangaDownloadDrawerOpen}
setOpen={setMangaDownloadDrawerOpen}
mangaKey={selectedManga?.key}
/>
</ModalDialog>
</Modal>
)
}
export interface SearchModalProps {
open: boolean;
setOpen: Dispatch<boolean>;
open: boolean
setOpen: Dispatch<boolean>
}
function isUrl(str: string): boolean {

View File

@@ -8,9 +8,13 @@ import {
import { FileLibrary } from '../api/data-contracts.ts'
import { ApiContext } from './ApiContext.tsx'
export const FileLibraryContext = createContext<FileLibrary[]>([]);
export const FileLibraryContext = createContext<FileLibrary[]>([])
export default function LibraryProvider({ children }: { children: ReactNode }) : ReactNode {
export default function LibraryProvider({
children,
}: {
children: ReactNode
}): ReactNode {
const Api = useContext(ApiContext)
const [state, setState] = useState<FileLibrary[]>([])
@@ -23,7 +27,5 @@ export default function LibraryProvider({ children }: { children: ReactNode }) :
})
}, [Api])
return (
<FileLibraryContext value={state}>{children}</FileLibraryContext>
);
return <FileLibraryContext value={state}>{children}</FileLibraryContext>
}