Single Style for Manga

This commit is contained in:
2025-03-16 22:25:43 +01:00
parent d9bbbed1c0
commit 007b49c624
11 changed files with 234 additions and 335 deletions

View File

@ -4,7 +4,8 @@ import '../styles/monitorMangaList.css';
import {JobType} from "./interfaces/Jobs/IJob";
import '../styles/MangaCoverCard.css'
import DownloadAvailableChaptersJob from "./interfaces/Jobs/DownloadAvailableChaptersJob";
import {CoverCard} from "./interfaces/IManga";
import {MangaItem} from "./interfaces/IManga";
import Manga from "./Manga";
export default function MonitorJobsList({onStartSearch, onJobsChanged, connectedToBackend, apiUri, updateList} : {onStartSearch() : void, onJobsChanged: EventHandler<any>, connectedToBackend: boolean, apiUri: string, updateList: Date}) {
const [MonitoringJobs, setMonitoringJobs] = useState<DownloadAvailableChaptersJob[]>([]);
@ -35,10 +36,10 @@ export default function MonitorJobsList({onStartSearch, onJobsChanged, connected
}
function StartSearchMangaEntry() : ReactElement {
return (<div key="monitorMangaEntry.StartSearch" className="startSearchEntry Manga" onClick={onStartSearch}>
<img src="../media/blahaj.png" alt="Blahaj"></img>
return (<div key="monitorMangaEntry.StartSearch" className="startSearchEntry MangaItem" onClick={onStartSearch}>
<img className="MangaItem-Cover" src="../media/blahaj.png" alt="Blahaj"></img>
<div>
<p style={{textAlign: "center", width: "100%"}} className="Manga-name">Add new Manga</p>
<p style={{textAlign: "center", width: "100%"}} className="MangaItem-Name">Add new Manga</p>
<p style={{fontSize: "42pt", textAlign: "center"}}>+</p>
</div>
</div>);
@ -48,7 +49,12 @@ export default function MonitorJobsList({onStartSearch, onJobsChanged, connected
<div id="MonitorMangaList">
<StartSearchMangaEntry />
{MonitoringJobs.map((MonitoringJob) =>
<CoverCard apiUri={apiUri} mangaId={MonitoringJob.mangaId} key={MonitoringJob.mangaId} />
<MangaItem apiUri={apiUri} mangaId={MonitoringJob.mangaId} key={MonitoringJob.mangaId}>
<></>
<button className="Manga-DeleteButton" onClick={() => {
Manga.DeleteManga(apiUri, MonitoringJob.mangaId);
}}>Delete</button>
</MangaItem>
)}
</div>);
}

View File

@ -4,7 +4,8 @@ import '../styles/queuePopUp.css';
import '../styles/popup.css';
import Job from "./Job";
import DownloadSingleChapterJob from "./interfaces/Jobs/DownloadSingleChapterJob";
import { ItemDownloadSingleChapterJob } from "./interfaces/IManga";
import { MangaItem } from "./interfaces/IManga";
import {ChapterItem} from "./interfaces/IChapter";
export default function QueuePopUp({connectedToBackend, children, apiUri} : {connectedToBackend: boolean, children: JSX.Element[], apiUri: string}) {
@ -59,10 +60,16 @@ export default function QueuePopUp({connectedToBackend, children, apiUri} : {con
</div>
<div id="QueuePopUpBody" className="popupBody">
<div>
{RunningJobs.filter(j => j.jobType == JobType.DownloadSingleChapterJob).map(j => <ItemDownloadSingleChapterJob apiUri={apiUri} job={j as DownloadSingleChapterJob} key={j.jobId} />)}
{RunningJobs.filter(j => j.jobType == JobType.DownloadSingleChapterJob).map(j => {
let job = j as DownloadSingleChapterJob;
return <ChapterItem apiUri={apiUri} chapterId={job.chapterId} />
})}
</div>
<div>
{WaitingJobs.filter(j => j.jobType == JobType.DownloadSingleChapterJob).map(j => <ItemDownloadSingleChapterJob apiUri={apiUri} job={j as DownloadSingleChapterJob} key={j.jobId} />)}
{WaitingJobs.filter(j => j.jobType == JobType.DownloadSingleChapterJob).map(j =>{
let job = j as DownloadSingleChapterJob;
return <ChapterItem apiUri={apiUri} chapterId={job.chapterId} />
})}
</div>
</div>
</div>

View File

@ -2,9 +2,8 @@ import React, {ChangeEventHandler, EventHandler, useEffect, useState} from 'reac
import {MangaConnector} from "./MangaConnector";
import IMangaConnector from "./interfaces/IMangaConnector";
import {isValidUri} from "../App";
import IManga, {ExtendedInfo} from "./interfaces/IManga";
import IManga, {MangaItem} from "./interfaces/IManga";
import '../styles/search.css';
import '../styles/ExtendedInfo.css'
import SearchFunctions from "./SearchFunctions";
import Job from "./Job";
import ILocalLibrary from "./interfaces/ILocalLibrary";
@ -132,15 +131,15 @@ export default function Search({apiUri, jobInterval, onJobsChanged, closeSearch}
{searchResults === undefined
? <p></p>
: searchResults.map(result =>
<ExtendedInfo key={"Searchresult-"+result.mangaId} apiUri={apiUri} manga={result} actions={[
<MangaItem apiUri={apiUri} mangaId={result.mangaId}>
<select defaultValue={selectedLibrary === null ? "" : selectedLibrary.localLibraryId} onChange={selectedLibraryChanged}>
{selectedLibrary === null || libraries === null ? <option value="">Loading</option>
: libraries.map(library => <option key={library.localLibraryId} value={library.localLibraryId}>{library.libraryName} ({library.basePath})</option>)}
</select>,
</select>
<button className="Manga-AddButton" onClick={() => {
Job.CreateDownloadAvailableChaptersJob(apiUri, result.mangaId, {recurrenceTimeMs: jobInterval.getTime(), localLibraryId: selectedLibrary!.localLibraryId}).then(() => onJobsChanged(result.mangaId));
}}>Monitor</button>
]}/>
</MangaItem>
)
}
</div>

View File

@ -1,5 +1,5 @@
import IManga from "./interfaces/IManga";
import {getData, postData} from "../App";
import {postData} from "../App";
export default class SearchFunctions {

View File

@ -6,7 +6,10 @@ export default interface IAuthor {
authorName: string;
}
export function AuthorElement({apiUri, authorId} : {apiUri: string, authorId: string}) : ReactElement{
export function AuthorElement({apiUri, authorId} : {apiUri: string, authorId: string | null}) : ReactElement{
if(authorId === null)
return (<p className="Manga-Author-Name">Author</p>);
let [name, setName] = React.useState<string>(authorId);
useEffect(()=> {
@ -17,5 +20,5 @@ export function AuthorElement({apiUri, authorId} : {apiUri: string, authorId: st
});
}, [])
return (<span>{name}</span>);
return (<p className="Manga-Author-Name">{name}</p>);
}

View File

@ -1,10 +0,0 @@
export default interface IChapter{
chapterId: string;
volumeNumber: number;
chapterNumber: string;
url: string;
title: string | undefined;
archiveFileName: string;
downloaded: boolean;
parentMangaId: string;
}

View File

@ -0,0 +1,48 @@
import React, {ReactElement, ReactEventHandler, useEffect, useState} from "react";
import Manga from "../Manga";
import IManga from "./IManga";
import Chapter from "../Chapter";
export default interface IChapter{
chapterId: string;
volumeNumber: number;
chapterNumber: string;
url: string;
title: string | undefined;
archiveFileName: string;
downloaded: boolean;
parentMangaId: string;
}
export function ChapterItem({apiUri, chapterId} : {apiUri: string, chapterId: string}) : ReactElement {
const setCoverItem : ReactEventHandler<HTMLImageElement> = (e) => {
setMangaCoverHtmlImageItem(e.currentTarget);
}
let [chapter, setChapter] = useState<IChapter | null>(null);
let [manga, setManga] = useState<IManga | null>(null);
let [mangaCoverUrl, setMangaCoverUrl] = useState<string>("../../media/blahaj.png");
let [mangaCoverHtmlImageItem, setMangaCoverHtmlImageItem] = useState<HTMLImageElement | null>(null);
useEffect(() => {
Chapter.GetChapterFromId(apiUri, chapterId).then(setChapter);
}, []);
useEffect(() => {
if(chapter === null)
manga = null;
else
Manga.GetMangaById(apiUri, chapter.parentMangaId).then(setManga);
}, [chapter]);
useEffect(() => {
if(chapter != null && mangaCoverHtmlImageItem != null)
setMangaCoverUrl(Manga.GetMangaCoverImageUrl(apiUri, chapter.parentMangaId, mangaCoverHtmlImageItem));
}, [chapter, mangaCoverHtmlImageItem]);
return (<div className="ChapterItem" key={chapterId}>
<img className="ChapterItem-Cover" src={mangaCoverUrl} alt="Manga Cover" onLoad={setCoverItem} onResize={setCoverItem}></img>
<p className="ChapterItem-MangaName">{manga ? manga.name : "Manga-Name"}</p>
<p className="ChapterItem-ChapterName">{chapter ? chapter.title : "Chapter-Title"}</p>
<p className="ChapterItem-Volume">Vol.{chapter ? chapter.volumeNumber : "VolNum"}</p>
<p className="ChapterItem-Chapter">Ch.{chapter ? chapter.chapterNumber : "ChNum"}</p>
<a className="ChapterItem-Website" href={chapter ? chapter.url : "#"}><img src="../../media/link.svg" alt="Link"/></a>
</div>)
}

View File

@ -1,6 +1,5 @@
import React, {ReactElement, useEffect} from "react";
import {getData} from "../../App";
import IAuthor from "./IAuthor";
export default interface ILink {
linkId: string;
@ -8,7 +7,10 @@ export default interface ILink {
linkUrl: string;
}
export function LinkElement({apiUri, linkId} : {apiUri: string, linkId: string}) : ReactElement{
export function LinkElement({apiUri, linkId} : {apiUri: string, linkId: string | null}) : ReactElement{
if(linkId === null)
return (<a className="Manga-Link-Value" href="#">Link</a>);
let [provider, setProvider] = React.useState<string>(linkId);
let [linkUrl, setLinkUrl] = React.useState<string>("");
@ -21,5 +23,5 @@ export function LinkElement({apiUri, linkId} : {apiUri: string, linkId: string})
});
}, [])
return (<a href={linkUrl}>{provider}</a>);
return (<a className="Manga-Link-Value" href={linkUrl}>{provider}</a>);
}

View File

@ -1,17 +1,14 @@
import Manga from "../Manga";
import React, {ReactElement, ReactEventHandler, useEffect} from "react";
import React, {Children, ReactElement, ReactEventHandler, useEffect, useState} from "react";
import Icon from '@mdi/react';
import { mdiTagTextOutline, mdiAccountEdit, mdiLinkVariant } from '@mdi/js';
import MarkdownPreview from '@uiw/react-markdown-preview';
import {AuthorElement} from "./IAuthor";
import {LinkElement} from "./ILink";
import DownloadSingleChapterJob from "./Jobs/DownloadSingleChapterJob";
import IChapter from "./IChapter";
import Chapter from "../Chapter";
export default interface IManga{
mangaId: string;
connectorId: string;
idOnConnectorSite: string;
name: string;
description: string;
websiteUrl: string;
@ -35,128 +32,65 @@ export enum MangaReleaseStatus {
Unreleased = "Unreleased",
}
export const defaultManga: IManga = {
altTitleIds: [],
authorIds: [],
connectorId: "",
description: "",
folderName: "",
ignoreChapterBefore: 0,
linkIds: [],
mangaConnectorId: "",
name: "",
originalLanguage: "",
releaseStatus: MangaReleaseStatus.Unreleased,
tags: [],
websiteUrl: "",
year: 0,
mangaId: ""
}
export function CoverCard({apiUri, mangaId} : {apiUri: string, mangaId: string}) : ReactElement {
let [manga, setContent] = React.useState<IManga>(defaultManga);
let [extendedInfo, setExtendedInfo] = React.useState(false);
export function MangaItem({apiUri, mangaId, children} : {apiUri: string, mangaId: string, children?: (string | ReactElement)[]}) : ReactElement {
const LoadMangaCover : ReactEventHandler<HTMLImageElement> = (e) => {
if(e.currentTarget.src != Manga.GetMangaCoverImageUrl(apiUri, mangaId, e.currentTarget))
e.currentTarget.src = Manga.GetMangaCoverImageUrl(apiUri, mangaId, e.currentTarget);
}
let [manga, setManga] = useState<IManga | null>(null);
let [clicked, setClicked] = useState<boolean>(false);
useEffect(() => {
Manga.GetMangaById(apiUri, mangaId).then(setContent);
Manga.GetMangaById(apiUri, mangaId).then(setManga);
}, []);
const MangaCover : ReactEventHandler<HTMLImageElement> = (e) => {
if(e.currentTarget.src != Manga.GetMangaCoverImageUrl(apiUri, manga.mangaId, e.currentTarget))
e.currentTarget.src = Manga.GetMangaCoverImageUrl(apiUri, manga.mangaId, e.currentTarget);
}
return (
<div className="Manga" key={manga.mangaId} onClick={(e) => {
setExtendedInfo(!extendedInfo);
}}>
<img src={Manga.GetMangaCoverImageUrl(apiUri, manga.mangaId, undefined)} alt="Manga Cover" onLoad={MangaCover} onResize={MangaCover}></img>
<div className="SimpleCover">
<p className="pill connector-name">{manga.mangaConnectorId}</p>
<div className="Manga-status" release-status={manga.releaseStatus}></div>
<p className="Manga-name">{manga.name}</p>
</div>
{extendedInfo ? <div extended-info={extendedInfo ? "yes" : "no"}>
<ExtendedInfo apiUri={apiUri} manga={manga} actions={[
<button className="Manga-DeleteButton" onClick={() => {
Manga.DeleteManga(apiUri, manga.mangaId);
}}>Delete</button>
]} />
</div> : null}
</div>);
}
export function ExtendedInfo({apiUri, manga, actions} : {apiUri: string, manga: IManga, actions: ReactElement[]}) : ReactElement {
const MangaCover : ReactEventHandler<HTMLImageElement> = (e) => {
if(e.currentTarget.src != Manga.GetMangaCoverImageUrl(apiUri, manga.mangaId, e.currentTarget))
e.currentTarget.src = Manga.GetMangaCoverImageUrl(apiUri, manga.mangaId, e.currentTarget);
}
return(
<div className="SearchResult" key={manga.mangaId}>
<img src={Manga.GetMangaCoverImageUrl(apiUri, manga.mangaId, undefined)} alt="Manga Cover" onLoad={MangaCover} onResize={MangaCover}></img>
<p className="connector-name">{manga.mangaConnectorId}</p>
<div className="Manga-status" release-status={manga.releaseStatus}></div>
<p className="Manga-name"><a href={manga.websiteUrl}>{manga.name}<img src="../../media/link.svg"
alt=""/></a></p>
<div className="Manga-tags">
{manga.authorIds.map(authorId =>
<p className="Manga-author" key={manga.mangaId + "-author-" + authorId} >
<Icon path={mdiAccountEdit} size={0.5} />
<AuthorElement apiUri={apiUri} authorId={authorId}></AuthorElement>
</p>)}
{manga.tags.map(tag =>
<p className="Manga-tag" key={manga.mangaId + "-tag-" + tag}>
<Icon path={mdiTagTextOutline} size={0.5}/>
{tag}
</p>)}
{manga.linkIds.map(linkId =>
<p className="Manga-link" key={manga.mangaId + "-link-" + linkId}>
<Icon path={mdiLinkVariant} size={0.5}/>
<LinkElement apiUri={apiUri} linkId={linkId}></LinkElement>
</p>)}
</div>
<MarkdownPreview className="Manga-description" source={manga.description}
style={{backgroundColor: "transparent", color: "black"}}/>
<div className="Manga-actions">
{actions.map((p, i) => <div key={i}>{p}</div>)}
</div>
</div>);
}
export function ItemDownloadSingleChapterJob({apiUri, job} : {apiUri: string, job: DownloadSingleChapterJob}){
const MangaCover : ReactEventHandler<HTMLImageElement> = (e) => {
if(manga === null)
return;
if(e.currentTarget.src != Manga.GetMangaCoverImageUrl(apiUri, manga.mangaId, e.currentTarget))
e.currentTarget.src = Manga.GetMangaCoverImageUrl(apiUri, manga.mangaId, e.currentTarget);
}
let [chapter, setChapter] = React.useState<IChapter|null>(null);
let [manga, setManga] = React.useState<IManga|null>(null);
useEffect(() => {
Chapter.GetChapterFromId(apiUri, job.chapterId).then(setChapter);
}, []);
useEffect(() => {
if(chapter === null){
setManga(null);
return;
}
Manga.GetMangaById(apiUri, chapter.parentMangaId).then(setManga);
}, [chapter]);
return (
<div className="DownloadSingleChapterJob" key={"DownloadSingleChapterJob-" + job.jobId}>
<img src={manga ? Manga.GetMangaCoverImageUrl(apiUri, manga.mangaId, undefined) : ""} alt="Manga Cover" onLoad={MangaCover} onResize={MangaCover}></img>
<p className="DownloadSingleChapterJob-Name">{manga ? manga.name : job.chapterId}</p>
<p className="DownloadSingleChapterJob-Title">
{chapter ? "Vol." + chapter.volumeNumber + " Ch." + chapter.chapterNumber + ": " + chapter.title : "loading"}
<a href={chapter ? chapter.url : ""}>
<img src="../../media/link.svg" alt=""/>
</a>
</p>
return (<div className="MangaItem" key={mangaId} is-clicked={clicked ? "clicked" : "not-clicked"} onClick={()=>setClicked(!clicked)}>
<img className="MangaItem-Cover" src={Manga.GetMangaCoverImageUrl(apiUri, mangaId, undefined)} alt="Manga Cover" onLoad={LoadMangaCover} onResize={LoadMangaCover}></img>
<p className="MangaItem-Connector">{manga ? manga.mangaConnectorId : "Connector"}</p>
<p className="MangaItem-Status" release-status={manga?.releaseStatus}></p>
<p className="MangaItem-Name">{manga ? manga.name : "Name"}</p>
<a className="MangaItem-Website" href={manga ? manga.websiteUrl : "#"}><img src="../../media/link.svg" alt="Link"/></a>
<div className="MangaItem-Tags">
{manga ? manga.authorIds.map(authorId =>
<p className="MangaItem-Author" key={authorId} >
<Icon path={mdiAccountEdit} size={0.5} />
<AuthorElement apiUri={apiUri} authorId={authorId}></AuthorElement>
</p>)
:
<p className="MangaItem-Author">
<Icon path={mdiAccountEdit} size={0.5} />
<AuthorElement apiUri={apiUri} authorId={null}></AuthorElement>
</p>}
{manga ? manga.tags.map(tag =>
<p className="MangaItem-Tag" key={tag}>
<Icon path={mdiTagTextOutline} size={0.5}/>
<p className="MangaItem-Tag-Value">{tag}</p>
</p>)
:
<p className="MangaItem-Tag">
<Icon path={mdiTagTextOutline} size={0.5}/>
<p className="MangaItem-Tag-Value">Tag</p>
</p>
}
{manga ? manga.linkIds.map(linkId =>
<p className="MangaItem-Link" key={linkId}>
<Icon path={mdiLinkVariant} size={0.5}/>
<LinkElement apiUri={apiUri} linkId={linkId} />
</p>)
:
<p className="MangaItem-Link">
<Icon path={mdiLinkVariant} size={0.5}/>
<LinkElement apiUri={apiUri} linkId={null} />
</p>}
</div>
);
<MarkdownPreview className="MangaItem-Description" source={manga ? manga.description : "# Description"} />
<div className="MangaItem-Props">
{children ? children.map(c => {
if(c instanceof Element)
return c as ReactElement;
else
return <span>{c}</span>
}) : null}
</div>
</div>)
}