mirror of
https://github.com/C9Glax/tranga-website.git
synced 2025-09-10 20:08:19 +02:00
Compare commits
3 Commits
84520d8e18
...
42a1e1a2ce
Author | SHA1 | Date | |
---|---|---|---|
42a1e1a2ce | |||
aad18c0195 | |||
1eef710efc |
2
.github/workflows/docker-image-master.yml
vendored
2
.github/workflows/docker-image-master.yml
vendored
@@ -3,8 +3,6 @@ name: Docker Image CI
|
|||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [ "master" ]
|
branches: [ "master" ]
|
||||||
pull_request:
|
|
||||||
branches: [ "master" ]
|
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
@@ -2,7 +2,7 @@ name: Docker Image CI
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [ "cuttingedge" ]
|
branches: [ "vite-react-ts" ]
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
@@ -42,4 +42,4 @@ jobs:
|
|||||||
pull: true
|
pull: true
|
||||||
push: true
|
push: true
|
||||||
tags: |
|
tags: |
|
||||||
glax/tranga-website:dev
|
glax/tranga-website:Server-V2
|
81
README.md
81
README.md
@@ -19,42 +19,45 @@
|
|||||||
<p align="center">
|
<p align="center">
|
||||||
This is the Website for <a href="https://github.com/C9Glax/tranga">Tranga</a> (API)
|
This is the Website for <a href="https://github.com/C9Glax/tranga">Tranga</a> (API)
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<th><img alt="GitHub branch check runs" src="https://img.shields.io/github/check-runs/c9glax/tranga-website/master?label=master"></th>
|
||||||
|
<td><img alt="Last Run" src="https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fapi.github.com%2Frepos%2Fc9glax%2Ftranga-website%2Factions%2Fworkflows%2Fdocker-image-master.yml%2Fruns%3Fper_page%3D1&query=workflow_runs%5B0%5D.created_at&label=Last%20Run"></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th><img alt="GitHub branch check runs" src="https://img.shields.io/github/check-runs/c9glax/tranga-website/cuttingedge?label=cuttingedge"></th>
|
||||||
|
<td><img alt="Last Run" src="https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fapi.github.com%2Frepos%2Fc9glax%2Ftranga-website%2Factions%2Fworkflows%2Fdocker-image-cuttingedge.yml%2Fruns%3Fper_page%3D1&query=workflow_runs%5B0%5D.created_at&label=Last%20Run"></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th><img alt="GitHub branch check runs" src="https://img.shields.io/github/check-runs/c9glax/tranga-website/vite-react-ts?label=vite-react-ts"></th>
|
||||||
|
<td><img alt="Last Run" src="https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fapi.github.com%2Frepos%2Fc9glax%2Ftranga-website%2Factions%2Fworkflows%2Fdocker-image-vite-react-ts.yml%2Fruns%3Fper_page%3D1&query=workflow_runs%5B0%5D.created_at&label=Last%20Run"></td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<!-- TABLE OF CONTENTS -->
|
|
||||||
<details>
|
|
||||||
<summary>Table of Contents</summary>
|
|
||||||
<ol>
|
|
||||||
<li>
|
|
||||||
<a href="#about-the-project">About The Project</a>
|
|
||||||
<ul>
|
|
||||||
<li><a href="#built-with">Built With</a></li>
|
|
||||||
</ul>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a href="#getting-started">Getting Started</a>
|
|
||||||
</li>
|
|
||||||
<li><a href="#roadmap">Roadmap</a></li>
|
|
||||||
<li><a href="#contributing">Contributing</a></li>
|
|
||||||
<li><a href="#license">License</a></li>
|
|
||||||
<li><a href="#acknowledgments">Acknowledgments</a></li>
|
|
||||||
</ol>
|
|
||||||
</details>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<!-- ABOUT THE PROJECT -->
|
<!-- ABOUT THE PROJECT -->
|
||||||
|
## Screenshots
|
||||||
|
|
||||||
|
| Search | SearchResults | Default-View |
|
||||||
|
|----------------------------------------------------------------------------|---------------------------------------------------------------------------------|----------------------------------------------------------------------------|
|
||||||
|
|  |  |  |
|
||||||
|
| Search opens with click on "Add new Manga" | When selecting different connectors, available languages automatically update | Clicking on an Item here will bring up a view with more information |
|
||||||
|
|
||||||
|
|
||||||
## About The Project
|
## About The Project
|
||||||
|
|
||||||
Tranga-Website is the Web-frontend to [Tranga](https://github.com/C9Glax/tranga) (the API). It displays information aquired from Tranga and can create Jobs (Manga-Downloads).
|
Tranga-Website is the Web-frontend to [Tranga](https://github.com/C9Glax/tranga) (the API).
|
||||||
|
|
||||||
### What this does do (and nothing else)
|
### What this does do (and nothing else)
|
||||||
|
|
||||||
This repo makes HTTP-requests to the [Tranga-API](https://github.com/C9Glax/tranga) to display it's present configuration.
|
This project makes HTTP-requests to the [Tranga-API](https://github.com/C9Glax/tranga) to display and modify the present configuration.
|
||||||
|
|
||||||
### Built With
|
## Built With
|
||||||
|
|
||||||
- nginx
|
- nginx
|
||||||
- vite
|
- vite
|
||||||
@@ -67,39 +70,17 @@ This repo makes HTTP-requests to the [Tranga-API](https://github.com/C9Glax/tran
|
|||||||
<!-- GETTING STARTED -->
|
<!-- GETTING STARTED -->
|
||||||
## Getting Started
|
## Getting Started
|
||||||
|
|
||||||
There is a single release:
|
Go to [Tranga](https://github.com/C9Glax/tranga?tab=readme-ov-file#getting-started) and read the README there.
|
||||||
|
|
||||||
|
|
||||||
### Docker
|
|
||||||
|
|
||||||
Download [docker-compose.yaml](https://github.com/C9Glax/tranga-website/blob/cuttingedge/docker-compose.yaml) and configure to your needs.
|
|
||||||
The `docker-compose` also includes [Tranga](https://github.com/C9Glax/tranga) as backend. For its configuration refer to the repo README.
|
|
||||||
|
|
||||||
|
|
||||||
<!-- CONTRIBUTING -->
|
<!-- CONTRIBUTING -->
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
The following is copy & pasted:
|
Go to [Tranga](https://github.com/C9Glax/tranga?tab=readme-ov-file#contributing) and read the README there.
|
||||||
|
|
||||||
Contributions are what make the open source community such an amazing place to learn, inspire, and create. Any contributions you make are **greatly appreciated**.
|
|
||||||
|
|
||||||
If you have a suggestion that would make this better, please fork the repo and create a pull request. You can also simply open an issue with the tag "enhancement".
|
|
||||||
Don't forget to give the project a star! Thanks again!
|
|
||||||
|
|
||||||
1. Fork the Project
|
|
||||||
2. Create your Feature Branch (`git checkout -b feature/AmazingFeature`)
|
|
||||||
3. Commit your Changes (`git commit -m 'Add some AmazingFeature'`)
|
|
||||||
4. Push to the Branch (`git push origin feature/AmazingFeature`)
|
|
||||||
5. Open a Pull Request
|
|
||||||
|
|
||||||
<p align="right">(<a href="#readme-top">back to top</a>)</p>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<!-- LICENSE -->
|
<!-- LICENSE -->
|
||||||
## License
|
## License
|
||||||
|
|
||||||
Distributed under the GNU GPLv3 License. See `LICENSE.txt` for more information.
|
See `LICENSE.txt` for more information.
|
||||||
|
|
||||||
<p align="right">(<a href="#readme-top">back to top</a>)</p>
|
<p align="right">(<a href="#readme-top">back to top</a>)</p>
|
||||||
|
|
||||||
|
BIN
Screenshots/Screenshot 2025-03-14 at 00-53-04 Tranga.png
Normal file
BIN
Screenshots/Screenshot 2025-03-14 at 00-53-04 Tranga.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 157 KiB |
BIN
Screenshots/Screenshot 2025-03-14 at 00-53-15 Tranga.png
Normal file
BIN
Screenshots/Screenshot 2025-03-14 at 00-53-15 Tranga.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 161 KiB |
BIN
Screenshots/Screenshot 2025-03-14 at 00-53-59 Tranga.png
Normal file
BIN
Screenshots/Screenshot 2025-03-14 at 00-53-59 Tranga.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 161 KiB |
BIN
Screenshots/Screenshot 2025-03-14 at 00-54-59 Tranga.png
Normal file
BIN
Screenshots/Screenshot 2025-03-14 at 00-54-59 Tranga.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 454 KiB |
BIN
Screenshots/Screenshot 2025-03-14 at 00-55-13 Tranga.png
Normal file
BIN
Screenshots/Screenshot 2025-03-14 at 00-55-13 Tranga.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 269 KiB |
19
Website/modules/Chapter.tsx
Normal file
19
Website/modules/Chapter.tsx
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import {getData} from "../App";
|
||||||
|
import IChapter from "./interfaces/IChapter";
|
||||||
|
|
||||||
|
export default class Chapter{
|
||||||
|
|
||||||
|
static async GetChapterFromId(apiUri: string, chapterId: string): Promise<IChapter> {
|
||||||
|
if(chapterId === undefined || chapterId === null) {
|
||||||
|
console.error(`chapterId was not provided`);
|
||||||
|
return Promise.reject();
|
||||||
|
}
|
||||||
|
return getData(`${apiUri}/v2/Query/Chapter/${chapterId}`)
|
||||||
|
.then((json) => {
|
||||||
|
//console.info("Got all Manga");
|
||||||
|
const ret = json as IChapter;
|
||||||
|
//console.debug(ret);
|
||||||
|
return (ret);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@@ -4,7 +4,7 @@ import Job from './Job';
|
|||||||
import Icon from '@mdi/react';
|
import Icon from '@mdi/react';
|
||||||
import {mdiCounter, mdiEyeCheck, mdiRun, mdiTrayFull} from '@mdi/js';
|
import {mdiCounter, mdiEyeCheck, mdiRun, mdiTrayFull} from '@mdi/js';
|
||||||
import QueuePopUp from "./QueuePopUp";
|
import QueuePopUp from "./QueuePopUp";
|
||||||
import {JobState, JobType} from "./interfaces/IJob";
|
import {JobState, JobType} from "./interfaces/Jobs/IJob";
|
||||||
|
|
||||||
export default function Footer({connectedToBackend, apiUri} : {connectedToBackend: boolean, apiUri: string}) {
|
export default function Footer({connectedToBackend, apiUri} : {connectedToBackend: boolean, apiUri: string}) {
|
||||||
const [MonitoringJobsCount, setMonitoringJobsCount] = React.useState(0);
|
const [MonitoringJobsCount, setMonitoringJobsCount] = React.useState(0);
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import {deleteData, getData, patchData, postData, putData} from '../App';
|
import {deleteData, getData, patchData, postData, putData} from '../App';
|
||||||
import IJob, {JobState, JobType} from "./interfaces/IJob";
|
import IJob, {JobState, JobType} from "./interfaces/Jobs/IJob";
|
||||||
import IModifyJobRecord from "./interfaces/records/IModifyJobRecord";
|
import IModifyJobRecord from "./interfaces/records/IModifyJobRecord";
|
||||||
|
|
||||||
export default class Job
|
export default class Job
|
||||||
|
@@ -1,6 +1,5 @@
|
|||||||
import IManga from './interfaces/IManga';
|
import IManga from './interfaces/IManga';
|
||||||
import {deleteData, getData, patchData, postData} from '../App';
|
import {deleteData, getData, patchData, postData} from '../App';
|
||||||
import {RefObject} from "react";
|
|
||||||
import IChapter from "./interfaces/IChapter";
|
import IChapter from "./interfaces/IChapter";
|
||||||
|
|
||||||
export default class Manga
|
export default class Manga
|
||||||
|
@@ -1,19 +1,15 @@
|
|||||||
import React, {EventHandler, MouseEventHandler, ReactElement, useEffect, useState} from 'react';
|
import React, {EventHandler, ReactElement, useEffect, useState} from 'react';
|
||||||
import Job from './Job';
|
import Job from './Job';
|
||||||
import '../styles/monitorMangaList.css';
|
import '../styles/monitorMangaList.css';
|
||||||
import IJob, {JobType} from "./interfaces/IJob";
|
import {JobType} from "./interfaces/Jobs/IJob";
|
||||||
import IManga from "./interfaces/IManga";
|
|
||||||
import '../styles/MangaCoverCard.css'
|
import '../styles/MangaCoverCard.css'
|
||||||
|
import DownloadAvailableChaptersJob from "./interfaces/Jobs/DownloadAvailableChaptersJob";
|
||||||
|
import {CoverCard} from "./interfaces/IManga";
|
||||||
|
|
||||||
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, onJobsChanged, connectedToBackend, apiUri, updateList} : {onStartSearch() : void, onJobsChanged: EventHandler<any>, connectedToBackend: boolean, apiUri: string, updateList: Date}) {
|
||||||
const [MonitoringJobs, setMonitoringJobs] = useState<IJob[]>([]);
|
const [MonitoringJobs, setMonitoringJobs] = useState<DownloadAvailableChaptersJob[]>([]);
|
||||||
const [AllManga, setAllManga] = useState<IManga[]>([]);
|
|
||||||
const [joblistUpdateInterval, setJoblistUpdateInterval] = React.useState<number>();
|
const [joblistUpdateInterval, setJoblistUpdateInterval] = React.useState<number>();
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
|
|
||||||
}, [MonitoringJobs]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if(connectedToBackend){
|
if(connectedToBackend){
|
||||||
UpdateMonitoringJobsList();
|
UpdateMonitoringJobsList();
|
||||||
@@ -35,35 +31,24 @@ export default function MonitorJobsList({onStartSearch, onJobsChanged, connected
|
|||||||
return;
|
return;
|
||||||
//console.debug("Updating MonitoringJobsList");
|
//console.debug("Updating MonitoringJobsList");
|
||||||
Job.GetJobsWithType(apiUri, JobType.DownloadAvailableChaptersJob)
|
Job.GetJobsWithType(apiUri, JobType.DownloadAvailableChaptersJob)
|
||||||
.then((jobs) => setMonitoringJobs(jobs));
|
.then((jobs) => setMonitoringJobs(jobs as DownloadAvailableChaptersJob[]));
|
||||||
}
|
}
|
||||||
|
|
||||||
function StartSearchMangaEntry() : ReactElement {
|
function StartSearchMangaEntry() : ReactElement {
|
||||||
return (<div key="monitorMangaEntry.StartSearch" className="monitorMangaEntry" onClick={onStartSearch}>
|
return (<div key="monitorMangaEntry.StartSearch" className="startSearchEntry Manga" onClick={onStartSearch}>
|
||||||
<div className="Manga" key="StartSearch.Manga">
|
<img src="../media/blahaj.png" alt="Blahaj"></img>
|
||||||
<img src="../media/blahaj.png" alt="Blahaj"></img>
|
<div>
|
||||||
<div>
|
<p style={{textAlign: "center", width: "100%"}} className="Manga-name">Add new Manga</p>
|
||||||
<p style={{textAlign: "center", width: "100%"}} className="Manga-name">Add new Manga</p>
|
<p style={{fontSize: "42pt", textAlign: "center"}}>+</p>
|
||||||
<p style={{fontSize: "42pt", textAlign: "center"}}>+</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>);
|
</div>);
|
||||||
}
|
}
|
||||||
|
|
||||||
const DeleteJob : MouseEventHandler = (e) => {
|
|
||||||
const jobId = e.currentTarget.id.slice(e.currentTarget.id.indexOf("-")+1);
|
|
||||||
//console.info(`Pressed ${e.currentTarget.id} => ${jobId}`);
|
|
||||||
Job.DeleteJob(apiUri, jobId).then(() => onJobsChanged(jobId));
|
|
||||||
}
|
|
||||||
|
|
||||||
const StartJob : MouseEventHandler = (e) => {
|
|
||||||
const jobId = e.currentTarget.id.slice(e.currentTarget.id.indexOf("-")+1);
|
|
||||||
//console.info(`Pressed ${e.currentTarget.id} => ${jobId}`);
|
|
||||||
Job.StartJob(apiUri, jobId);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div id="MonitorMangaList">
|
<div id="MonitorMangaList">
|
||||||
{StartSearchMangaEntry()}
|
<StartSearchMangaEntry />
|
||||||
</div>)
|
{MonitoringJobs.map((MonitoringJob) =>
|
||||||
|
<CoverCard apiUri={apiUri} mangaId={MonitoringJob.mangaId} key={MonitoringJob.mangaId} />
|
||||||
|
)}
|
||||||
|
</div>);
|
||||||
}
|
}
|
@@ -1,10 +1,10 @@
|
|||||||
import React, {useEffect, useState} from 'react';
|
import React, {useEffect, useState} from 'react';
|
||||||
import IJob, {JobState} from "./interfaces/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';
|
||||||
import Job from "./Job";
|
import Job from "./Job";
|
||||||
import IManga, {QueueItem} from "./interfaces/IManga";
|
import DownloadSingleChapterJob from "./interfaces/Jobs/DownloadSingleChapterJob";
|
||||||
import Manga from "./Manga";
|
import { ItemDownloadSingleChapterJob } from "./interfaces/IManga";
|
||||||
|
|
||||||
export default function QueuePopUp({connectedToBackend, children, apiUri} : {connectedToBackend: boolean, children: JSX.Element[], apiUri: string}) {
|
export default function QueuePopUp({connectedToBackend, children, apiUri} : {connectedToBackend: boolean, children: JSX.Element[], apiUri: string}) {
|
||||||
|
|
||||||
@@ -33,17 +33,17 @@ export default function QueuePopUp({connectedToBackend, children, apiUri} : {con
|
|||||||
|
|
||||||
function UpdateMonitoringJobsList(){
|
function UpdateMonitoringJobsList(){
|
||||||
Job.GetJobsInState(apiUri, JobState.Waiting)
|
Job.GetJobsInState(apiUri, JobState.Waiting)
|
||||||
.then((jobs:IJob[]) => {
|
.then((jobs: IJob[]) => {
|
||||||
//console.log(StandbyJobs)
|
//console.log(jobs);
|
||||||
setWaitingJobs(jobs);
|
return jobs;
|
||||||
//console.log(StandbyJobs)
|
})
|
||||||
});
|
.then(setWaitingJobs);
|
||||||
Job.GetJobsInState(apiUri, JobState.Running)
|
Job.GetJobsInState(apiUri, JobState.Running)
|
||||||
.then((jobs:IJob[]) =>{
|
.then((jobs: IJob[]) => {
|
||||||
//console.log(StandbyJobs)
|
//console.log(jobs);
|
||||||
setRunningJobs(jobs);
|
return jobs;
|
||||||
//console.log(StandbyJobs)
|
})
|
||||||
});
|
.then(setRunningJobs);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (<>
|
return (<>
|
||||||
@@ -58,6 +58,12 @@ export default function QueuePopUp({connectedToBackend, children, apiUri} : {con
|
|||||||
onClick={() => setShowQueuePopup(false)}/>
|
onClick={() => setShowQueuePopup(false)}/>
|
||||||
</div>
|
</div>
|
||||||
<div id="QueuePopUpBody" className="popupBody">
|
<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} />)}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{WaitingJobs.filter(j => j.jobType == JobType.DownloadSingleChapterJob).map(j => <ItemDownloadSingleChapterJob apiUri={apiUri} job={j as DownloadSingleChapterJob} key={j.jobId} />)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
: <></>
|
: <></>
|
||||||
|
@@ -2,10 +2,11 @@ import React, {ChangeEventHandler, EventHandler, useEffect, useState} from 'reac
|
|||||||
import {MangaConnector} from "./MangaConnector";
|
import {MangaConnector} from "./MangaConnector";
|
||||||
import IMangaConnector from "./interfaces/IMangaConnector";
|
import IMangaConnector from "./interfaces/IMangaConnector";
|
||||||
import {isValidUri} from "../App";
|
import {isValidUri} from "../App";
|
||||||
import IManga, {SearchResult} from "./interfaces/IManga";
|
import IManga, {ExtendedInfo} from "./interfaces/IManga";
|
||||||
import '../styles/search.css';
|
import '../styles/search.css';
|
||||||
import '../styles/MangaSearchResult.css'
|
import '../styles/ExtendedInfo.css'
|
||||||
import SearchFunctions from "./SearchFunctions";
|
import SearchFunctions from "./SearchFunctions";
|
||||||
|
import Job from "./Job";
|
||||||
|
|
||||||
export default function Search({apiUri, jobInterval, onJobsChanged, closeSearch} : {apiUri: string, jobInterval: Date, onJobsChanged: (internalId: string) => void, closeSearch(): void}) {
|
export default function Search({apiUri, jobInterval, onJobsChanged, closeSearch} : {apiUri: string, jobInterval: Date, onJobsChanged: (internalId: string) => void, closeSearch(): void}) {
|
||||||
const [mangaConnectors, setConnectors] = useState<IMangaConnector[]>();
|
const [mangaConnectors, setConnectors] = useState<IMangaConnector[]>();
|
||||||
@@ -90,14 +91,15 @@ export default function Search({apiUri, jobInterval, onJobsChanged, closeSearch}
|
|||||||
<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}></input>
|
||||||
<select id="Searchbox-Connector" value={selectedConnector === undefined ? "" : selectedConnector.name} onChange={selectedConnectorChanged}>
|
<select id="Searchbox-Connector" value={selectedConnector === undefined ? "" : selectedConnector.name} onChange={selectedConnectorChanged}>
|
||||||
<option value="" disabled hidden>Select</option>
|
{mangaConnectors === undefined ? <option value="Loading">Loading</option> : <option value="" disabled hidden>Select</option>}
|
||||||
{mangaConnectors === undefined
|
{mangaConnectors === undefined
|
||||||
? <option value="Loading">Loading</option>
|
? 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}>
|
||||||
|
{mangaConnectors === undefined ? <option value="Loading">Loading</option> : <option value="" disabled hidden>Select Connector</option>}
|
||||||
{selectedConnector === undefined
|
{selectedConnector === undefined
|
||||||
? <option value="" disabled hidden>Select Connector</option>
|
? 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}>Search</button>
|
||||||
@@ -106,7 +108,14 @@ export default function Search({apiUri, jobInterval, onJobsChanged, closeSearch}
|
|||||||
<div id="SearchResults">
|
<div id="SearchResults">
|
||||||
{searchResults === undefined
|
{searchResults === undefined
|
||||||
? <p></p>
|
? <p></p>
|
||||||
: searchResults.map(result => SearchResult(apiUri, result, jobInterval, onJobsChanged))}
|
: searchResults.map(result =>
|
||||||
|
<ExtendedInfo key={"Searchresult-"+result.mangaId} apiUri={apiUri} manga={result} actions={[
|
||||||
|
<button className="Manga-AddButton" onClick={() => {
|
||||||
|
Job.CreateDownloadAvailableChaptersJob(apiUri, result.mangaId, jobInterval.getTime()).then(() => onJobsChanged(result.mangaId));
|
||||||
|
}}>Monitor</button>
|
||||||
|
]}/>
|
||||||
|
)
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>);
|
</div>);
|
||||||
}
|
}
|
@@ -9,7 +9,7 @@ export function LoadFrontendSettings(): IFrontendSettings {
|
|||||||
const cookies = new Cookies();
|
const cookies = new Cookies();
|
||||||
return {
|
return {
|
||||||
jobInterval: cookies.get('jobInterval') === undefined
|
jobInterval: cookies.get('jobInterval') === undefined
|
||||||
? new Date(0,0,0,3)
|
? new Date(Date.parse("1970-01-01T03:00:00.000Z"))
|
||||||
: cookies.get('jobInterval'),
|
: cookies.get('jobInterval'),
|
||||||
apiUri: cookies.get('apiUri') === undefined
|
apiUri: cookies.get('apiUri') === undefined
|
||||||
? `${window.location.protocol}//${window.location.host}/api`
|
? `${window.location.protocol}//${window.location.host}/api`
|
||||||
|
@@ -1,12 +1,13 @@
|
|||||||
import Manga from "../Manga";
|
import Manga from "../Manga";
|
||||||
import React, {ReactElement, ReactEventHandler} from "react";
|
import React, {ReactElement, ReactEventHandler, useEffect} from "react";
|
||||||
import Icon from '@mdi/react';
|
import Icon from '@mdi/react';
|
||||||
import { mdiTagTextOutline, mdiAccountEdit, mdiLinkVariant } from '@mdi/js';
|
import { mdiTagTextOutline, mdiAccountEdit, mdiLinkVariant } from '@mdi/js';
|
||||||
import MarkdownPreview from '@uiw/react-markdown-preview';
|
import MarkdownPreview from '@uiw/react-markdown-preview';
|
||||||
import IJob from "./IJob";
|
|
||||||
import {AuthorElement} from "./IAuthor";
|
import {AuthorElement} from "./IAuthor";
|
||||||
import Job from "../Job";
|
|
||||||
import {LinkElement} from "./ILink";
|
import {LinkElement} from "./ILink";
|
||||||
|
import DownloadSingleChapterJob from "./Jobs/DownloadSingleChapterJob";
|
||||||
|
import IChapter from "./IChapter";
|
||||||
|
import Chapter from "../Chapter";
|
||||||
|
|
||||||
export default interface IManga{
|
export default interface IManga{
|
||||||
mangaId: string;
|
mangaId: string;
|
||||||
@@ -34,28 +35,66 @@ export enum MangaReleaseStatus {
|
|||||||
Unreleased = "Unreleased",
|
Unreleased = "Unreleased",
|
||||||
}
|
}
|
||||||
|
|
||||||
export function CoverCard(apiUri: string, manga: IManga) : ReactElement {
|
export const defaultManga: IManga = {
|
||||||
return(
|
altTitleIds: [],
|
||||||
<div className="Manga" key={manga.mangaId}>
|
authorIds: [],
|
||||||
<img src="../../media/blahaj.png" alt="Manga Cover"></img>
|
connectorId: "",
|
||||||
<div>
|
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);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
Manga.GetMangaById(apiUri, mangaId).then(setContent);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
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>
|
<p className="pill connector-name">{manga.mangaConnectorId}</p>
|
||||||
<div className="Manga-status" release-status={manga.releaseStatus}></div>
|
<div className="Manga-status" release-status={manga.releaseStatus}></div>
|
||||||
<p className="Manga-name">{manga.name}</p>
|
<p className="Manga-name">{manga.name}</p>
|
||||||
</div>
|
</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>);
|
</div>);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SearchResult(apiUri: string, manga: IManga, interval: Date, onJobsChanged: (internalId: string) => void) : ReactElement {
|
export function ExtendedInfo({apiUri, manga, actions} : {apiUri: string, manga: IManga, actions: ReactElement[]}) : ReactElement {
|
||||||
const MangaCover : ReactEventHandler<HTMLImageElement> = (e) => {
|
const MangaCover : ReactEventHandler<HTMLImageElement> = (e) => {
|
||||||
console.log(manga.mangaId);
|
|
||||||
if(e.currentTarget.src != Manga.GetMangaCoverImageUrl(apiUri, manga.mangaId, e.currentTarget))
|
if(e.currentTarget.src != Manga.GetMangaCoverImageUrl(apiUri, manga.mangaId, e.currentTarget))
|
||||||
e.currentTarget.src = Manga.GetMangaCoverImageUrl(apiUri, manga.mangaId, e.currentTarget);
|
e.currentTarget.src = Manga.GetMangaCoverImageUrl(apiUri, manga.mangaId, e.currentTarget);
|
||||||
}
|
}
|
||||||
|
|
||||||
return(
|
return(
|
||||||
<div className="SearchResult" key={manga.mangaId}>
|
<div className="SearchResult" key={manga.mangaId}>
|
||||||
<img src={Manga.GetMangaCoverImageUrl(apiUri, manga.mangaId, undefined)} alt="Manga Cover" onLoad={MangaCover}></img>
|
<img src={Manga.GetMangaCoverImageUrl(apiUri, manga.mangaId, undefined)} alt="Manga Cover" onLoad={MangaCover} onResize={MangaCover}></img>
|
||||||
<p className="connector-name">{manga.mangaConnectorId}</p>
|
<p className="connector-name">{manga.mangaConnectorId}</p>
|
||||||
<div className="Manga-status" release-status={manga.releaseStatus}></div>
|
<div className="Manga-status" release-status={manga.releaseStatus}></div>
|
||||||
<p className="Manga-name"><a href={manga.websiteUrl}>{manga.name}<img src="../../media/link.svg"
|
<p className="Manga-name"><a href={manga.websiteUrl}>{manga.name}<img src="../../media/link.svg"
|
||||||
@@ -79,30 +118,45 @@ export function SearchResult(apiUri: string, manga: IManga, interval: Date, onJo
|
|||||||
</div>
|
</div>
|
||||||
<MarkdownPreview className="Manga-description" source={manga.description}
|
<MarkdownPreview className="Manga-description" source={manga.description}
|
||||||
style={{backgroundColor: "transparent", color: "black"}}/>
|
style={{backgroundColor: "transparent", color: "black"}}/>
|
||||||
<button className="Manga-AddButton" onClick={() => {
|
<div className="Manga-actions">
|
||||||
Job.CreateDownloadAvailableChaptersJob(apiUri, manga.mangaId, interval.getMilliseconds()).then(() => onJobsChanged(manga.mangaId));
|
{actions.map((p, i) => <div key={i}>{p}</div>)}
|
||||||
}}>Monitor
|
</div>
|
||||||
</button>
|
|
||||||
</div>);
|
</div>);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function QueueItem(apiUri: string, manga: IManga, job: IJob, triggerUpdate: () => void){
|
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 (
|
return (
|
||||||
<div className="QueueJob" key={"QueueJob-" + job.jobId}>
|
<div className="DownloadSingleChapterJob" key={"DownloadSingleChapterJob-" + job.jobId}>
|
||||||
<img src="../../media/blahaj.png" alt="Manga Cover"></img>
|
<img src={manga ? Manga.GetMangaCoverImageUrl(apiUri, manga.mangaId, undefined) : ""} alt="Manga Cover" onLoad={MangaCover} onResize={MangaCover}></img>
|
||||||
<p className="QueueJob-Name">{manga.name}</p>
|
<p className="DownloadSingleChapterJob-Name">{manga ? manga.name : job.chapterId}</p>
|
||||||
<p className="QueueJob-JobType">{job.jobType}</p>
|
<p className="DownloadSingleChapterJob-Title">
|
||||||
<div className="QueueJob-actions">
|
{chapter ? "Vol." + chapter.volumeNumber + " Ch." + chapter.chapterNumber + ": " + chapter.title : "loading"}
|
||||||
<button className="QueueJob-Cancel"
|
<a href={chapter ? chapter.url : ""}>
|
||||||
onClick={() => Job.StopJob(apiUri, job.jobId).then(triggerUpdate)}>Cancel
|
<img src="../../media/link.svg" alt=""/>
|
||||||
</button>
|
</a>
|
||||||
{job.parentJobId != null
|
</p>
|
||||||
? <button className="QueueJob-Cancel"
|
|
||||||
onClick={() => Job.StopJob(apiUri, job.parentJobId!).then(triggerUpdate)}>Cancel all
|
|
||||||
related</button>
|
|
||||||
: <></>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
@@ -0,0 +1,5 @@
|
|||||||
|
import IJob from "./IJob";
|
||||||
|
|
||||||
|
export default interface DownloadAvailableChaptersJob extends IJob {
|
||||||
|
mangaId: string;
|
||||||
|
}
|
5
Website/modules/interfaces/Jobs/DownloadMangaCoverJob.ts
Normal file
5
Website/modules/interfaces/Jobs/DownloadMangaCoverJob.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import IJob from "./IJob";
|
||||||
|
|
||||||
|
export default interface DownloadMangaCoverJob extends IJob {
|
||||||
|
mangaId: string;
|
||||||
|
}
|
@@ -0,0 +1,5 @@
|
|||||||
|
import IJob from "./IJob";
|
||||||
|
|
||||||
|
export default interface DownloadSingleChapterJob extends IJob {
|
||||||
|
chapterId: string;
|
||||||
|
}
|
6
Website/modules/interfaces/Jobs/MoveFileOrFolderJob.ts
Normal file
6
Website/modules/interfaces/Jobs/MoveFileOrFolderJob.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import IJob from "./IJob";
|
||||||
|
|
||||||
|
export default interface MoveFileOrFolderJob extends IJob {
|
||||||
|
fromLocation: string;
|
||||||
|
toLocation: string;
|
||||||
|
}
|
5
Website/modules/interfaces/Jobs/RetrieveChaptersJob.ts
Normal file
5
Website/modules/interfaces/Jobs/RetrieveChaptersJob.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import IJob from "./IJob";
|
||||||
|
|
||||||
|
export default interface RetrieveChaptersJob extends IJob {
|
||||||
|
mangaId: string;
|
||||||
|
}
|
@@ -0,0 +1,5 @@
|
|||||||
|
import IJob from "./IJob";
|
||||||
|
|
||||||
|
export default interface UpdateFilesDownloadedJob extends IJob {
|
||||||
|
mangaId: string;
|
||||||
|
}
|
5
Website/modules/interfaces/Jobs/UpdateMetadataJob.ts
Normal file
5
Website/modules/interfaces/Jobs/UpdateMetadataJob.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import IJob from "./IJob";
|
||||||
|
|
||||||
|
export default interface UpdateMetadataJob extends IJob {
|
||||||
|
mangaId: string;
|
||||||
|
}
|
@@ -66,10 +66,12 @@
|
|||||||
padding: 0;
|
padding: 0;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
max-height: 100%;
|
||||||
|
overflow-y: scroll;
|
||||||
}
|
}
|
||||||
|
|
||||||
.SearchResult > .Manga-tags p {
|
.SearchResult > .Manga-tags p {
|
||||||
margin: 0 2px;
|
margin: 2px;
|
||||||
padding: 5px;
|
padding: 5px;
|
||||||
font-size: 10pt;
|
font-size: 10pt;
|
||||||
height: fit-content;
|
height: fit-content;
|
||||||
@@ -102,8 +104,7 @@
|
|||||||
overflow-y: scroll;
|
overflow-y: scroll;
|
||||||
}
|
}
|
||||||
|
|
||||||
.SearchResult > .Manga-AddButton {
|
.SearchResult > .Manga-actions button {
|
||||||
grid-area: button;
|
|
||||||
background-color: white;
|
background-color: white;
|
||||||
border: 1px solid var(--primary-color);
|
border: 1px solid var(--primary-color);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
@@ -112,6 +113,12 @@
|
|||||||
padding: 5px 10px;
|
padding: 5px 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.SearchResult > .Manga-actions {
|
||||||
|
grid-area: button;
|
||||||
|
padding: 0;
|
||||||
|
margin: 5px 0 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
.SearchResult > .Manga-AddButton:hover {
|
.SearchResult > .Manga-AddButton:hover {
|
||||||
background-color: #eee;
|
background-color: #eee;
|
||||||
}
|
}
|
||||||
@@ -126,6 +133,6 @@
|
|||||||
bottom: 7px;
|
bottom: 7px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.monitorMangaEntry {
|
.startSearchEntry {
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
@@ -32,6 +32,10 @@
|
|||||||
z-index: 0;
|
z-index: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.startSearchEntry::after{
|
||||||
|
background: initial !important;
|
||||||
|
}
|
||||||
|
|
||||||
.Manga-name{
|
.Manga-name{
|
||||||
width: fit-content;
|
width: fit-content;
|
||||||
font-size: 16pt;
|
font-size: 16pt;
|
||||||
@@ -102,7 +106,7 @@
|
|||||||
background-color: gray;
|
background-color: gray;
|
||||||
}
|
}
|
||||||
|
|
||||||
.Manga img {
|
.Manga > img {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
@@ -117,7 +121,7 @@
|
|||||||
margin: 2px 0;
|
margin: 2px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.Manga > div {
|
.Manga > .SimpleCover {
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -125,3 +129,15 @@
|
|||||||
left: 0;
|
left: 0;
|
||||||
top: 0;
|
top: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
div[extended-info="no"]{
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
div[extended-info="yes"]{
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
@@ -4,7 +4,7 @@
|
|||||||
flex-flow: row;
|
flex-flow: row;
|
||||||
flex-wrap: nowrap;
|
flex-wrap: nowrap;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
height: 100%;
|
height: calc(100vh - 100px);
|
||||||
overflow-y: scroll;
|
overflow-y: scroll;
|
||||||
scrollbar-color: var(--accent-color) var(--primary-color);
|
scrollbar-color: var(--accent-color) var(--primary-color);
|
||||||
scrollbar-width: thin;
|
scrollbar-width: thin;
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
#QueuePopUp #QueuePopUpBody {
|
#QueuePopUp #QueuePopUpBody {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
color: black;
|
||||||
}
|
}
|
||||||
|
|
||||||
#QueuePopUp #QueuePopUpBody > * {
|
#QueuePopUp #QueuePopUpBody > * {
|
||||||
|
Reference in New Issue
Block a user