Fix MangaCard/List

Style AddNotificationConnector
This commit is contained in:
2025-07-22 14:58:22 +02:00
parent 30ed3d8a2d
commit 7b038ad377
7 changed files with 122 additions and 67 deletions

View File

@@ -4,12 +4,12 @@ import Settings from "./Components/Settings/Settings.tsx";
import Header from "./Header.tsx"; import Header from "./Header.tsx";
import {createContext, useEffect, useState} from "react"; import {createContext, useEffect, useState} from "react";
import {V2} from "./apiClient/V2.ts"; import {V2} from "./apiClient/V2.ts";
import {GetManga, MangaContext } from './apiClient/MangaContext.tsx';
import { ApiContext } from './apiClient/ApiContext.tsx'; import { ApiContext } from './apiClient/ApiContext.tsx';
import MangaList from "./Components/Mangas/MangaList.tsx"; import MangaList from "./Components/Mangas/MangaList.tsx";
import {MangaConnector} from "./apiClient/data-contracts.ts"; import {Manga, MangaConnector} from "./apiClient/data-contracts.ts";
export const MangaConnectorContext = createContext<MangaConnector[]>([]); export const MangaConnectorContext = createContext<MangaConnector[]>([]);
export const MangaContext = createContext<Manga[]>([]);
export default function App () { export default function App () {
const apiUriStr = localStorage.getItem("apiUri") ?? window.location.href.substring(0, window.location.href.lastIndexOf("/")) + "/api"; const apiUriStr = localStorage.getItem("apiUri") ?? window.location.href.substring(0, window.location.href.lastIndexOf("/")) + "/api";
@@ -17,10 +17,24 @@ export default function App () {
const [Api, setApi] = useState<V2>(new V2()); const [Api, setApi] = useState<V2>(new V2());
const [mangaConnectors, setMangaConnectors] = useState<MangaConnector[]>([]); const [mangaConnectors, setMangaConnectors] = useState<MangaConnector[]>([]);
const [manga, setManga] = useState<Manga[]>([]);
useEffect(() => { useEffect(() => {
Api.mangaConnectorList().then(response => { Api.mangaConnectorList().then(response => {
if (response.ok) if (response.ok)
setMangaConnectors(response.data); setMangaConnectors(response.data);
});
Api.mangaList().then(response => {
if (!response.ok)
{
setManga([]);
return;
}
Api.mangaWithIDsCreate(response.data).then(response => {
if (response.ok)
setManga(response.data);
})
}) })
}, [Api]); }, [Api]);
@@ -35,7 +49,7 @@ export default function App () {
return ( return (
<ApiContext.Provider value={Api}> <ApiContext.Provider value={Api}>
<MangaConnectorContext.Provider value={mangaConnectors}> <MangaConnectorContext.Provider value={mangaConnectors}>
<MangaContext.Provider value={{GetManga: GetManga}}> <MangaContext.Provider value={manga}>
<Sheet className={"app"}> <Sheet className={"app"}>
<Header> <Header>
<Settings setApiUri={setApiUri} /> <Settings setApiUri={setApiUri} />

View File

@@ -4,10 +4,10 @@
} }
.manga-cover-blur { .manga-cover-blur {
background: linear-gradient(to bottom, rgba(0,0,0,0.8), rgba(0,0,0,0.2) 75%); background: linear-gradient(135deg, rgba(245, 169, 184, 0.9) 20%, rgba(91, 206, 250, 0.6));
box-shadow: 0 4px 30px rgba(0, 0, 0, 0.1); box-shadow: 0 4px 30px rgba(0, 0, 0, 0.1);)
backdrop-filter: blur(9px); backdrop-filter: blur(6px);
-webkit-backdrop-filter: blur(9px); -webkit-backdrop-filter: blur(6px);
} }
.manga-card-badge-icon { .manga-card-badge-icon {

View File

@@ -13,18 +13,16 @@ import {
} from "@mui/joy"; } from "@mui/joy";
import {Manga} from "../../apiClient/data-contracts.ts"; import {Manga} from "../../apiClient/data-contracts.ts";
import {Dispatch, SetStateAction, useContext, useState} from "react"; import {Dispatch, SetStateAction, useContext, useState} from "react";
import {MangaContext} from "../../apiClient/MangaContext.tsx";
import "./MangaCard.css"; import "./MangaCard.css";
import MangaConnectorBadge from "./MangaConnectorBadge.tsx"; import MangaConnectorBadge from "./MangaConnectorBadge.tsx";
import ModalClose from "@mui/joy/ModalClose"; import ModalClose from "@mui/joy/ModalClose";
import {ApiContext} from "../../apiClient/ApiContext.tsx"; import {ApiContext} from "../../apiClient/ApiContext.tsx";
import MarkdownPreview from '@uiw/react-markdown-preview'; import MarkdownPreview from '@uiw/react-markdown-preview';
import {MangaContext} from "../../App.tsx";
export function MangaCardFromId({mangaId} : {mangaId: string}) { export function MangaCardFromId({mangaId} : {mangaId: string}) {
const Mangas = useContext(MangaContext); const mangas = useContext(MangaContext);
const [manga, setManga] = useState<Manga | undefined>(undefined); const manga = mangas.find(manga => manga.key === mangaId);
Mangas.GetManga(mangaId).then(setManga);
return <MangaCard manga={manga} /> return <MangaCard manga={manga} />
} }

View File

@@ -1,23 +1,15 @@
import {useContext, useState} from "react"; import {useContext} from "react";
import {ApiContext} from "../../apiClient/ApiContext.tsx"; import {MangaCard} from "./MangaCard.tsx";
import {MangaCardFromId} from "./MangaCard.tsx";
import {Stack} from "@mui/joy"; import {Stack} from "@mui/joy";
import "./MangaList.css"; import "./MangaList.css";
import {MangaContext} from "../../App.tsx";
export default function MangaList (){ export default function MangaList (){
const Api = useContext(ApiContext); const mangas = useContext(MangaContext);
const [mangaIds, setMangaIds] = useState<string[]>();
Api.mangaList().then((response) => {
if (!response.ok)
return;
setMangaIds(response.data);
});
return ( return (
<Stack className={"manga-list"} direction={"row"} useFlexGap={true} spacing={2} flexWrap={"wrap"}> <Stack className={"manga-list"} direction={"row"} useFlexGap={true} spacing={2} flexWrap={"wrap"}>
{mangaIds?.map(id => <MangaCardFromId key={id} mangaId={id} />)} {mangas?.map(manga => <MangaCard key={manga.key} manga={manga} />)}
</Stack> </Stack>
); );

View File

@@ -1,8 +1,27 @@
import {ReactNode, useContext, useState} from "react"; import {ReactNode, useContext, useState} from "react";
import { ApiContext } from "../../apiClient/ApiContext"; import { ApiContext } from "../../apiClient/ApiContext";
import {Button, Input, Modal, ModalDialog, Tab, TabList, TabPanel, Tabs} from "@mui/joy"; import {
Button,
CircularProgress,
Input,
Modal,
ModalDialog,
Stack,
Tab,
TabList,
TabPanel,
Tabs
} from "@mui/joy";
import ModalClose from "@mui/joy/ModalClose"; import ModalClose from "@mui/joy/ModalClose";
import {GotifyRecord, NtfyRecord, PushoverRecord} from "../../apiClient/data-contracts.ts"; import {GotifyRecord, NtfyRecord, PushoverRecord} from "../../apiClient/data-contracts.ts";
import {Close, Done} from "@mui/icons-material";
enum LoadingState {
none,
loading,
success,
failure
}
export default function ({open, setOpen} : {open: boolean, setOpen: (open: boolean) => void}) { export default function ({open, setOpen} : {open: boolean, setOpen: (open: boolean) => void}) {
@@ -25,11 +44,38 @@ export default function ({open, setOpen} : {open: boolean, setOpen: (open: boole
); );
} }
function NotificationConnectorTab({ value, children, add }: { value: string, children: ReactNode, add: (data: any) => void }) { function NotificationConnectorTab({ value, children, add, state }: { value: string, children: ReactNode, add: (data: any) => void, state: LoadingState }) {
const StateIndicator = (state : LoadingState) : ReactNode => {
switch (state) {
case LoadingState.loading:
return (<CircularProgress />);
case LoadingState.failure:
return (<Close />);
case LoadingState.success:
return (<Done />);
default: return null;
}
}
// @ts-ignore
const StateColor = (state : LoadingState) => {
switch (state) {
case LoadingState.failure:
return "danger";
case LoadingState.success:
return "success";
default: return undefined;
}
}
const IsLoading = (state : LoadingState) : boolean => state === LoadingState.loading;
return ( return (
<TabPanel value={value}> <TabPanel value={value}>
{children} <Stack spacing={1}>
<Button onClick={add}>Add</Button> {children}
<Button onClick={add} endDecorator={StateIndicator(state)} loading={IsLoading(state)} disabled={IsLoading(state)} color={StateColor(state)}>Add</Button>
</Stack>
</TabPanel> </TabPanel>
); );
} }
@@ -37,9 +83,22 @@ function NotificationConnectorTab({ value, children, add }: { value: string, chi
function Gotify() { function Gotify() {
const Api = useContext(ApiContext); const Api = useContext(ApiContext);
const [gotifyData, setGotifyData] = useState<GotifyRecord>({}); const [gotifyData, setGotifyData] = useState<GotifyRecord>({});
const [loadingState, setLoadingState] = useState<LoadingState>(LoadingState.none);
const Add = () => {
setLoadingState(LoadingState.loading);
Api.notificationConnectorGotifyUpdate(gotifyData)
.then((response) => {
if (response.ok)
setLoadingState(LoadingState.success);
else
setLoadingState(LoadingState.failure);
})
.catch(_ => setLoadingState(LoadingState.failure));
}
return ( return (
<NotificationConnectorTab value={"gotify"} add={() => Api.notificationConnectorGotifyUpdate(gotifyData)}> <NotificationConnectorTab value={"gotify"} add={Add} state={loadingState}>
<Input placeholder={"Name"} value={gotifyData.name as string} onChange={(e) => setGotifyData({...gotifyData, name: e.target.value})} /> <Input placeholder={"Name"} value={gotifyData.name as string} onChange={(e) => setGotifyData({...gotifyData, name: e.target.value})} />
<Input placeholder={"https://[...]/message"} value={gotifyData.endpoint as string} onChange={(e) => setGotifyData({...gotifyData, endpoint: e.target.value})} /> <Input placeholder={"https://[...]/message"} value={gotifyData.endpoint as string} onChange={(e) => setGotifyData({...gotifyData, endpoint: e.target.value})} />
<Input placeholder={"Apptoken"} type={"password"} value={gotifyData.appToken as string} onChange={(e) => setGotifyData({...gotifyData, appToken: e.target.value})} /> <Input placeholder={"Apptoken"} type={"password"} value={gotifyData.appToken as string} onChange={(e) => setGotifyData({...gotifyData, appToken: e.target.value})} />
@@ -51,9 +110,22 @@ function Gotify() {
function Ntfy() { function Ntfy() {
const Api = useContext(ApiContext); const Api = useContext(ApiContext);
const [ntfyData, setNtfyData] = useState<NtfyRecord>({}); const [ntfyData, setNtfyData] = useState<NtfyRecord>({});
const [loadingState, setLoadingState] = useState<LoadingState>(LoadingState.none);
const Add = () => {
setLoadingState(LoadingState.loading);
Api.notificationConnectorNtfyUpdate(ntfyData)
.then((response) => {
if (response.ok)
setLoadingState(LoadingState.success);
else
setLoadingState(LoadingState.failure);
})
.catch(_ => setLoadingState(LoadingState.failure));
}
return ( return (
<NotificationConnectorTab value={"ntfy"} add={() => Api.notificationConnectorNtfyUpdate(ntfyData)}> <NotificationConnectorTab value={"ntfy"} add={Add} state={loadingState}>
<Input placeholder={"Name"} value={ntfyData.name as string} onChange={(e) => setNtfyData({...ntfyData, name: e.target.value})} /> <Input placeholder={"Name"} value={ntfyData.name as string} onChange={(e) => setNtfyData({...ntfyData, name: e.target.value})} />
<Input placeholder={"Endpoint"} value={ntfyData.endpoint as string} onChange={(e) => setNtfyData({...ntfyData, endpoint: e.target.value})} /> <Input placeholder={"Endpoint"} value={ntfyData.endpoint as string} onChange={(e) => setNtfyData({...ntfyData, endpoint: e.target.value})} />
<Input placeholder={"Topic"} value={ntfyData.topic as string} onChange={(e) => setNtfyData({...ntfyData, topic: e.target.value})} /> <Input placeholder={"Topic"} value={ntfyData.topic as string} onChange={(e) => setNtfyData({...ntfyData, topic: e.target.value})} />
@@ -67,9 +139,22 @@ function Ntfy() {
function Pushover() { function Pushover() {
const Api = useContext(ApiContext); const Api = useContext(ApiContext);
const [pushoverData, setPushoverData] = useState<PushoverRecord>({}); const [pushoverData, setPushoverData] = useState<PushoverRecord>({});
const [loadingState, setLoadingState] = useState<LoadingState>(LoadingState.none);
const Add = () => {
setLoadingState(LoadingState.loading);
Api.notificationConnectorPushoverUpdate(pushoverData)
.then((response) => {
if (response.ok)
setLoadingState(LoadingState.success);
else
setLoadingState(LoadingState.failure);
})
.catch(_ => setLoadingState(LoadingState.failure));
}
return ( return (
<NotificationConnectorTab value={"pushover"} add={() => Api.notificationConnectorPushoverUpdate(pushoverData)}> <NotificationConnectorTab value={"pushover"} add={Add} state={loadingState}>
<Input placeholder={"Name"} value={pushoverData.name as string} onChange={(e) => setPushoverData({...pushoverData, name: e.target.value})} /> <Input placeholder={"Name"} value={pushoverData.name as string} onChange={(e) => setPushoverData({...pushoverData, name: e.target.value})} />
<Input placeholder={"User"} value={pushoverData.user as string} onChange={(e) => setPushoverData({...pushoverData, user: e.target.value})} /> <Input placeholder={"User"} value={pushoverData.user as string} onChange={(e) => setPushoverData({...pushoverData, user: e.target.value})} />
<Input placeholder={"AppToken"} type={"password"} value={pushoverData.appToken as string} onChange={(e) => setPushoverData({...pushoverData, appToken: e.target.value})} /> <Input placeholder={"AppToken"} type={"password"} value={pushoverData.appToken as string} onChange={(e) => setPushoverData({...pushoverData, appToken: e.target.value})} />

View File

@@ -15,7 +15,7 @@ import {createContext, Dispatch, useContext, useEffect, useState} from "react";
import {Article} from '@mui/icons-material'; import {Article} from '@mui/icons-material';
import {TrangaSettings} from "../../apiClient/data-contracts.ts"; import {TrangaSettings} from "../../apiClient/data-contracts.ts";
import {ApiContext} from "../../apiClient/ApiContext.tsx"; import {ApiContext} from "../../apiClient/ApiContext.tsx";
import NotificationConnectors from "./NotificationConnectors.tsx"; import NotificationConnectors from "./AddNotificationConnector.tsx";
export const SettingsContext = createContext<TrangaSettings>({}); export const SettingsContext = createContext<TrangaSettings>({});

View File

@@ -1,34 +0,0 @@
import {createContext, useContext} from "react";
import {Manga} from "./data-contracts.ts";
import {ApiContext} from "./ApiContext.tsx";
const mangaPromises = new Map<string, Promise<Manga | undefined>>();
const mangas : Manga[] = [];
export const GetManga : (id: string) => Promise<Manga | undefined> = (id: string) => {
const API = useContext(ApiContext);
const promise = mangaPromises.get(id);
if(promise) return promise;
const p = new Promise<Manga | undefined>((resolve, reject) => {
let ret = mangas?.find(m => m.key == id);
if (ret) resolve(ret);
console.log(`Fetching manga ${id}`);
API.mangaDetail(id)
.then(result => {
if (!result.ok)
throw new Error(`Error fetching manga detail ${id}`);
mangas.push(result.data);
resolve(result.data);
}).catch(reject);
});
mangaPromises.set(id, p);
return p;
};
export const MangaContext = createContext<{ GetManga: (id: string) => Promise<Manga | undefined> }>(
{
GetManga: GetManga
}
);