mirror of
https://github.com/C9Glax/tranga-website.git
synced 2025-04-19 14:53:20 +02:00
Add Loader-Spinner
Style Settings, re-add api-url-field
This commit is contained in:
parent
0b0abb3801
commit
1d8dd7381d
152
Website/App.tsx
152
Website/App.tsx
@ -6,157 +6,114 @@ import MonitorJobsList from "./modules/MonitorJobsList";
|
|||||||
import './styles/index.css'
|
import './styles/index.css'
|
||||||
import IFrontendSettings, {LoadFrontendSettings} from "./modules/interfaces/IFrontendSettings";
|
import IFrontendSettings, {LoadFrontendSettings} from "./modules/interfaces/IFrontendSettings";
|
||||||
import {useCookies} from "react-cookie";
|
import {useCookies} from "react-cookie";
|
||||||
|
import Loader from "./modules/Loader";
|
||||||
|
|
||||||
export default function App(){
|
export default function App(){
|
||||||
const [, setCookie] = useCookies(['apiUri', 'jobInterval']);
|
const [, setCookie] = useCookies(['apiUri', 'jobInterval']);
|
||||||
const [connected, setConnected] = React.useState(false);
|
const [connected, setConnected] = React.useState(false);
|
||||||
const [showSearch, setShowSearch] = React.useState(false);
|
const [showSearch, setShowSearch] = React.useState(false);
|
||||||
const [frontendSettings, setFrontendSettings] = useState<IFrontendSettings>(LoadFrontendSettings());
|
const [frontendSettings, setFrontendSettings] = useState<IFrontendSettings>(LoadFrontendSettings());
|
||||||
const [updateInterval, setUpdateInterval] = React.useState<number>();
|
const [updateInterval, setUpdateInterval] = React.useState<number | undefined>(undefined);
|
||||||
const [updateMonitorList, setUpdateMonitorList] = React.useState<Date>(new Date());
|
const checkConnectedInterval = 5000;
|
||||||
const checkConnectedInterval = 1000;
|
|
||||||
|
|
||||||
const apiUri = frontendSettings.apiUri;
|
const apiUri = frontendSettings.apiUri;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
checkConnection(apiUri).then(res => setConnected(res)).catch(() => setConnected(false));
|
setCookie('apiUri', frontendSettings.apiUri);
|
||||||
|
setCookie('jobInterval', frontendSettings.jobInterval);
|
||||||
|
updateConnected(apiUri, connected, setConnected);
|
||||||
|
}, [frontendSettings]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
if(updateInterval === undefined){
|
if(updateInterval === undefined){
|
||||||
setUpdateInterval(setInterval(() => {
|
setUpdateInterval(setInterval(() => {
|
||||||
checkConnection(apiUri).then(res => setConnected(res)).catch(() => setConnected(false));
|
updateConnected(apiUri, connected, setConnected);
|
||||||
}, checkConnectedInterval));
|
}, checkConnectedInterval));
|
||||||
}else{
|
}else{
|
||||||
clearInterval(updateInterval);
|
clearInterval(updateInterval);
|
||||||
setUpdateInterval(undefined);
|
setUpdateInterval(undefined);
|
||||||
}
|
}
|
||||||
}, [frontendSettings]);
|
}, [connected]);
|
||||||
|
|
||||||
function ChangeSettings(settings: IFrontendSettings) {
|
|
||||||
setFrontendSettings(settings);
|
|
||||||
setCookie('apiUri', settings.apiUri);
|
|
||||||
setCookie('jobInterval', settings.jobInterval);
|
|
||||||
}
|
|
||||||
|
|
||||||
const UpdateList = () => {setUpdateMonitorList(new Date())}
|
|
||||||
|
|
||||||
return(<div>
|
return(<div>
|
||||||
<Header apiUri={apiUri} backendConnected={connected} settings={frontendSettings} />
|
<Header apiUri={apiUri} backendConnected={connected} settings={frontendSettings} setFrontendSettings={setFrontendSettings} />
|
||||||
{connected
|
{connected
|
||||||
? <>
|
? <>
|
||||||
{showSearch
|
{showSearch
|
||||||
? <>
|
? <>
|
||||||
<Search apiUri={apiUri} jobInterval={frontendSettings.jobInterval} onJobsChanged={UpdateList} closeSearch={() => setShowSearch(false)} />
|
<Search apiUri={apiUri} jobInterval={frontendSettings.jobInterval} closeSearch={() => setShowSearch(false)} />
|
||||||
<hr/>
|
<hr/>
|
||||||
</>
|
</>
|
||||||
: <></>}
|
: <></>}
|
||||||
<MonitorJobsList updateList={updateMonitorList} apiUri={apiUri} onStartSearch={() => setShowSearch(true)} onJobsChanged={UpdateList} connectedToBackend={connected} />
|
<MonitorJobsList apiUri={apiUri} onStartSearch={() => setShowSearch(true)} connectedToBackend={connected} checkConnectedInterval={checkConnectedInterval} />
|
||||||
</>
|
</>
|
||||||
: <>
|
: <>
|
||||||
<h1>No connection to the Backend.</h1>
|
<h1>No connection to the Backend.</h1>
|
||||||
<h3>Check the Settings ApiUri.</h3>
|
<h3>Check the Settings ApiUri.</h3>
|
||||||
|
<Loader loading={true} />
|
||||||
</>}
|
</>}
|
||||||
<Footer apiUri={apiUri} connectedToBackend={connected} />
|
<Footer apiUri={apiUri} connectedToBackend={connected} checkConnectedInterval={checkConnectedInterval} />
|
||||||
</div>)
|
</div>)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getData(uri: string) : Promise<object> {
|
export function getData(uri: string) : Promise<object> {
|
||||||
return fetch(uri,
|
return makeRequest("GET", uri, null) as Promise<object>;
|
||||||
{
|
|
||||||
method: 'GET',
|
|
||||||
headers : {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'Accept': 'application/json'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.then(function(response){
|
|
||||||
if(!response.ok) throw new Error("Could not fetch data");
|
|
||||||
return response.json();
|
|
||||||
})
|
|
||||||
.catch(function(err){
|
|
||||||
console.error(`Error GETting Data ${uri}\n${err}`);
|
|
||||||
return Promise.reject();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function postData(uri: string, content: object | string | number) : Promise<object> {
|
export function postData(uri: string, content: object | string | number) : Promise<object> {
|
||||||
return fetch(uri,
|
return makeRequest("POST", uri, content) as Promise<object>;
|
||||||
{
|
|
||||||
method: 'POST',
|
|
||||||
headers : {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'Accept': 'application/json'
|
|
||||||
},
|
|
||||||
body: JSON.stringify(content)
|
|
||||||
})
|
|
||||||
.then(function(response){
|
|
||||||
if(!response.ok)
|
|
||||||
throw new Error("Could not fetch data");
|
|
||||||
let json = response.json();
|
|
||||||
return json.then((json) => json).catch(() => null);
|
|
||||||
})
|
|
||||||
.catch(function(err){
|
|
||||||
console.error(`Error POSTing Data ${uri}\n${err}`);
|
|
||||||
return Promise.reject();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function deleteData(uri: string) : Promise<void> {
|
export function deleteData(uri: string) : Promise<void> {
|
||||||
return fetch(uri,
|
return makeRequest("PUT", uri, null) as Promise<void>;
|
||||||
{
|
|
||||||
method: 'DELETE',
|
|
||||||
headers : {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'Accept': 'application/json'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.then(() =>{
|
|
||||||
return Promise.resolve();
|
|
||||||
})
|
|
||||||
.catch(function(err){
|
|
||||||
console.error(`Error DELETEing Data ${uri}\n${err}`);
|
|
||||||
return Promise.reject();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function patchData(uri: string, content: object | string | number) : Promise<object> {
|
export function patchData(uri: string, content: object | string | number) : Promise<object> {
|
||||||
return fetch(uri,
|
return makeRequest("patch", uri, content) as Promise<object>;
|
||||||
{
|
|
||||||
method: 'PATCH',
|
|
||||||
headers : {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'Accept': 'application/json'
|
|
||||||
},
|
|
||||||
body: JSON.stringify(content)
|
|
||||||
})
|
|
||||||
.then(function(response){
|
|
||||||
if(!response.ok)
|
|
||||||
throw new Error("Could not fetch data");
|
|
||||||
let json = response.json();
|
|
||||||
return json.then((json) => json).catch(() => null);
|
|
||||||
})
|
|
||||||
.catch(function(err){
|
|
||||||
console.error(`Error PATCHing Data ${uri}\n${err}`);
|
|
||||||
return Promise.reject();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function putData(uri: string, content: object | string | number) : Promise<object> {
|
export function putData(uri: string, content: object | string | number) : Promise<object> {
|
||||||
|
return makeRequest("PUT", uri, content) as Promise<object>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeRequest(method: string, uri: string, content: object | string | number | null) : Promise<object | void> {
|
||||||
return fetch(uri,
|
return fetch(uri,
|
||||||
{
|
{
|
||||||
method: 'PUT',
|
method: method,
|
||||||
headers : {
|
headers : {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
'Accept': 'application/json'
|
'Accept': 'application/json'
|
||||||
},
|
},
|
||||||
body: JSON.stringify(content)
|
body: content ? JSON.stringify(content) : null
|
||||||
})
|
})
|
||||||
.then(function(response){
|
.then(function(response){
|
||||||
if(!response.ok)
|
if(!response.ok){
|
||||||
throw new Error("Could not fetch data");
|
if(response.status === 503){
|
||||||
|
let retryHeaderVal = response.headers.get("Retry-After");
|
||||||
|
let seconds = 10;
|
||||||
|
if(!retryHeaderVal){
|
||||||
|
return response.text().then(text => {
|
||||||
|
seconds = parseInt(text);
|
||||||
|
return new Promise(resolve => setTimeout(resolve, seconds * 1000))
|
||||||
|
.then(() => {
|
||||||
|
return makeRequest(method, uri, content);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}else {
|
||||||
|
seconds = parseInt(retryHeaderVal);
|
||||||
|
return new Promise(resolve => setTimeout(resolve, seconds * 1000))
|
||||||
|
.then(() => {
|
||||||
|
return makeRequest(method, uri, content);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}else
|
||||||
|
throw new Error(response.statusText);
|
||||||
|
}
|
||||||
let json = response.json();
|
let json = response.json();
|
||||||
return json.then((json) => json).catch(() => null);
|
return json.then((json) => json).catch(() => null);
|
||||||
})
|
})
|
||||||
.catch(function(err){
|
.catch(function(err : Error){
|
||||||
console.error(`Error PUTting Data ${uri}\n${err}`);
|
console.error(`Error ${method}ing Data ${uri}\n${err}`);
|
||||||
return Promise.reject();
|
return Promise.reject();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -170,6 +127,15 @@ export function isValidUri(uri: string) : boolean{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const updateConnected = (apiUri: string, connected: boolean, setConnected: (c: boolean) => void) => {
|
||||||
|
checkConnection(apiUri)
|
||||||
|
.then(res => {
|
||||||
|
if(connected != res)
|
||||||
|
setConnected(res);
|
||||||
|
})
|
||||||
|
.catch(() => setConnected(false));
|
||||||
|
}
|
||||||
|
|
||||||
export const checkConnection = async (apiUri: string): Promise<boolean> =>{
|
export const checkConnection = async (apiUri: string): Promise<boolean> =>{
|
||||||
return fetch(`${apiUri}/swagger`,
|
return fetch(`${apiUri}/swagger`,
|
||||||
{
|
{
|
||||||
|
@ -6,12 +6,12 @@ import {mdiCounter, mdiEyeCheck, mdiRun, mdiTrayFull} from '@mdi/js';
|
|||||||
import QueuePopUp from "./QueuePopUp";
|
import QueuePopUp from "./QueuePopUp";
|
||||||
import {JobState, JobType} from "./interfaces/Jobs/IJob";
|
import {JobState, JobType} from "./interfaces/Jobs/IJob";
|
||||||
|
|
||||||
export default function Footer({connectedToBackend, apiUri} : {connectedToBackend: boolean, apiUri: string}) {
|
export default function Footer({connectedToBackend, apiUri, checkConnectedInterval} : {connectedToBackend: boolean, apiUri: string, checkConnectedInterval: number}) {
|
||||||
const [MonitoringJobsCount, setMonitoringJobsCount] = React.useState(0);
|
const [MonitoringJobsCount, setMonitoringJobsCount] = React.useState(0);
|
||||||
const [AllJobsCount, setAllJobsCount] = React.useState(0);
|
const [AllJobsCount, setAllJobsCount] = React.useState(0);
|
||||||
const [RunningJobsCount, setRunningJobsCount] = React.useState(0);
|
const [RunningJobsCount, setRunningJobsCount] = React.useState(0);
|
||||||
const [WaitingJobsCount, setWaitingJobs] = React.useState(0);
|
const [WaitingJobsCount, setWaitingJobs] = React.useState(0);
|
||||||
const [countUpdateInterval, setCountUpdateInterval] = React.useState<number>();
|
const [countUpdateInterval, setCountUpdateInterval] = React.useState<number | undefined>(undefined);
|
||||||
|
|
||||||
function UpdateBackendState(){
|
function UpdateBackendState(){
|
||||||
JobFunctions.GetJobsWithType(apiUri, JobType.DownloadAvailableChaptersJob).then((jobs) => setMonitoringJobsCount(jobs.length));
|
JobFunctions.GetJobsWithType(apiUri, JobType.DownloadAvailableChaptersJob).then((jobs) => setMonitoringJobsCount(jobs.length));
|
||||||
@ -23,9 +23,11 @@ export default function Footer({connectedToBackend, apiUri} : {connectedToBacken
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if(connectedToBackend){
|
if(connectedToBackend){
|
||||||
UpdateBackendState();
|
UpdateBackendState();
|
||||||
setCountUpdateInterval(setInterval(() => {
|
if(countUpdateInterval === undefined){
|
||||||
UpdateBackendState();
|
setCountUpdateInterval(setInterval(() => {
|
||||||
}, 2000));
|
UpdateBackendState();
|
||||||
|
}, checkConnectedInterval));
|
||||||
|
}
|
||||||
}else{
|
}else{
|
||||||
clearInterval(countUpdateInterval);
|
clearInterval(countUpdateInterval);
|
||||||
setCountUpdateInterval(undefined);
|
setCountUpdateInterval(undefined);
|
||||||
@ -36,7 +38,7 @@ export default function Footer({connectedToBackend, apiUri} : {connectedToBacken
|
|||||||
<footer>
|
<footer>
|
||||||
<div className="statusBadge" ><Icon path={mdiEyeCheck} size={1}/> <span>{MonitoringJobsCount}</span></div>
|
<div className="statusBadge" ><Icon path={mdiEyeCheck} size={1}/> <span>{MonitoringJobsCount}</span></div>
|
||||||
<span>+</span>
|
<span>+</span>
|
||||||
<QueuePopUp connectedToBackend={connectedToBackend} apiUri={apiUri}>
|
<QueuePopUp connectedToBackend={connectedToBackend} apiUri={apiUri} checkConnectedInterval={checkConnectedInterval}>
|
||||||
<div className="statusBadge hoverHand"><Icon path={mdiRun} size={1}/> <span>{RunningJobsCount}</span>
|
<div className="statusBadge hoverHand"><Icon path={mdiRun} size={1}/> <span>{RunningJobsCount}</span>
|
||||||
</div>
|
</div>
|
||||||
<span>+</span>
|
<span>+</span>
|
||||||
|
@ -3,13 +3,13 @@ import '../styles/header.css'
|
|||||||
import IFrontendSettings from "./interfaces/IFrontendSettings";
|
import IFrontendSettings from "./interfaces/IFrontendSettings";
|
||||||
import Settings from "./Settings";
|
import Settings from "./Settings";
|
||||||
|
|
||||||
export default function Header({backendConnected, apiUri, settings} : {backendConnected: boolean, apiUri: string, settings: IFrontendSettings}){
|
export default function Header({backendConnected, apiUri, settings, setFrontendSettings} : {backendConnected: boolean, apiUri: string, settings: IFrontendSettings, setFrontendSettings: (settings: IFrontendSettings) => void}){
|
||||||
return (
|
return (
|
||||||
<header>
|
<header>
|
||||||
<div id="titlebox">
|
<div id="titlebox">
|
||||||
<img alt="website image is Blahaj" src="../media/blahaj.png"/>
|
<img alt="website image is Blahaj" src="../media/blahaj.png"/>
|
||||||
<span>Tranga</span>
|
<span>Tranga</span>
|
||||||
</div>
|
</div>
|
||||||
<Settings backendConnected={backendConnected} apiUri={apiUri} settings={settings} />
|
<Settings backendConnected={backendConnected} apiUri={apiUri} frontendSettings={settings} setFrontendSettings={setFrontendSettings} />
|
||||||
</header>)
|
</header>)
|
||||||
}
|
}
|
6
Website/modules/Loader.tsx
Normal file
6
Website/modules/Loader.tsx
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import "../styles/loader.css";
|
||||||
|
import {CSSProperties} from "react";
|
||||||
|
|
||||||
|
export default function Loader({loading, style} : {loading: boolean, style?:CSSProperties|null}) {
|
||||||
|
return <span is-loading={loading ? "loading" : "done"} style={style ? style : undefined}></span>
|
||||||
|
}
|
@ -8,7 +8,6 @@ export default class LocalLibraryFunctions
|
|||||||
return getData(`${apiUri}/v2/LocalLibraries`)
|
return getData(`${apiUri}/v2/LocalLibraries`)
|
||||||
.then((json) => {
|
.then((json) => {
|
||||||
const ret = json as ILocalLibrary[];
|
const ret = json as ILocalLibrary[];
|
||||||
console.debug(ret);
|
|
||||||
return (ret);
|
return (ret);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -139,7 +139,7 @@ export default class MangaFunctions
|
|||||||
console.error(`chapterThreshold was not provided`);
|
console.error(`chapterThreshold was not provided`);
|
||||||
return Promise.reject();
|
return Promise.reject();
|
||||||
}
|
}
|
||||||
return patchData(`${apiUri}/v2/Manga/${mangaId}/IgnoreChaptersBefore`, {chapterThreshold});
|
return patchData(`${apiUri}/v2/Manga/${mangaId}/IgnoreChaptersBefore`, chapterThreshold);
|
||||||
}
|
}
|
||||||
|
|
||||||
static async MoveFolder(apiUri: string, mangaId: string, newPath: string): Promise<object> {
|
static async MoveFolder(apiUri: string, mangaId: string, newPath: string): Promise<object> {
|
||||||
|
@ -7,40 +7,45 @@ import IDownloadAvailableChaptersJob from "./interfaces/Jobs/IDownloadAvailableC
|
|||||||
import {MangaItem} from "./interfaces/IManga";
|
import {MangaItem} from "./interfaces/IManga";
|
||||||
import MangaFunctions from "./MangaFunctions";
|
import MangaFunctions from "./MangaFunctions";
|
||||||
|
|
||||||
export default function MonitorJobsList({onStartSearch, onJobsChanged, connectedToBackend, apiUri, updateList} : {onStartSearch() : void, onJobsChanged: EventHandler<any>, connectedToBackend: boolean, apiUri: string, updateList: Date}) {
|
export default function MonitorJobsList({onStartSearch, connectedToBackend, apiUri, checkConnectedInterval} : {onStartSearch() : void, connectedToBackend: boolean, apiUri: string, checkConnectedInterval: number}) {
|
||||||
const [MonitoringJobs, setMonitoringJobs] = useState<IDownloadAvailableChaptersJob[]>([]);
|
const [MonitoringJobs, setMonitoringJobs] = useState<IDownloadAvailableChaptersJob[]>([]);
|
||||||
const [joblistUpdateInterval, setJoblistUpdateInterval] = React.useState<number>();
|
const [joblistUpdateInterval, setJoblistUpdateInterval] = React.useState<number | undefined>(undefined);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if(connectedToBackend){
|
if(connectedToBackend) {
|
||||||
UpdateMonitoringJobsList();
|
UpdateMonitoringJobsList();
|
||||||
setJoblistUpdateInterval(setInterval(() => {
|
if(joblistUpdateInterval === undefined){
|
||||||
UpdateMonitoringJobsList();
|
setJoblistUpdateInterval(setInterval(() => {
|
||||||
}, 1000));
|
UpdateMonitoringJobsList();
|
||||||
|
}, checkConnectedInterval));
|
||||||
|
}
|
||||||
}else{
|
}else{
|
||||||
clearInterval(joblistUpdateInterval);
|
clearInterval(joblistUpdateInterval);
|
||||||
setJoblistUpdateInterval(undefined);
|
setJoblistUpdateInterval(undefined);
|
||||||
}
|
}
|
||||||
}, [connectedToBackend]);
|
}, [connectedToBackend]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
UpdateMonitoringJobsList();
|
|
||||||
}, [updateList]);
|
|
||||||
|
|
||||||
function UpdateMonitoringJobsList(){
|
function UpdateMonitoringJobsList(){
|
||||||
if(!connectedToBackend)
|
if(!connectedToBackend)
|
||||||
return;
|
return;
|
||||||
//console.debug("Updating MonitoringJobsList");
|
//console.debug("Updating MonitoringJobsList");
|
||||||
JobFunctions.GetJobsWithType(apiUri, JobType.DownloadAvailableChaptersJob)
|
JobFunctions.GetJobsWithType(apiUri, JobType.DownloadAvailableChaptersJob)
|
||||||
.then((jobs) => setMonitoringJobs(jobs as IDownloadAvailableChaptersJob[]));
|
.then((jobs) => jobs as IDownloadAvailableChaptersJob[])
|
||||||
|
.then((jobs) => {
|
||||||
|
if(jobs.length != MonitoringJobs.length ||
|
||||||
|
MonitoringJobs.filter(j => jobs.find(nj => nj.jobId == j.jobId)).length > 1 ||
|
||||||
|
jobs.filter(nj => MonitoringJobs.find(j => nj.jobId == j.jobId)).length > 1){
|
||||||
|
setMonitoringJobs(jobs);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function StartSearchMangaEntry() : ReactElement {
|
function StartSearchMangaEntry() : ReactElement {
|
||||||
return (<div key="monitorMangaEntry.StartSearch" className="startSearchEntry MangaItem" onClick={onStartSearch}>
|
return (<div key="monitorMangaEntry.StartSearch" className="startSearchEntry MangaItem" onClick={onStartSearch}>
|
||||||
<img className="MangaItem-Cover" src="../media/blahaj.png" alt="Blahaj"></img>
|
<img className="MangaItem-Cover" src="../media/blahaj.png" alt="Blahaj"></img>
|
||||||
<div>
|
<div>
|
||||||
<p style={{textAlign: "center", width: "100%"}} className="MangaItem-Name">Add new Manga</p>
|
<div style={{margin: "30px auto", color: "black", textShadow: "1px 2px #f5a9b8"}} className="MangaItem-Name">Add new Manga</div>
|
||||||
<p style={{fontSize: "42pt", textAlign: "center"}}>+</p>
|
<div style={{fontSize: "42pt", textAlign: "center", textShadow: "1px 2px #5bcefa"}}>+</div>
|
||||||
</div>
|
</div>
|
||||||
</div>);
|
</div>);
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React, {useEffect, useState} from 'react';
|
import React, {ReactElement, useEffect, useState} from 'react';
|
||||||
import IJob, {JobState, JobType} from "./interfaces/Jobs/IJob";
|
import IJob, {JobState, JobType} from "./interfaces/Jobs/IJob";
|
||||||
import '../styles/queuePopUp.css';
|
import '../styles/queuePopUp.css';
|
||||||
import '../styles/popup.css';
|
import '../styles/popup.css';
|
||||||
@ -6,25 +6,21 @@ import JobFunctions from "./JobFunctions";
|
|||||||
import IDownloadSingleChapterJob from "./interfaces/Jobs/IDownloadSingleChapterJob";
|
import IDownloadSingleChapterJob from "./interfaces/Jobs/IDownloadSingleChapterJob";
|
||||||
import {ChapterItem} from "./interfaces/IChapter";
|
import {ChapterItem} from "./interfaces/IChapter";
|
||||||
|
|
||||||
export default function QueuePopUp({connectedToBackend, children, apiUri} : {connectedToBackend: boolean, children: JSX.Element[], apiUri: string}) {
|
export default function QueuePopUp({connectedToBackend, children, apiUri, checkConnectedInterval} : {connectedToBackend: boolean, children: ReactElement[], apiUri: string, checkConnectedInterval: number}) {
|
||||||
|
|
||||||
const [WaitingJobs, setWaitingJobs] = React.useState<IJob[]>([]);
|
const [WaitingJobs, setWaitingJobs] = React.useState<IJob[]>([]);
|
||||||
const [RunningJobs, setRunningJobs] = React.useState<IJob[]>([]);
|
const [RunningJobs, setRunningJobs] = React.useState<IJob[]>([]);
|
||||||
const [showQueuePopup, setShowQueuePopup] = useState<boolean>(false);
|
const [showQueuePopup, setShowQueuePopup] = useState<boolean>(false);
|
||||||
const [queueListInterval, setQueueListInterval] = React.useState<number>();
|
const [queueListInterval, setQueueListInterval] = React.useState<number | undefined>(undefined);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if(!showQueuePopup)
|
if(connectedToBackend) {
|
||||||
return;
|
|
||||||
UpdateMonitoringJobsList();
|
|
||||||
}, [showQueuePopup]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if(connectedToBackend){
|
|
||||||
UpdateMonitoringJobsList();
|
UpdateMonitoringJobsList();
|
||||||
setQueueListInterval(setInterval(() => {
|
if(queueListInterval === undefined){
|
||||||
UpdateMonitoringJobsList();
|
setQueueListInterval(setInterval(() => {
|
||||||
}, 2000));
|
UpdateMonitoringJobsList();
|
||||||
|
}, checkConnectedInterval));
|
||||||
|
}
|
||||||
}else{
|
}else{
|
||||||
clearInterval(queueListInterval);
|
clearInterval(queueListInterval);
|
||||||
setQueueListInterval(undefined);
|
setQueueListInterval(undefined);
|
||||||
|
@ -8,8 +8,9 @@ import SearchFunctions from "./SearchFunctions";
|
|||||||
import JobFunctions from "./JobFunctions";
|
import JobFunctions from "./JobFunctions";
|
||||||
import ILocalLibrary from "./interfaces/ILocalLibrary";
|
import ILocalLibrary from "./interfaces/ILocalLibrary";
|
||||||
import LocalLibraryFunctions from "./LocalLibraryFunctions";
|
import LocalLibraryFunctions from "./LocalLibraryFunctions";
|
||||||
|
import Loader from "./Loader";
|
||||||
|
|
||||||
export default function Search({apiUri, jobInterval, onJobsChanged, closeSearch} : {apiUri: string, jobInterval: Date, onJobsChanged: (internalId: string) => void, closeSearch(): void}) {
|
export default function Search({apiUri, jobInterval, closeSearch} : {apiUri: string, jobInterval: Date, closeSearch(): void}) {
|
||||||
const [mangaConnectors, setConnectors] = useState<IMangaConnector[]>();
|
const [mangaConnectors, setConnectors] = useState<IMangaConnector[]>();
|
||||||
const [selectedConnector, setSelectedConnector] = useState<IMangaConnector>();
|
const [selectedConnector, setSelectedConnector] = useState<IMangaConnector>();
|
||||||
const [selectedLanguage, setSelectedLanguage] = useState<string>();
|
const [selectedLanguage, setSelectedLanguage] = useState<string>();
|
||||||
@ -19,11 +20,8 @@ export default function Search({apiUri, jobInterval, onJobsChanged, closeSearch}
|
|||||||
const pattern = /https:\/\/([a-z0-9.]+\.[a-z0-9]{2,})(?:\/.*)?/i
|
const pattern = /https:\/\/([a-z0-9.]+\.[a-z0-9]{2,})(?:\/.*)?/i
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if(mangaConnectors === undefined) {
|
MangaConnectorFunctions.GetAllConnectors(apiUri).then(setConnectors).then(() => setLoading(false));
|
||||||
MangaConnectorFunctions.GetAllConnectors(apiUri).then(setConnectors)
|
}, []);
|
||||||
return;
|
|
||||||
}
|
|
||||||
}, [mangaConnectors]);
|
|
||||||
|
|
||||||
const selectedConnectorChanged : ChangeEventHandler<HTMLSelectElement> = (event) => {
|
const selectedConnectorChanged : ChangeEventHandler<HTMLSelectElement> = (event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
@ -73,22 +71,26 @@ export default function Search({apiUri, jobInterval, onJobsChanged, closeSearch}
|
|||||||
console.error("URL in Searchbox detected, but does not match selected connector.");
|
console.error("URL in Searchbox detected, but does not match selected connector.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
setLoading(true);
|
||||||
if(!isValidUri(searchBoxValue)){
|
if(!isValidUri(searchBoxValue)){
|
||||||
SearchFunctions.SearchNameOnConnector(apiUri, selectedConnector.name, searchBoxValue)
|
SearchFunctions.SearchNameOnConnector(apiUri, selectedConnector.name, searchBoxValue)
|
||||||
.then((mangas: IManga[]) => {
|
.then((mangas: IManga[]) => {
|
||||||
setSearchResults(mangas);
|
setSearchResults(mangas);
|
||||||
});
|
})
|
||||||
|
.finally(()=>setLoading(false));
|
||||||
}else{
|
}else{
|
||||||
SearchFunctions.SearchUrl(apiUri, searchBoxValue)
|
SearchFunctions.SearchUrl(apiUri, searchBoxValue)
|
||||||
.then((manga: IManga) => {
|
.then((manga: IManga) => {
|
||||||
setSearchResults([manga]);
|
setSearchResults([manga]);
|
||||||
});
|
})
|
||||||
|
.finally(()=>setLoading(false));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const changeSelectedLanguage : ChangeEventHandler<HTMLSelectElement> = (event) => setSelectedLanguage(event.target.value);
|
const changeSelectedLanguage : ChangeEventHandler<HTMLSelectElement> = (event) => setSelectedLanguage(event.target.value);
|
||||||
let [selectedLibrary, setSelectedLibrary] = useState<ILocalLibrary | null>(null);
|
let [selectedLibrary, setSelectedLibrary] = useState<ILocalLibrary | null>(null);
|
||||||
let [libraries, setLibraries] = useState<ILocalLibrary[] | null>(null);
|
let [libraries, setLibraries] = useState<ILocalLibrary[] | null>(null);
|
||||||
|
let [loading, setLoading] = useState<boolean>(true);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
LocalLibraryFunctions.GetLibraries(apiUri).then(setLibraries);
|
LocalLibraryFunctions.GetLibraries(apiUri).then(setLibraries);
|
||||||
}, []);
|
}, []);
|
||||||
@ -111,36 +113,37 @@ export default function Search({apiUri, jobInterval, onJobsChanged, closeSearch}
|
|||||||
|
|
||||||
return (<div id="Search">
|
return (<div id="Search">
|
||||||
<div id="SearchBox">
|
<div id="SearchBox">
|
||||||
<input type="text" placeholder="Manganame" id="Searchbox-Manganame" onKeyDown={(e) => {if(e.key == "Enter") ExecuteSearch(null);}} onChange={searchBoxValueChanged}></input>
|
<input type="text" placeholder="Manganame" id="Searchbox-Manganame" onKeyDown={(e) => {if(e.key == "Enter") ExecuteSearch(null);}} onChange={searchBoxValueChanged} disabled={loading} />
|
||||||
<select id="Searchbox-Connector" value={selectedConnector === undefined ? "" : selectedConnector.name} onChange={selectedConnectorChanged}>
|
<select id="Searchbox-Connector" value={selectedConnector === undefined ? "" : selectedConnector.name} onChange={selectedConnectorChanged} disabled={loading}>
|
||||||
{mangaConnectors === undefined ? <option value="Loading">Loading</option> : <option value="" disabled hidden>Select</option>}
|
{mangaConnectors === undefined ? <option value="Loading">Loading</option> : <option value="" disabled hidden>Select</option>}
|
||||||
{mangaConnectors === undefined
|
{mangaConnectors === undefined
|
||||||
? null
|
? null
|
||||||
: mangaConnectors.map(con => <option value={con.name} key={con.name}>{con.name}</option>)}
|
: mangaConnectors.map(con => <option value={con.name} key={con.name}>{con.name}</option>)}
|
||||||
</select>
|
</select>
|
||||||
<select id="Searchbox-language" onChange={changeSelectedLanguage} value={selectedLanguage === null ? "" : selectedLanguage}>
|
<select id="Searchbox-language" onChange={changeSelectedLanguage} value={selectedLanguage === null ? "" : selectedLanguage} disabled={loading}>
|
||||||
{mangaConnectors === undefined ? <option value="Loading">Loading</option> : <option value="" disabled hidden>Select Connector</option>}
|
{mangaConnectors === undefined ? <option value="Loading">Loading</option> : <option value="" disabled hidden>Select Connector</option>}
|
||||||
{selectedConnector === undefined
|
{selectedConnector === undefined
|
||||||
? null
|
? null
|
||||||
: selectedConnector.supportedLanguages.map(language => <option value={language} key={language}>{language}</option>)}
|
: selectedConnector.supportedLanguages.map(language => <option value={language} key={language}>{language}</option>)}
|
||||||
</select>
|
</select>
|
||||||
<button id="Searchbox-button" type="submit" onClick={ExecuteSearch}>Search</button>
|
<button id="Searchbox-button" type="submit" onClick={ExecuteSearch} disabled={loading}>Search</button>
|
||||||
|
<Loader loading={loading} style={{width:"40px", height:"40px"}}/>
|
||||||
</div>
|
</div>
|
||||||
<img alt="Close Search" id="closeSearch" src="../media/close-x.svg" onClick={closeSearch} />
|
<img alt="Close Search" id="closeSearch" src="../media/close-x.svg" onClick={closeSearch} />
|
||||||
<div id="SearchResults">
|
<div id="SearchResults">
|
||||||
{searchResults === undefined
|
{searchResults === undefined
|
||||||
? <p></p>
|
? null
|
||||||
: searchResults.map(result =>
|
: searchResults.map(result => {
|
||||||
<MangaItem apiUri={apiUri} mangaId={result.mangaId}>
|
return <MangaItem apiUri={apiUri} mangaId={result.mangaId} >
|
||||||
<select defaultValue={selectedLibrary === null ? "" : selectedLibrary.localLibraryId} onChange={selectedLibraryChanged}>
|
<select defaultValue={selectedLibrary === null ? "" : selectedLibrary.localLibraryId} onChange={selectedLibraryChanged}>
|
||||||
{selectedLibrary === null || libraries === null ? <option value="">Loading</option>
|
{selectedLibrary === null || libraries === null ? <option value="">Loading</option>
|
||||||
: libraries.map(library => <option key={library.localLibraryId} value={library.localLibraryId}>{library.libraryName} ({library.basePath})</option>)}
|
: libraries.map(library => <option key={library.localLibraryId} value={library.localLibraryId}>{library.libraryName} ({library.basePath})</option>)}
|
||||||
</select>
|
</select>
|
||||||
<button className="Manga-AddButton" onClick={() => {
|
<button className="Manga-AddButton" onClick={() => {
|
||||||
JobFunctions.CreateDownloadAvailableChaptersJob(apiUri, result.mangaId, {recurrenceTimeMs: jobInterval.getTime(), localLibraryId: selectedLibrary!.localLibraryId}).then(() => onJobsChanged(result.mangaId));
|
JobFunctions.CreateDownloadAvailableChaptersJob(apiUri, result.mangaId, {recurrenceTimeMs: jobInterval.getTime(), localLibraryId: selectedLibrary!.localLibraryId});
|
||||||
}}>Monitor</button>
|
}}>Monitor</button>
|
||||||
</MangaItem>
|
</MangaItem>
|
||||||
)
|
})
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>);
|
</div>);
|
||||||
|
@ -5,7 +5,7 @@ import React, {useEffect, useState} from "react";
|
|||||||
import INotificationConnector, {NotificationConnectorItem} from "./interfaces/INotificationConnector";
|
import INotificationConnector, {NotificationConnectorItem} from "./interfaces/INotificationConnector";
|
||||||
import NotificationConnectorFunctions from "./NotificationConnectorFunctions";
|
import NotificationConnectorFunctions from "./NotificationConnectorFunctions";
|
||||||
|
|
||||||
export default function Settings({backendConnected, apiUri, settings} : {backendConnected: boolean, apiUri: string, settings: IFrontendSettings}) {
|
export default function Settings({backendConnected, apiUri, frontendSettings, setFrontendSettings} : {backendConnected: boolean, apiUri: string, frontendSettings: IFrontendSettings, setFrontendSettings: (settings: IFrontendSettings) => void}) {
|
||||||
let [showSettings, setShowSettings] = useState<boolean>(false);
|
let [showSettings, setShowSettings] = useState<boolean>(false);
|
||||||
let [notificationConnectors, setNotificationConnectors] = useState<INotificationConnector[]>([]);
|
let [notificationConnectors, setNotificationConnectors] = useState<INotificationConnector[]>([]);
|
||||||
|
|
||||||
@ -15,6 +15,7 @@ export default function Settings({backendConnected, apiUri, settings} : {backend
|
|||||||
NotificationConnectorFunctions.GetNotificationConnectors(apiUri).then(setNotificationConnectors);
|
NotificationConnectorFunctions.GetNotificationConnectors(apiUri).then(setNotificationConnectors);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div id="Settings">
|
<div id="Settings">
|
||||||
<div onClick={() => setShowSettings(true)}>
|
<div onClick={() => setShowSettings(true)}>
|
||||||
@ -27,6 +28,23 @@ export default function Settings({backendConnected, apiUri, settings} : {backend
|
|||||||
<img alt="Close Settings" className="close" src="../media/close-x.svg" onClick={() => setShowSettings(false)}/>
|
<img alt="Close Settings" className="close" src="../media/close-x.svg" onClick={() => setShowSettings(false)}/>
|
||||||
</div>
|
</div>
|
||||||
<div id="SettingsPopUpBody" className="popupBody">
|
<div id="SettingsPopUpBody" className="popupBody">
|
||||||
|
<div className="settings-apiuri">
|
||||||
|
<label>ApiUri</label>
|
||||||
|
<input type="url" defaultValue={frontendSettings.apiUri} onChange={(e) => {
|
||||||
|
let newSettings = frontendSettings;
|
||||||
|
newSettings.apiUri = e.currentTarget.value;
|
||||||
|
setFrontendSettings(newSettings);
|
||||||
|
}} id="ApiUri" />
|
||||||
|
</div>
|
||||||
|
<div className="settings-apiuri">
|
||||||
|
<label>Default Job-Interval</label>
|
||||||
|
<input type="time" defaultValue={new Date(frontendSettings.jobInterval).getTime()} onChange={(e) => {
|
||||||
|
let newSettings = frontendSettings;
|
||||||
|
newSettings.jobInterval = e.currentTarget.valueAsDate ?? frontendSettings.jobInterval;
|
||||||
|
setFrontendSettings(newSettings);
|
||||||
|
console.log(frontendSettings);
|
||||||
|
}}/>
|
||||||
|
</div>
|
||||||
{notificationConnectors.map(c => <NotificationConnectorItem apiUri={apiUri} notificationConnector={c} />)}
|
{notificationConnectors.map(c => <NotificationConnectorItem apiUri={apiUri} notificationConnector={c} />)}
|
||||||
<NotificationConnectorItem apiUri={apiUri} notificationConnector={null} />
|
<NotificationConnectorItem apiUri={apiUri} notificationConnector={null} />
|
||||||
</div>
|
</div>
|
||||||
|
@ -7,18 +7,17 @@ export default interface IAuthor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function AuthorElement({apiUri, authorId} : {apiUri: string, authorId: string | null}) : ReactElement{
|
export function AuthorElement({apiUri, authorId} : {apiUri: string, authorId: string | null}) : ReactElement{
|
||||||
if(authorId === null)
|
let [author, setAuthor] = React.useState<IAuthor | null>(null);
|
||||||
return (<p className="Manga-Author-Name">Author</p>);
|
|
||||||
|
|
||||||
let [name, setName] = React.useState<string>(authorId);
|
|
||||||
|
|
||||||
useEffect(()=> {
|
useEffect(()=> {
|
||||||
|
if(authorId === null)
|
||||||
|
return;
|
||||||
getData(`${apiUri}/v2/Query/Author/${authorId}`)
|
getData(`${apiUri}/v2/Query/Author/${authorId}`)
|
||||||
.then((json) => {
|
.then((json) => {
|
||||||
let ret = json as IAuthor;
|
let ret = json as IAuthor;
|
||||||
setName(ret.authorName);
|
setAuthor(ret);
|
||||||
});
|
});
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
return (<p className="Manga-Author-Name">{name}</p>);
|
return (<span className="Manga-Author-Name">{author ? author.authorName : authorId}</span>);
|
||||||
}
|
}
|
@ -8,20 +8,17 @@ export default interface ILink {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function LinkElement({apiUri, linkId} : {apiUri: string, linkId: string | null}) : ReactElement{
|
export function LinkElement({apiUri, linkId} : {apiUri: string, linkId: string | null}) : ReactElement{
|
||||||
if(linkId === null)
|
let [link, setLink] = React.useState<ILink | null>(null);
|
||||||
return (<a className="Manga-Link-Value" href="#">Link</a>);
|
|
||||||
|
|
||||||
let [provider, setProvider] = React.useState<string>(linkId);
|
|
||||||
let [linkUrl, setLinkUrl] = React.useState<string>("");
|
|
||||||
|
|
||||||
useEffect(()=> {
|
useEffect(()=> {
|
||||||
|
if(linkId === null)
|
||||||
|
return;
|
||||||
getData(`${apiUri}/v2/Query/Link/${linkId}`)
|
getData(`${apiUri}/v2/Query/Link/${linkId}`)
|
||||||
.then((json) => {
|
.then((json) => {
|
||||||
let ret = json as ILink;
|
let ret = json as ILink;
|
||||||
setProvider(ret.linkProvider);
|
setLink(ret);
|
||||||
setLinkUrl(ret.linkUrl);
|
|
||||||
});
|
});
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
return (<a className="Manga-Link-Value" href={linkUrl}>{provider}</a>);
|
return (<a className="Manga-Link-Value" href={link ? link.linkUrl : "#"}>{link ? link.linkProvider : linkId}</a>);
|
||||||
}
|
}
|
@ -5,6 +5,8 @@ import { mdiTagTextOutline, mdiAccountEdit, mdiLinkVariant } from '@mdi/js';
|
|||||||
import MarkdownPreview from '@uiw/react-markdown-preview';
|
import MarkdownPreview from '@uiw/react-markdown-preview';
|
||||||
import {AuthorElement} from "./IAuthor";
|
import {AuthorElement} from "./IAuthor";
|
||||||
import {LinkElement} from "./ILink";
|
import {LinkElement} from "./ILink";
|
||||||
|
import IChapter from "./IChapter";
|
||||||
|
import Loader from "../Loader";
|
||||||
|
|
||||||
export default interface IManga{
|
export default interface IManga{
|
||||||
mangaId: string;
|
mangaId: string;
|
||||||
@ -40,56 +42,87 @@ export function MangaItem({apiUri, mangaId, children} : {apiUri: string, mangaId
|
|||||||
|
|
||||||
let [manga, setManga] = useState<IManga | null>(null);
|
let [manga, setManga] = useState<IManga | null>(null);
|
||||||
let [clicked, setClicked] = useState<boolean>(false);
|
let [clicked, setClicked] = useState<boolean>(false);
|
||||||
|
let [latestChapterDownloaded, setLatestChapterDownloaded] = useState<IChapter | null>(null);
|
||||||
|
let [latestChapterAvailable, setLatestChapterAvailable] = useState<IChapter | null>(null);
|
||||||
|
let [loadingChapterStats, setLoadingChapterStats] = useState<boolean>(true);
|
||||||
|
let [settingThreshold, setSettingThreshold] = useState<boolean>(false);
|
||||||
|
const invalidTargets = ["input", "textarea", "button", "select", "a"];
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
MangaFunctions.GetMangaById(apiUri, mangaId).then(setManga);
|
MangaFunctions.GetMangaById(apiUri, mangaId).then(setManga);
|
||||||
|
MangaFunctions.GetLatestChapterDownloaded(apiUri, mangaId)
|
||||||
|
.then(setLatestChapterDownloaded)
|
||||||
|
.finally(() => {
|
||||||
|
if(latestChapterDownloaded && latestChapterAvailable)
|
||||||
|
setLoadingChapterStats(false);
|
||||||
|
});
|
||||||
|
MangaFunctions.GetLatestChapterAvailable(apiUri, mangaId)
|
||||||
|
.then(setLatestChapterAvailable)
|
||||||
|
.finally(() => {
|
||||||
|
if(latestChapterDownloaded && latestChapterAvailable)
|
||||||
|
setLoadingChapterStats(false);
|
||||||
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (<div className="MangaItem" key={mangaId} is-clicked={clicked ? "clicked" : "not-clicked"} onClick={()=>setClicked(!clicked)}>
|
return (<div className="MangaItem" key={mangaId} is-clicked={clicked ? "clicked" : "not-clicked"} onClick={(e)=> {
|
||||||
<img className="MangaItem-Cover" src={MangaFunctions.GetMangaCoverImageUrl(apiUri, mangaId, undefined)} alt="MangaFunctions Cover" onLoad={LoadMangaCover} onResize={LoadMangaCover}></img>
|
e.preventDefault();
|
||||||
<p className="MangaItem-Connector">{manga ? manga.mangaConnectorId : "Connector"}</p>
|
const target = e.target as HTMLElement;
|
||||||
<p className="MangaItem-Status" release-status={manga?.releaseStatus}></p>
|
if(invalidTargets.find(x => x == target.localName) === undefined )
|
||||||
<p className="MangaItem-Name">{manga ? manga.name : "Name"}</p>
|
setClicked(!clicked)
|
||||||
|
}}>
|
||||||
|
<img className="MangaItem-Cover" src="../../media/blahaj.png" alt="MangaFunctions Cover" onLoad={LoadMangaCover} onResize={LoadMangaCover}></img>
|
||||||
|
<div className="MangaItem-Connector">{manga ? manga.mangaConnectorId : "Connector"}</div>
|
||||||
|
<div className="MangaItem-Status" release-status={manga?.releaseStatus}></div>
|
||||||
|
<div className="MangaItem-Name">{manga ? manga.name : "Name"}</div>
|
||||||
<a className="MangaItem-Website" href={manga ? manga.websiteUrl : "#"}><img src="../../media/link.svg" alt="Link"/></a>
|
<a className="MangaItem-Website" href={manga ? manga.websiteUrl : "#"}><img src="../../media/link.svg" alt="Link"/></a>
|
||||||
<div className="MangaItem-Tags">
|
<div className="MangaItem-Tags">
|
||||||
{manga ? manga.authorIds.map(authorId =>
|
{manga ? manga.authorIds.map(authorId =>
|
||||||
<p className="MangaItem-Author" key={authorId} >
|
<div className="MangaItem-Author" key={authorId} >
|
||||||
<Icon path={mdiAccountEdit} size={0.5} />
|
<Icon path={mdiAccountEdit} size={0.5} />
|
||||||
<AuthorElement apiUri={apiUri} authorId={authorId}></AuthorElement>
|
<AuthorElement apiUri={apiUri} authorId={authorId}></AuthorElement>
|
||||||
</p>)
|
</div>)
|
||||||
:
|
:
|
||||||
<p className="MangaItem-Author">
|
<div className="MangaItem-Author" key="null-Author">
|
||||||
<Icon path={mdiAccountEdit} size={0.5} />
|
<Icon path={mdiAccountEdit} size={0.5} />
|
||||||
<AuthorElement apiUri={apiUri} authorId={null}></AuthorElement>
|
<AuthorElement apiUri={apiUri} authorId={null}></AuthorElement>
|
||||||
</p>}
|
</div>}
|
||||||
{manga ? manga.tags.map(tag =>
|
{manga ? manga.tags.map(tag =>
|
||||||
<p className="MangaItem-Tag" key={tag}>
|
<div className="MangaItem-Tag" key={tag}>
|
||||||
<Icon path={mdiTagTextOutline} size={0.5}/>
|
<Icon path={mdiTagTextOutline} size={0.5}/>
|
||||||
<p className="MangaItem-Tag-Value">{tag}</p>
|
<span className="MangaItem-Tag-Value">{tag}</span>
|
||||||
</p>)
|
</div>)
|
||||||
:
|
:
|
||||||
<p className="MangaItem-Tag">
|
<div className="MangaItem-Tag" key="null-Tag">
|
||||||
<Icon path={mdiTagTextOutline} size={0.5}/>
|
<Icon path={mdiTagTextOutline} size={0.5}/>
|
||||||
<p className="MangaItem-Tag-Value">Tag</p>
|
<span className="MangaItem-Tag-Value">Tag</span>
|
||||||
</p>
|
</div>
|
||||||
}
|
}
|
||||||
{manga ? manga.linkIds.map(linkId =>
|
{manga ? manga.linkIds.map(linkId =>
|
||||||
<p className="MangaItem-Link" key={linkId}>
|
<div className="MangaItem-Link" key={linkId}>
|
||||||
<Icon path={mdiLinkVariant} size={0.5}/>
|
<Icon path={mdiLinkVariant} size={0.5}/>
|
||||||
<LinkElement apiUri={apiUri} linkId={linkId} />
|
<LinkElement apiUri={apiUri} linkId={linkId} />
|
||||||
</p>)
|
</div>)
|
||||||
:
|
:
|
||||||
<p className="MangaItem-Link">
|
<div className="MangaItem-Link" key="null-Link">
|
||||||
<Icon path={mdiLinkVariant} size={0.5}/>
|
<Icon path={mdiLinkVariant} size={0.5}/>
|
||||||
<LinkElement apiUri={apiUri} linkId={null} />
|
<LinkElement apiUri={apiUri} linkId={null} />
|
||||||
</p>}
|
</div>}
|
||||||
</div>
|
</div>
|
||||||
<MarkdownPreview className="MangaItem-Description" source={manga ? manga.description : "# Description"} />
|
<MarkdownPreview className="MangaItem-Description" source={manga ? manga.description : "# Description"} />
|
||||||
<div className="MangaItem-Props">
|
<div className="MangaItem-Props">
|
||||||
|
<div className="MangaItem-Props-Threshold">
|
||||||
|
Start at Chapter
|
||||||
|
<input type="text" defaultValue={latestChapterDownloaded ? latestChapterDownloaded.chapterNumber : ""} disabled={settingThreshold} onChange={(e) => {
|
||||||
|
setSettingThreshold(true);
|
||||||
|
MangaFunctions.SetIgnoreThreshold(apiUri, mangaId, e.currentTarget.valueAsNumber).finally(()=>setSettingThreshold(false));
|
||||||
|
}} />
|
||||||
|
<Loader loading={settingThreshold} />
|
||||||
|
out of <span className="MangaItem-Props-Threshold-Available">{latestChapterAvailable ? latestChapterAvailable.chapterNumber : <Loader loading={loadingChapterStats}/>}</span>
|
||||||
|
</div>
|
||||||
{children ? children.map(c => {
|
{children ? children.map(c => {
|
||||||
if(c instanceof Element)
|
if(c instanceof Element)
|
||||||
return c as ReactElement;
|
return c as ReactElement;
|
||||||
else
|
else
|
||||||
return <span>{c}</span>
|
return c
|
||||||
}) : null}
|
}) : null}
|
||||||
</div>
|
</div>
|
||||||
</div>)
|
</div>)
|
||||||
|
@ -1,5 +0,0 @@
|
|||||||
export default interface IMangaAltTitle {
|
|
||||||
altTitleId: string;
|
|
||||||
language: string;
|
|
||||||
title: string;
|
|
||||||
}
|
|
25
Website/modules/interfaces/IMangaAltTitle.tsx
Normal file
25
Website/modules/interfaces/IMangaAltTitle.tsx
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import React, {ReactElement, useEffect} from "react";
|
||||||
|
import {getData} from "../../App";
|
||||||
|
import IAuthor from "./IAuthor";
|
||||||
|
|
||||||
|
export default interface IMangaAltTitle {
|
||||||
|
altTitleId: string;
|
||||||
|
language: string;
|
||||||
|
title: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AltTitleElement({apiUri, altTitleId} : {apiUri: string, altTitleId: string | null}) : ReactElement{
|
||||||
|
let [altTitle, setAltTitle] = React.useState<IMangaAltTitle | null>(null);
|
||||||
|
|
||||||
|
useEffect(()=> {
|
||||||
|
if(altTitleId === null)
|
||||||
|
return;
|
||||||
|
getData(`${apiUri}/v2/Query/AltTitle/${altTitleId}`)
|
||||||
|
.then((json) => {
|
||||||
|
let ret = json as IMangaAltTitle;
|
||||||
|
setAltTitle(ret);
|
||||||
|
});
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return (<span className="Manga-AltTitle">{altTitle ? altTitle.title : altTitleId}</span>);
|
||||||
|
}
|
@ -1,4 +1,7 @@
|
|||||||
import {ReactElement, ReactEventHandler, useState} from "react";
|
import {ReactElement, ReactEventHandler, useState} from "react";
|
||||||
|
import "../../styles/notificationConnector.css";
|
||||||
|
import Loader from "../Loader";
|
||||||
|
import NotificationConnectorFunctions from "../NotificationConnectorFunctions";
|
||||||
|
|
||||||
export default interface INotificationConnector {
|
export default interface INotificationConnector {
|
||||||
name: string;
|
name: string;
|
||||||
@ -9,49 +12,77 @@ export default interface INotificationConnector {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function NotificationConnectorItem({apiUri, notificationConnector} : {apiUri: string, notificationConnector: INotificationConnector | null}) : ReactElement {
|
export function NotificationConnectorItem({apiUri, notificationConnector} : {apiUri: string, notificationConnector: INotificationConnector | null}) : ReactElement {
|
||||||
const Save : ReactEventHandler<HTMLButtonElement> = (e) => {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
const AddHeader : ReactEventHandler<HTMLButtonElement> = (e) => {
|
const AddHeader : ReactEventHandler<HTMLButtonElement> = (e) => {
|
||||||
const div = e.currentTarget.parentElement as HTMLDivElement;
|
let header : Record<string, string> = {};
|
||||||
setHeaderElements([...headerElements, <HeaderElement record={null} />])
|
let x = info;
|
||||||
|
x.headers = [header, ...x.headers];
|
||||||
|
setInfo(x);
|
||||||
|
setHeaderElements([...headerElements, <HeaderElement record={header} />])
|
||||||
}
|
}
|
||||||
const [headerElements, setHeaderElements] = useState<ReactElement[]>([]);
|
const [headerElements, setHeaderElements] = useState<ReactElement[]>([]);
|
||||||
|
const [loading, setLoading] = useState<boolean>(false);
|
||||||
|
const [info, setInfo] = useState<INotificationConnector>({
|
||||||
|
name: "",
|
||||||
|
url: "",
|
||||||
|
headers: [],
|
||||||
|
httpMethod: "",
|
||||||
|
body: ""
|
||||||
|
});
|
||||||
|
|
||||||
return (<div className="NotificationConnectorItem" key={notificationConnector ? notificationConnector.name : "new"}>
|
return (<div className="NotificationConnectorItem" key={notificationConnector ? notificationConnector.name : "new"}>
|
||||||
<p className="NotificationConnectorItem-Name">{notificationConnector ? notificationConnector.name : "New Notification Connector"}</p>
|
<p className="NotificationConnectorItem-Name">{notificationConnector ? notificationConnector.name : "New Notification Connector"}</p>
|
||||||
<select className="NotificationConnectorItem-RequestMethod" defaultValue={notificationConnector ? notificationConnector.httpMethod : ""} disabled={notificationConnector != null}>
|
<div className="NotificationConnectorItem-Url">
|
||||||
<option value="" disabled hidden>Request Method</option>
|
<select className="NotificationConnectorItem-RequestMethod" defaultValue={notificationConnector ? notificationConnector.httpMethod : ""} disabled={notificationConnector != null} onChange={(e) => {
|
||||||
<option value="GET">GET</option>
|
let x = info;
|
||||||
<option value="POST">POST</option>
|
x.httpMethod = e.currentTarget.value;
|
||||||
</select>
|
setInfo(x);
|
||||||
<input type="url" className="NotificationConnectorItem-Url" placeholder="URL" value={notificationConnector ? notificationConnector.url : ""} disabled={notificationConnector != null} />
|
}}>
|
||||||
<textarea className="NotificationConnectorItem-Body" placeholder="Request-Body" value={notificationConnector ? notificationConnector.body : ""} disabled={notificationConnector != null} />
|
<option value="" disabled hidden>Request Method</option>
|
||||||
|
<option value="GET">GET</option>
|
||||||
|
<option value="POST">POST</option>
|
||||||
|
</select>
|
||||||
|
<input type="url" className="NotificationConnectorItem-RequestUrl" placeholder="URL" defaultValue={notificationConnector ? notificationConnector.url : ""} disabled={notificationConnector != null} onChange={(e) => {
|
||||||
|
let x = info;
|
||||||
|
x.url = e.currentTarget.value;
|
||||||
|
setInfo(x);
|
||||||
|
}} />
|
||||||
|
</div>
|
||||||
|
<textarea className="NotificationConnectorItem-Body" placeholder="Request-Body" defaultValue={notificationConnector ? notificationConnector.body : ""} disabled={notificationConnector != null} onChange={(e) => {
|
||||||
|
let x = info;
|
||||||
|
x.body = e.currentTarget.value;
|
||||||
|
setInfo(x);
|
||||||
|
}} />
|
||||||
{notificationConnector != null ? null :
|
{notificationConnector != null ? null :
|
||||||
(
|
(
|
||||||
<p className="NotificationConnectorItem-Explanation">Explanation Text</p>
|
<p className="NotificationConnectorItem-Explanation">Explanation Text</p>
|
||||||
)}
|
)}
|
||||||
<div className="NotificationConnectorItem-Headers">
|
<div className="NotificationConnectorItem-Headers">
|
||||||
|
{headerElements}
|
||||||
{notificationConnector
|
{notificationConnector
|
||||||
? notificationConnector.headers.map((h: Record<string, string>) =>
|
? notificationConnector.headers.map((h: Record<string, string>) =>
|
||||||
(<HeaderElement record={h} />)
|
(<HeaderElement record={h} disabled={notificationConnector != null}/>)
|
||||||
) :
|
) :
|
||||||
(
|
(
|
||||||
<button className="NotificationConnectorItem-AddHeader" onClick={AddHeader}>Add Header</button>
|
<button className="NotificationConnectorItem-AddHeader" onClick={AddHeader}>Add Header</button>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
{headerElements}
|
|
||||||
</div>
|
</div>
|
||||||
{notificationConnector != null ? null : (
|
{notificationConnector != null ? null : (
|
||||||
<button className="NotificationConnectorItem-Save" onClick={Save}>Save</button>
|
<>
|
||||||
|
<button className="NotificationConnectorItem-Save" onClick={(e) => {
|
||||||
|
setLoading(true);
|
||||||
|
NotificationConnectorFunctions.CreateNotificationConnector(apiUri, info)
|
||||||
|
.finally(() => setLoading(false));
|
||||||
|
}}>Add</button>
|
||||||
|
<Loader loading={loading} style={{width:"40px",height:"40px",margin:"calc(sin(70)*(50% - 40px))"}}/>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</div>);
|
</div>);
|
||||||
}
|
}
|
||||||
|
|
||||||
function HeaderElement({record} : {record: Record<string, string> | null}) : ReactElement {
|
function HeaderElement({record, disabled} : {record: Record<string, string>, disabled?: boolean | null}) : ReactElement {
|
||||||
return <div className="NotificationConnectorItem-Header" key={"newHeader"}>
|
return <div className="NotificationConnectorItem-Header" key={record.name}>
|
||||||
<input type="text" className="NotificationConnectorItem-Header-Key" placeholder="Header-Key" value={record ? record.name : ""} disabled={record != null} />
|
<input type="text" className="NotificationConnectorItem-Header-Key" placeholder="Header-Key" defaultValue={record.name} disabled={disabled?disabled:false} onChange={(e) => record.name = e.currentTarget.value}/>
|
||||||
<input type="text" className="NotificationConnectorItem-Header-Value" placeholder="Header-Value" value={record ? record.value : ""} disabled={record != null} />
|
<input type="text" className="NotificationConnectorItem-Header-Value" placeholder="Header-Value" defaultValue={record.value} disabled={disabled?disabled:false} onChange={(e) => record.value = e.currentTarget.value} />
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
@ -2,7 +2,7 @@
|
|||||||
--background-color: #030304;
|
--background-color: #030304;
|
||||||
--second-background-color: white;
|
--second-background-color: white;
|
||||||
--primary-color: #f5a9b8;
|
--primary-color: #f5a9b8;
|
||||||
--secondary-color: #5bcefa;
|
--secondary-color: #5bcefa;
|
||||||
--blur-background: rgba(245, 169, 184, 0.58);
|
--blur-background: rgba(245, 169, 184, 0.58);
|
||||||
--accent-color: #fff;
|
--accent-color: #fff;
|
||||||
/* --primary-color: green;
|
/* --primary-color: green;
|
||||||
@ -11,6 +11,9 @@
|
|||||||
--accent-color: olive; */
|
--accent-color: olive; */
|
||||||
--topbar-height: 60px;
|
--topbar-height: 60px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
scrollbar-color: var(--primary-color) var(--second-background-color);
|
||||||
|
scroll-behavior: smooth;
|
||||||
|
scrollbar-width: thin;
|
||||||
}
|
}
|
||||||
|
|
||||||
body{
|
body{
|
||||||
|
80
Website/styles/loader.css
Normal file
80
Website/styles/loader.css
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
span[is-loading="done"] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
span[is-loading="loading"] {
|
||||||
|
transform: rotateZ(45deg);
|
||||||
|
perspective: 1000px;
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
color: var(--second-background-color);
|
||||||
|
position: fixed;
|
||||||
|
background-color: var(--secondary-color);
|
||||||
|
background-blend-mode: lighten;
|
||||||
|
}
|
||||||
|
|
||||||
|
span[is-loading="loading"]:before,
|
||||||
|
span[is-loading="loading"]:after {
|
||||||
|
content: '';
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: inherit;
|
||||||
|
height: inherit;
|
||||||
|
border-radius: 50%;
|
||||||
|
transform: rotateX(70deg);
|
||||||
|
animation: 1s spin linear infinite;
|
||||||
|
}
|
||||||
|
span[is-loading="loading"]:after {
|
||||||
|
color: var(--primary-color);
|
||||||
|
transform: rotateY(70deg);
|
||||||
|
animation-delay: .4s;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes rotate {
|
||||||
|
0% {
|
||||||
|
transform: translate(-50%, -50%) rotateZ(0deg);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: translate(-50%, -50%) rotateZ(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes rotateccw {
|
||||||
|
0% {
|
||||||
|
transform: translate(-50%, -50%) rotate(0deg);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: translate(-50%, -50%) rotate(-360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
box-shadow: .2em 0px 0 0px currentcolor;
|
||||||
|
}
|
||||||
|
12% {
|
||||||
|
box-shadow: .2em .2em 0 0 currentcolor;
|
||||||
|
}
|
||||||
|
25% {
|
||||||
|
box-shadow: 0 .2em 0 0px currentcolor;
|
||||||
|
}
|
||||||
|
37% {
|
||||||
|
box-shadow: -.2em .2em 0 0 currentcolor;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
box-shadow: -.2em 0 0 0 currentcolor;
|
||||||
|
}
|
||||||
|
62% {
|
||||||
|
box-shadow: -.2em -.2em 0 0 currentcolor;
|
||||||
|
}
|
||||||
|
75% {
|
||||||
|
box-shadow: 0px -.2em 0 0 currentcolor;
|
||||||
|
}
|
||||||
|
87% {
|
||||||
|
box-shadow: .2em -.2em 0 0 currentcolor;
|
||||||
|
}
|
||||||
|
}
|
@ -1,25 +1,24 @@
|
|||||||
.MangaItem-Connector {
|
|
||||||
flex-grow: 0;
|
|
||||||
height: 14pt;
|
|
||||||
font-size: 12pt;
|
|
||||||
border-radius: 9pt;
|
|
||||||
background-color: var(--primary-color);
|
|
||||||
padding: 2pt 17px;
|
|
||||||
color: black;
|
|
||||||
width: fit-content;
|
|
||||||
margin: 10px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.MangaItem{
|
.MangaItem{
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
background-color: var(--secondary-color);
|
background-color: var(--secondary-color);
|
||||||
width: 180px;
|
width: 200px;
|
||||||
height: 300px;
|
height: 300px;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
margin: 10px 10px;
|
overflow: hidden;
|
||||||
padding: 14px 20px;
|
|
||||||
position: relative;
|
position: relative;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 200px 600px;
|
||||||
|
grid-template-rows: 80px 190px 30px;
|
||||||
|
grid-template-areas:
|
||||||
|
"cover tags"
|
||||||
|
"cover description"
|
||||||
|
"cover footer";
|
||||||
|
margin: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.MangaItem[is-clicked="clicked"]{
|
||||||
|
width: 800px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.MangaItem::after{
|
.MangaItem::after{
|
||||||
@ -33,7 +32,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.MangaItem > * {
|
.MangaItem > * {
|
||||||
z-index: 1;
|
z-index: 2;
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -41,40 +40,62 @@
|
|||||||
background: initial !important;
|
background: initial !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.MangaItem-Connector {
|
||||||
|
grid-area: cover;
|
||||||
|
top: 10px;
|
||||||
|
left: 10px;
|
||||||
|
flex-grow: 0;
|
||||||
|
height: 14pt;
|
||||||
|
font-size: 12pt;
|
||||||
|
border-radius: 9pt;
|
||||||
|
background-color: var(--primary-color);
|
||||||
|
padding: 2pt 17px;
|
||||||
|
color: black;
|
||||||
|
width: fit-content;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
.MangaItem-Name{
|
.MangaItem-Name{
|
||||||
|
grid-area: cover;
|
||||||
width: fit-content;
|
width: fit-content;
|
||||||
font-size: 16pt;
|
font-size: 16pt;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color: white;
|
color: white;
|
||||||
margin: 5px 0 !important;
|
top: 50px;
|
||||||
|
left: 10px;
|
||||||
|
margin: 0;
|
||||||
|
max-width: calc(100% - 20px);
|
||||||
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.MangaItem-Website {
|
.MangaItem-Website {
|
||||||
|
grid-area: cover;
|
||||||
display: block;
|
display: block;
|
||||||
height: 13px;
|
height: 13px;
|
||||||
width: 13px;
|
width: 13px;
|
||||||
position: absolute;
|
margin: 0px 0px auto auto;
|
||||||
top: 12px;
|
top: 12px;
|
||||||
right: 10px;
|
right: 10px;
|
||||||
z-index: 2;
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.MangaItem-Status {
|
.MangaItem-Status {
|
||||||
|
grid-area: cover;
|
||||||
display:block;
|
display:block;
|
||||||
height: 15px;
|
height: 15px;
|
||||||
width: 15px;
|
width: 15px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 15px;
|
top: 14px;
|
||||||
right: 35px;
|
right: 35px;
|
||||||
z-index: 2;
|
|
||||||
box-shadow: rgba(0, 0, 0, 0.16) 0px 1px 10px, rgb(51, 51, 51) 0px 0px 10px 3px;
|
box-shadow: rgba(0, 0, 0, 0.16) 0px 1px 10px, rgb(51, 51, 51) 0px 0px 10px 3px;
|
||||||
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.MangaItem-Status::after {
|
.MangaItem-Status::after {
|
||||||
content: attr(release-status);
|
content: attr(release-status);
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: -2.5pt;
|
||||||
right: 0;
|
right: 0;
|
||||||
|
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
@ -96,7 +117,7 @@
|
|||||||
visibility:visible;
|
visibility:visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
.MangaItem-Status[release-status="Ongoing"]{
|
.MangaItem-Status[release-status="Continuing"]{
|
||||||
background-color: limegreen;
|
background-color: limegreen;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -112,7 +133,7 @@
|
|||||||
background-color: firebrick;
|
background-color: firebrick;
|
||||||
}
|
}
|
||||||
|
|
||||||
.MangaItem-Status[release-status="Upcoming"]{
|
.MangaItem-Status[release-status="Unreleased"]{
|
||||||
background-color: aqua;
|
background-color: aqua;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -124,6 +145,7 @@
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
|
grid-area: cover;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
@ -131,35 +153,38 @@
|
|||||||
z-index: 0;
|
z-index: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.MangaItem p {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.MangaItem[is-clicked="not-clicked"] .MangaItem-Description, .MangaItem[is-clicked="not-clicked"] .MangaItem-Tags, .MangaItem[is-clicked="not-clicked"] .MangaItem-Props{
|
.MangaItem[is-clicked="not-clicked"] .MangaItem-Description, .MangaItem[is-clicked="not-clicked"] .MangaItem-Tags, .MangaItem[is-clicked="not-clicked"] .MangaItem-Props{
|
||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.MangaItem[is-clicked="clicked"] .MangaItem-Description, .MangaItem[is-clicked="clicked"] .MangaItem-Tags, .MangaItem[is-clicked="clicked"] .MangaItem-Props{
|
.MangaItem[is-clicked="clicked"] .MangaItem-Description, .MangaItem[is-clicked="clicked"] .MangaItem-Tags, .MangaItem[is-clicked="clicked"] .MangaItem-Props{
|
||||||
display: block;
|
display: block;
|
||||||
width: 80vw;
|
width: calc(100% - 6px);
|
||||||
background-color: white;
|
background-color: white;
|
||||||
padding: 3px;
|
padding: 0 3px;
|
||||||
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.MangaItem-Tags {
|
.MangaItem-Tags {
|
||||||
|
grid-area: tags;
|
||||||
display: flex !important;
|
display: flex !important;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
scrollbar-width: thin;
|
||||||
|
border-bottom: 1px solid var(--secondary-color);
|
||||||
|
justify-content: start;
|
||||||
|
align-content: start;
|
||||||
}
|
}
|
||||||
|
|
||||||
.MangaItem-Tags > * {
|
.MangaItem-Tags > * {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
width: max-content;
|
width: max-content;
|
||||||
margin: 2px 1px !important;
|
margin: 1px 2px !important;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
color: white;
|
color: white;
|
||||||
padding: 2px;
|
padding: 3px 2px 4px 2px;
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.MangaItem-Tags > * > * {
|
.MangaItem-Tags > * > * {
|
||||||
@ -167,8 +192,13 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.MangaItem-Tags > * > svg {
|
||||||
|
margin: auto 2px !important;
|
||||||
|
}
|
||||||
|
|
||||||
.MangaItem-Tags a, .MangaItem-Tags a:visited {
|
.MangaItem-Tags a, .MangaItem-Tags a:visited {
|
||||||
color: blue;
|
color: var(--primary-color);
|
||||||
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.MangaItem-Author {
|
.MangaItem-Author {
|
||||||
@ -184,12 +214,40 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.MangaItem-Description {
|
.MangaItem-Description {
|
||||||
|
grid-area: description;
|
||||||
color: black;
|
color: black;
|
||||||
max-height: 40vh;
|
max-height: 40vh;
|
||||||
|
scrollbar-width: thin;
|
||||||
|
border-bottom: 1px solid var(--secondary-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.MangaItem-Props {
|
.MangaItem-Props {
|
||||||
|
grid-area: footer;
|
||||||
display: flex !important;
|
display: flex !important;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.MangaItem-Props > * {
|
||||||
|
margin: 3px;
|
||||||
|
border: 1px solid var(--primary-color);
|
||||||
|
border-radius: 3px;
|
||||||
|
background-color: transparent;
|
||||||
|
width: fit-content;
|
||||||
|
}
|
||||||
|
|
||||||
|
.MangaItem-Props-Threshold {
|
||||||
|
color: black;
|
||||||
|
padding: 0 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.MangaItem-Props-Threshold > input {
|
||||||
|
margin: 0 3px;
|
||||||
|
width: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.MangaItem-Props-Threshold-Available{
|
||||||
|
text-decoration: underline;
|
||||||
}
|
}
|
@ -4,10 +4,8 @@
|
|||||||
flex-flow: row;
|
flex-flow: row;
|
||||||
flex-wrap: nowrap;
|
flex-wrap: nowrap;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
height: calc(100vh - 100px);
|
overflow-y: auto;
|
||||||
overflow-y: scroll;
|
margin: 5px 0;
|
||||||
scrollbar-color: var(--accent-color) var(--primary-color);
|
|
||||||
scrollbar-width: thin;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.MangaActionButtons {
|
.MangaActionButtons {
|
||||||
|
69
Website/styles/notificationConnector.css
Normal file
69
Website/styles/notificationConnector.css
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
.NotificationConnectorItem{
|
||||||
|
position: relative;
|
||||||
|
display: grid;
|
||||||
|
width: calc(100% - 10px);
|
||||||
|
grid-template-columns: 40% calc(60% - 10px);
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 5px;
|
||||||
|
border-radius: 5px;
|
||||||
|
grid-template-rows: 30px 30px auto 30px;
|
||||||
|
column-gap: 4px;
|
||||||
|
row-gap: 4px;
|
||||||
|
grid-template-areas:
|
||||||
|
"name name"
|
||||||
|
"url explanation "
|
||||||
|
"headers body"
|
||||||
|
"footer footer";
|
||||||
|
align-items: center;
|
||||||
|
border: 1px solid var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.NotificationConnectorItem p{
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.NotificationConnectorItem-Name{
|
||||||
|
grid-area: name;
|
||||||
|
justify-self: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.NotificationConnectorItem-Url{
|
||||||
|
grid-area: url;
|
||||||
|
}
|
||||||
|
|
||||||
|
.NotificationConnectorItem-Body{
|
||||||
|
grid-area: body;
|
||||||
|
width: calc(100% - 2px);
|
||||||
|
height: max-content;
|
||||||
|
min-height: 100%;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
resize: vertical;
|
||||||
|
}
|
||||||
|
|
||||||
|
.NotificationConnectorItem-Explanation{
|
||||||
|
grid-area: explanation;
|
||||||
|
align-self: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.NotificationConnectorItem-Headers{
|
||||||
|
grid-area: headers;
|
||||||
|
justify-self: flex-end;
|
||||||
|
align-self: flex-end;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.NotificationConnectorItem-Header {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
|
||||||
|
.NotificationConnectorItem-Header > input {
|
||||||
|
width: 48%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.NotificationConnectorItem-Save{
|
||||||
|
grid-area: footer;
|
||||||
|
justify-self: flex-end;
|
||||||
|
padding: 0 15px;
|
||||||
|
}
|
@ -38,6 +38,7 @@
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
top: 40px;
|
top: 40px;
|
||||||
left: 0;
|
left: 0;
|
||||||
width: 100%;
|
width: calc(100% - 30px);
|
||||||
height: calc(100% - 40px);
|
height: calc(100% - 50px);
|
||||||
|
margin: 5px 15px;
|
||||||
}
|
}
|
@ -1,6 +1,6 @@
|
|||||||
#Search{
|
#Search{
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 98vw;
|
width: 100vw;
|
||||||
margin: auto;
|
margin: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -13,6 +13,9 @@
|
|||||||
|
|
||||||
#SearchResults {
|
#SearchResults {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-flow: row wrap;
|
||||||
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
#SearchBox select, #SearchBox button, #SearchBox input {
|
#SearchBox select, #SearchBox button, #SearchBox input {
|
||||||
@ -51,7 +54,7 @@
|
|||||||
#closeSearch {
|
#closeSearch {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
right: 0;
|
right: 10px;
|
||||||
width: 30px;
|
width: 30px;
|
||||||
height: 30px;
|
height: 30px;
|
||||||
filter: brightness(0) saturate(100%) invert(100%) sepia(100%) saturate(1%) hue-rotate(20deg) brightness(103%) contrast(101%);
|
filter: brightness(0) saturate(100%) invert(100%) sepia(100%) saturate(1%) hue-rotate(20deg) brightness(103%) contrast(101%);
|
||||||
|
@ -13,3 +13,13 @@
|
|||||||
margin: 5px;
|
margin: 5px;
|
||||||
filter: invert(100%) sepia(20%) saturate(7480%) hue-rotate(179deg) brightness(121%) contrast(102%);
|
filter: invert(100%) sepia(20%) saturate(7480%) hue-rotate(179deg) brightness(121%) contrast(102%);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#SettingsPopUpBody {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
#SettingsPopUpBody > * {
|
||||||
|
margin: 5px 0;
|
||||||
|
}
|
875
package-lock.json
generated
875
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -9,7 +9,7 @@
|
|||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"react-toggle": "^4.1.3",
|
"react-toggle": "^4.1.3",
|
||||||
"typescript": "^5.6.3",
|
"typescript": "^5.6.3",
|
||||||
"vite": "^5.4.9"
|
"vite": "^6.2.2"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
Loading…
x
Reference in New Issue
Block a user