60 Commits
0.7 ... 1.1.1

Author SHA1 Message Date
732c2f119c More logging 2023-05-26 15:09:26 +02:00
81638f4b4a Path.join joins paths ya know 2023-05-26 14:51:11 +02:00
c547aa6422 favicon <3 2023-05-26 14:49:17 +02:00
d80980512e #28 is a pain in the buttcheeks 2023-05-26 14:47:13 +02:00
f9f802155d #28 why was there a '!' 2023-05-26 14:43:47 +02:00
eef0955009 #28 wrong filesnames 2023-05-26 14:36:02 +02:00
ec25900ac0 resolves #29 start manual execution 2023-05-26 14:32:08 +02:00
e5fe14a09e #28 2023-05-26 14:31:34 +02:00
5dc91095f8 #28 2023-05-26 14:30:52 +02:00
985ac8fc7a Fix #28 coverimages 2023-05-26 14:07:11 +02:00
c9537a9963 #24 2023-05-26 13:39:42 +02:00
4fd3c03804 Styling 2023-05-26 13:30:20 +02:00
a1e9dd0232 resolves #27 Foldernames ending in '.' 2023-05-25 22:22:57 +02:00
aa1f9b1b56 background fade 2023-05-25 22:17:47 +02:00
6069578b6e style choices 2023-05-25 22:15:06 +02:00
a84b768e24 design choices 2023-05-25 22:05:29 +02:00
d1a21af15d resolves #26 2023-05-25 21:58:45 +02:00
7423ae6ace Update README.md 2023-05-25 18:36:21 +02:00
3aa7ba9d96 screenshots 2023-05-25 18:28:43 +02:00
fdbb4570be Adjusted settings style 2023-05-25 18:18:31 +02:00
b643a0c2a9 Fix Wrong API uri for GetRunningTasks
Added GetQueue function

Added display for running, queued, total tasks
2023-05-25 18:09:18 +02:00
6fa6f897aa More legal characters 2023-05-25 17:34:24 +02:00
2bfab0298d border-radius 2023-05-25 17:33:55 +02:00
147a20385b illegal filenames 2023-05-25 16:55:58 +02:00
afa18d6a2c Illlegal characters on linux 2023-05-25 16:47:24 +02:00
66980eef23 Position publication viewer always withing display 2023-05-25 16:05:54 +02:00
65f468a30a popup position now "fixed"
changed publicationviewer width
2023-05-25 16:05:40 +02:00
a91c33ee4f image sizing 2023-05-25 15:50:00 +02:00
f39482fe4c Corrected image path in publication preview 2023-05-25 15:48:38 +02:00
41f47b4d6b styling 2023-05-25 15:47:59 +02:00
be40091102 Publication background fade 2023-05-25 15:41:24 +02:00
665092be6a image scaling 2023-05-25 15:40:03 +02:00
653cb699d0 Removed Sidebar
Moved settings tab to popup
Added footer
2023-05-25 15:34:10 +02:00
8dbc5446ad depends_on compose 2023-05-25 14:46:05 +02:00
750df4ed52 Wrong return value 2023-05-25 14:38:43 +02:00
4772ae0756 No unnecessary downloads of covers if they already exist 2023-05-25 14:35:33 +02:00
23f703d5a5 imageCache readonly for website 2023-05-25 14:30:33 +02:00
6aa0ea277b #22 2023-05-25 14:28:56 +02:00
780df1cd6e Created Image-Cache 2023-05-25 14:25:23 +02:00
0b7da2e9cb Merge remote-tracking branch 'origin/master'
# Conflicts:
#	Website/interaction.js
2023-05-25 13:59:06 +02:00
01a059d26b Base 64 images #22 2023-05-25 13:58:54 +02:00
a8dbece237 Base 64 images #22 2023-05-25 13:58:10 +02:00
5efa00e059 Added field posterBase64 to Publication #22 2023-05-25 13:50:48 +02:00
02075ed1b1 Renamed RequestType Cover to CoverUrl 2023-05-25 13:50:08 +02:00
fabd16ccea Remove unnecessary output from dockerfile 2023-05-25 13:49:29 +02:00
79928075b0 docker-compose.yaml 2023-05-25 10:49:24 +02:00
9b8eb6a197 add seconds field to addtask recurrence 2023-05-25 10:47:12 +02:00
1d263ef45a Configurable API-location 2023-05-25 10:42:19 +02:00
e0877add30 Paths for Linux 2023-05-25 10:25:24 +02:00
046cad8072 Dockerfile for Tranga 2023-05-25 10:25:11 +02:00
b2ce55be96 Port 2023-05-25 01:25:21 +02:00
a6e9013495 Latest alpine image 2023-05-25 00:05:31 +02:00
14c69631a6 Corrected port 2023-05-24 23:53:39 +02:00
ccc4e42a49 Komga update can now be configured in seconds 2023-05-24 23:53:32 +02:00
d6e75fda31 Fixed empty returns if some value were null 2023-05-24 23:52:40 +02:00
fc89537f63 Fixed Authorization on redirect 2023-05-24 23:52:25 +02:00
fd3423d03c Correct Port 2023-05-24 22:56:15 +02:00
878f77766f Fix CORS 2023-05-24 22:56:10 +02:00
08001fd684 SSL cert error 2023-05-24 22:55:32 +02:00
e2917d2f2e Changed CORS policy allow all origins
Added Dockerfile to website
Changed Ports
2023-05-24 22:30:11 +02:00
28 changed files with 777 additions and 339 deletions

13
Dockerfile Normal file
View File

@ -0,0 +1,13 @@
# syntax=docker/dockerfile:1
FROM mcr.microsoft.com/dotnet/sdk:7.0 as build-env
WORKDIR /src
COPY . /src/
RUN dotnet restore Tranga-API/Tranga-API.csproj
RUN dotnet publish -c Release -o /publish
FROM mcr.microsoft.com/dotnet/aspnet:7.0 as runtime
WORKDIR /publish
COPY --from=build-env /publish .
EXPOSE 80
ENTRYPOINT ["dotnet", "/publish/Tranga-API.dll"]

View File

@ -30,6 +30,9 @@
<li><a href="#built-with">Built With</a></li>
</ul>
</li>
<li>
<a href="#screenshots">Screenshots</a>
</li>
<li>
<a href="#getting-started">Getting Started</a>
<ul>
@ -64,24 +67,43 @@ That is why I wanted to create my own project, in a language I understand, and t
<p align="right">(<a href="#readme-top">back to top</a>)</p>
### Built With
- .NET-Core
- Newtonsoft.JSON
- Love <3
- Love <3 Blåhaj 🦈
<p align="right">(<a href="#readme-top">back to top</a>)</p>
## Screenshots
![image](screenshots/overview.png)
![image](screenshots/addtask.png)
| ![image](screenshots/settings.png) | ![image](screenshots/publication-description.png) |
|-----------------------------------:|:-------------------------------------------------:|
<p align="right">(<a href="#readme-top">back to top</a>)</p>
<!-- GETTING STARTED -->
## Getting Started
To use head over to [releases](https://git.bernloehr.eu/glax/Tranga/releases) and download a release.
There is two release types:
A CLI will guide you through setup.
- CLI
- Docker
### CLI
Head over to [releases](https://git.bernloehr.eu/glax/Tranga/releases) and download. The CLI will guide you through setup.
### Docker
Download [docker-compose.yaml](https://git.bernloehr.eu/glax/Tranga/src/branch/master/docker-compose.yaml) and configure to your needs.
Wherever you are mounting `/usr/share/Tranga-API` you also need to mount that same path + `/imageCache` in the webserver container.
### Prerequisites
@ -90,7 +112,7 @@ A CLI will guide you through setup.
<!-- ROADMAP -->
## Roadmap
- [ ] Web-UI #1
- [x] Web-UI #1
- [ ] More Connectors
- [ ] Manganato #2
- [ ] ?

View File

@ -1,20 +0,0 @@
FROM mcr.microsoft.com/dotnet/aspnet:7.0 AS base
WORKDIR /app
EXPOSE 80
EXPOSE 443
FROM mcr.microsoft.com/dotnet/sdk:7.0 AS build
WORKDIR /src
COPY ["Tranga-API/Tranga-API.csproj", "Tranga-API/"]
RUN dotnet restore "Tranga-API/Tranga-API.csproj"
COPY . .
WORKDIR "/src/Tranga-API"
RUN dotnet build "Tranga-API.csproj" -c Release -o /app/build
FROM build AS publish
RUN dotnet publish "Tranga-API.csproj" -c Release -o /app/publish /p:UseAppHost=false
FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "Tranga-API.dll"]

View File

@ -1,28 +1,35 @@
using System.Runtime.InteropServices;
using Logging;
using Tranga;
string applicationFolderPath =
Path.Join(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), "Tranga-API");
string downloadFolderPath = Path.Join(applicationFolderPath, "Manga");
string logsFolderPath = Path.Join(applicationFolderPath, "logs");
string applicationFolderPath = Path.Join(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), "Tranga-API");
string downloadFolderPath = RuntimeInformation.IsOSPlatform(OSPlatform.Linux) ? "/Manga" : Path.Join(applicationFolderPath, "Manga");
string logsFolderPath = RuntimeInformation.IsOSPlatform(OSPlatform.Linux) ? "/var/logs/Tranga" : Path.Join(applicationFolderPath, "logs");
string logFilePath = Path.Join(logsFolderPath, $"log-{DateTime.Now:dd-M-yyyy-HH-mm-ss}.txt");
string settingsFilePath = Path.Join(applicationFolderPath, "settings.json");
Directory.CreateDirectory(applicationFolderPath);
Directory.CreateDirectory(logsFolderPath);
Logger logger = new(new[] { Logger.LoggerType.FileLogger, Logger.LoggerType.ConsoleLogger }, Console.Out, Console.Out.Encoding, logFilePath);
Console.WriteLine($"Logfile-Path: {logFilePath}");
Console.WriteLine($"Settings-File-Path: {settingsFilePath}");
logger.WriteLine("Tranga", "Loading settings.");
Logger logger = new(new[] { Logger.LoggerType.FileLogger }, null, null, logFilePath);
logger.WriteLine("Tranga_CLI", "Loading Taskmanager.");
TrangaSettings settings;
if (File.Exists(settingsFilePath))
settings = TrangaSettings.LoadSettings(settingsFilePath);
else
settings = new TrangaSettings(downloadFolderPath, applicationFolderPath, null);
Directory.CreateDirectory(settings.workingDirectory);
Directory.CreateDirectory(settings.downloadLocation);
Directory.CreateDirectory(settings.coverImageCache);
logger.WriteLine("Tranga",$"Application-Folder: {settings.workingDirectory}");
logger.WriteLine("Tranga",$"Settings-File-Path: {settings.settingsFilePath}");
logger.WriteLine("Tranga",$"Download-Folder-Path: {settings.downloadLocation}");
logger.WriteLine("Tranga",$"Logfile-Path: {logFilePath}");
logger.WriteLine("Tranga",$"Image-Cache-Path: {settings.coverImageCache}");
logger.WriteLine("Tranga", "Loading Taskmanager.");
TaskManager taskManager = new (settings, logger);
var builder = WebApplication.CreateBuilder(args);
@ -36,7 +43,7 @@ builder.Services.AddCors(options =>
options.AddPolicy(name: corsHeader,
policy =>
{
policy.WithOrigins("http://localhost", "http://127.0.0.1", "http://localhost:63342");
policy.AllowAnyOrigin();
policy.WithMethods("GET", "POST", "DELETE");
});
});
@ -82,23 +89,45 @@ app.MapDelete("/Tasks/Delete", (string taskType, string? connectorName, string?
app.MapGet("/Tasks/Get", (string taskType, string? connectorName, string? searchString) =>
{
TrangaTask.Task task = Enum.Parse<TrangaTask.Task>(taskType);
if (searchString is null)
return taskManager.GetAllTasks().Where(tTask => tTask.task == task && tTask.connectorName == connectorName);
else
return taskManager.GetAllTasks().Where(tTask =>
tTask.task == task && tTask.connectorName == connectorName && tTask.ToString()
.Contains(searchString, StringComparison.InvariantCultureIgnoreCase));
try
{
TrangaTask.Task task = Enum.Parse<TrangaTask.Task>(taskType);
if (searchString is null || connectorName is null)
return taskManager.GetAllTasks().Where(tTask => tTask.task == task);
else
return taskManager.GetAllTasks().Where(tTask =>
tTask.task == task && tTask.connectorName == connectorName && tTask.ToString()
.Contains(searchString, StringComparison.InvariantCultureIgnoreCase));
}
catch (ArgumentException)
{
return Array.Empty<TrangaTask>();
}
});
app.MapPost("/Tasks/Start", (string taskType, string? connectorName, string? publicationId) =>
{
TrangaTask.Task pTask = Enum.Parse<TrangaTask.Task>(taskType);
TrangaTask? task = taskManager.GetAllTasks().FirstOrDefault(tTask =>
tTask.task == pTask && tTask.publication?.internalId == publicationId && tTask.connectorName == connectorName);
if (task is null)
try
{
TrangaTask.Task pTask = Enum.Parse<TrangaTask.Task>(taskType);
TrangaTask? task = null;
if (connectorName is null || publicationId is null)
task = taskManager.GetAllTasks().FirstOrDefault(tTask =>
tTask.task == pTask);
else
task = taskManager.GetAllTasks().FirstOrDefault(tTask =>
tTask.task == pTask && tTask.publication?.internalId == publicationId &&
tTask.connectorName == connectorName);
if (task is null)
return;
taskManager.ExecuteTaskNow(task);
}
catch (ArgumentException)
{
return;
taskManager.ExecuteTaskNow(task);
}
});
app.MapGet("/Tasks/GetRunningTasks",
@ -109,22 +138,50 @@ app.MapGet("/Queue/GetList",
app.MapPost("/Queue/Enqueue", (string taskType, string? connectorName, string? publicationId) =>
{
TrangaTask.Task pTask = Enum.Parse<TrangaTask.Task>(taskType);
TrangaTask? task = taskManager.GetAllTasks().FirstOrDefault(tTask =>
tTask.task == pTask && tTask.publication?.internalId == publicationId && tTask.connectorName == connectorName);
if (task is null)
try
{
TrangaTask.Task pTask = Enum.Parse<TrangaTask.Task>(taskType);
TrangaTask? task = null;
if (connectorName is null || publicationId is null)
task = taskManager.GetAllTasks().FirstOrDefault(tTask =>
tTask.task == pTask);
else
task = taskManager.GetAllTasks().FirstOrDefault(tTask =>
tTask.task == pTask && tTask.publication?.internalId == publicationId &&
tTask.connectorName == connectorName);
if (task is null)
return;
taskManager.AddTaskToQueue(task);
}
catch (ArgumentException)
{
return;
taskManager.AddTaskToQueue(task);
}
});
app.MapDelete("/Queue/Dequeue", (string taskType, string? connectorName, string? publicationId) =>
{
TrangaTask.Task pTask = Enum.Parse<TrangaTask.Task>(taskType);
TrangaTask? task = taskManager.GetAllTasks().FirstOrDefault(tTask =>
tTask.task == pTask && tTask.publication?.internalId == publicationId && tTask.connectorName == connectorName);
if (task is null)
try
{
TrangaTask.Task pTask = Enum.Parse<TrangaTask.Task>(taskType);
TrangaTask? task = null;
if (connectorName is null || publicationId is null)
task = taskManager.GetAllTasks().FirstOrDefault(tTask =>
tTask.task == pTask);
else
task = taskManager.GetAllTasks().FirstOrDefault(tTask =>
tTask.task == pTask && tTask.publication?.internalId == publicationId &&
tTask.connectorName == connectorName);
if (task is null)
return;
taskManager.RemoveTaskFromQueue(task);
}
catch (ArgumentException)
{
return;
taskManager.RemoveTaskFromQueue(task);
}
});
app.MapGet("/Settings/Get", () => taskManager.settings);

View File

@ -1,18 +0,0 @@
FROM mcr.microsoft.com/dotnet/runtime:7.0 AS base
WORKDIR /app
FROM mcr.microsoft.com/dotnet/sdk:7.0 AS build
WORKDIR /src
COPY ["Tranga-CLI/Tranga-CLI.csproj", "Tranga-CLI/"]
RUN dotnet restore "Tranga-CLI/Tranga-CLI.csproj"
COPY . .
WORKDIR "/src/Tranga-CLI"
RUN dotnet build "Tranga-CLI.csproj" -c Release -o /app/build
FROM build AS publish
RUN dotnet publish "Tranga-CLI.csproj" -c Release -o /app/publish /p:UseAppHost=false
FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "Tranga-CLI.dll"]

View File

@ -1,3 +1,4 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:Boolean x:Key="/Default/UserDictionary/Words/=Komga/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Taskmanager/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Tranga/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>

View File

@ -1,4 +1,5 @@
using System.Globalization;
using System.Text.RegularExpressions;
namespace Tranga;
@ -15,13 +16,14 @@ public struct Chapter
public string fileName { get; }
public string sortNumber { get; }
private static readonly Regex LegalCharacters = new Regex(@"([A-z]*[0-9]* *\.*-*,*\]*\[*'*\'*\)*\(*~*!*)*");
public Chapter(string? name, string? volumeNumber, string? chapterNumber, string url)
{
this.name = name;
this.volumeNumber = volumeNumber is { Length: > 0 } ? volumeNumber : "1";
this.chapterNumber = chapterNumber;
this.url = url;
string chapterName = string.Concat((name ?? "").Split(Path.GetInvalidFileNameChars()));
string chapterName = string.Concat(LegalCharacters.Matches(name ?? ""));
NumberFormatInfo nfi = new NumberFormatInfo()
{
NumberDecimalSeparator = "."

View File

@ -16,7 +16,9 @@ public abstract class Connector
protected Logger? logger;
protected Connector(string downloadLocation, Logger? logger)
protected string imageCachePath;
protected Connector(string downloadLocation, string imageCachePath, Logger? logger)
{
this.downloadLocation = downloadLocation;
this.logger = logger;
@ -24,6 +26,7 @@ public abstract class Connector
{
//RequestTypes for RateLimits
}, logger);
this.imageCachePath = imageCachePath;
}
public abstract string name { get; } //Name of the Connector (e.g. Website)
@ -57,7 +60,8 @@ public abstract class Connector
/// Retrieves the Cover from the Website
/// </summary>
/// <param name="publication">Publication to retrieve Cover for</param>
public abstract void DownloadCover(Publication publication);
/// <param name="settings">TrangaSettings</param>
public abstract void CloneCoverFromCache(Publication publication, TrangaSettings settings);
/// <summary>
/// Saves the series-info to series.json in the Publication Folder
@ -83,7 +87,7 @@ public abstract class Connector
/// <returns>XML-string</returns>
protected static string CreateComicInfo(Publication publication, Chapter chapter, Logger? logger)
{
logger?.WriteLine("Connector", $"Creating ComicInfo.Xml for {publication.sortName} Chapter {chapter.volumeNumber} {chapter.chapterNumber}");
logger?.WriteLine("Connector", $"Creating ComicInfo.Xml for {publication.sortName} {publication.internalId} {chapter.volumeNumber}-{chapter.chapterNumber}");
XElement comicInfo = new XElement("ComicInfo",
new XElement("Tags", string.Join(',',publication.tags)),
new XElement("LanguageISO", publication.originalLanguage),
@ -134,11 +138,12 @@ public abstract class Connector
/// <param name="imageUrls">List of URLs to download Images from</param>
/// <param name="saveArchiveFilePath">Full path to save archive to (without file ending .cbz)</param>
/// <param name="downloadClient">DownloadClient of the connector</param>
/// <param name="logger"></param>
/// <param name="comicInfoPath">Path of the generate Chapter ComicInfo.xml, if it was generated</param>
/// <param name="requestType">RequestType for RateLimits</param>
protected static void DownloadChapterImages(string[] imageUrls, string saveArchiveFilePath, DownloadClient downloadClient, byte requestType, Logger? logger, string? comicInfoPath = null)
{
logger?.WriteLine("Connector", "Downloading Images");
logger?.WriteLine("Connector", $"Downloading Images for {saveArchiveFilePath}");
//Check if Publication Directory already exists
string directoryPath = Path.GetDirectoryName(saveArchiveFilePath)!;
if (!Directory.Exists(directoryPath))
@ -156,13 +161,14 @@ public abstract class Connector
{
string[] split = imageUrl.Split('.');
string extension = split[^1];
logger?.WriteLine("Connector", $"Downloading Image {chapter + 1}/{imageUrls.Length}");
DownloadImage(imageUrl, Path.Join(tempFolder, $"{chapter++}.{extension}"), downloadClient, requestType);
}
if(comicInfoPath is not null)
File.Copy(comicInfoPath, Path.Join(tempFolder, "ComicInfo.xml"));
logger?.WriteLine("Connector", "Creating archive");
logger?.WriteLine("Connector", $"Creating archive {saveArchiveFilePath}");
//ZIP-it and ship-it
ZipFile.CreateFromDirectory(tempFolder, saveArchiveFilePath);
Directory.Delete(tempFolder, true); //Cleanup
@ -224,6 +230,7 @@ public abstract class Connector
catch (HttpRequestException e)
{
logger?.WriteLine(this.GetType().ToString(), e.Message);
logger?.WriteLine(this.GetType().ToString(), $"Waiting {_rateLimit[requestType] * 2}");
Thread.Sleep(_rateLimit[requestType] * 2);
}
}

View File

@ -14,11 +14,11 @@ public class MangaDex : Connector
Manga,
Feed,
AtHomeServer,
Cover,
Author
CoverUrl,
Author,
}
public MangaDex(string downloadLocation, Logger? logger) : base(downloadLocation, logger)
public MangaDex(string downloadLocation, string imageCachePath, Logger? logger) : base(downloadLocation, imageCachePath, logger)
{
name = "MangaDex";
this.downloadClient = new DownloadClient(new Dictionary<byte, int>()
@ -26,7 +26,7 @@ public class MangaDex : Connector
{(byte)RequestType.Manga, 250},
{(byte)RequestType.Feed, 250},
{(byte)RequestType.AtHomeServer, 40},
{(byte)RequestType.Cover, 250},
{(byte)RequestType.CoverUrl, 250},
{(byte)RequestType.Author, 250}
}, logger);
}
@ -55,6 +55,7 @@ public class MangaDex : Connector
total = result["total"]!.GetValue<int>(); //Update the total number of Publications
JsonArray mangaInResult = result["data"]!.AsArray(); //Manga-data-Array
logger?.WriteLine(this.GetType().ToString(), $"Getting publication data.");
//Loop each Manga and extract information from JSON
foreach (JsonNode? mangeNode in mangaInResult)
{
@ -98,6 +99,10 @@ public class MangaDex : Connector
authorId = relationships.FirstOrDefault(relationship => relationship!["type"]!.GetValue<string>() == "author")!["id"]!.GetValue<string>();
}
string? coverUrl = GetCoverUrl(publicationId, posterId);
string? coverCacheName = null;
if (coverUrl is not null)
coverCacheName = SaveImage(coverUrl);
string? author = GetAuthor(authorId);
Dictionary<string, string> linksDict = new();
@ -127,6 +132,7 @@ public class MangaDex : Connector
altTitlesDict,
tags.ToArray(),
coverUrl,
coverCacheName,
linksDict,
year,
originalLanguage,
@ -137,12 +143,13 @@ public class MangaDex : Connector
}
}
logger?.WriteLine(this.GetType().ToString(), $"Done getting publications (title={publicationTitle})");
return publications.ToArray();
}
public override Chapter[] GetChapters(Publication publication, string language = "")
{
logger?.WriteLine(this.GetType().ToString(), $"Getting Chapters {publication.sortName} (language={language})");
logger?.WriteLine(this.GetType().ToString(), $"Getting Chapters for {publication.sortName} {publication.internalId} (language={language})");
const int limit = 100; //How many values we want returned at once
int offset = 0; //"Page"
int total = int.MaxValue; //How many total results are there, is updated on first request
@ -192,12 +199,13 @@ public class MangaDex : Connector
{
NumberDecimalSeparator = "."
};
logger?.WriteLine(this.GetType().ToString(), $"Done getting Chapters for {publication.internalId}");
return chapters.OrderBy(chapter => Convert.ToSingle(chapter.chapterNumber, chapterNumberFormatInfo)).ToArray();
}
public override void DownloadChapter(Publication publication, Chapter chapter)
{
logger?.WriteLine(this.GetType().ToString(), $"Download Chapter {publication.sortName} {chapter.volumeNumber}-{chapter.chapterNumber}");
logger?.WriteLine(this.GetType().ToString(), $"Downloading Chapter-Info {publication.sortName} {publication.internalId} {chapter.volumeNumber}-{chapter.chapterNumber}");
//Request URLs for Chapter-Images
DownloadClient.RequestResult requestResult =
downloadClient.MakeRequest($"https://api.mangadex.org/at-home/server/{chapter.url}?forcePort443=false'", (byte)RequestType.AtHomeServer);
@ -224,15 +232,16 @@ public class MangaDex : Connector
private string? GetCoverUrl(string publicationId, string? posterId)
{
logger?.WriteLine(this.GetType().ToString(), $"Getting CoverUrl for {publicationId}");
if (posterId is null)
{
logger?.WriteLine(this.GetType().ToString(), $"No posterId");
logger?.WriteLine(this.GetType().ToString(), $"No posterId, aborting");
return null;
}
//Request information where to download Cover
DownloadClient.RequestResult requestResult =
downloadClient.MakeRequest($"https://api.mangadex.org/cover/{posterId}", (byte)RequestType.Cover);
downloadClient.MakeRequest($"https://api.mangadex.org/cover/{posterId}", (byte)RequestType.CoverUrl);
if (requestResult.statusCode != HttpStatusCode.OK)
return null;
JsonObject? result = JsonSerializer.Deserialize<JsonObject>(requestResult.result);
@ -242,6 +251,7 @@ public class MangaDex : Connector
string fileName = result["data"]!["attributes"]!["fileName"]!.GetValue<string>();
string coverUrl = $"https://uploads.mangadex.org/covers/{publicationId}/{fileName}";
logger?.WriteLine(this.GetType().ToString(), $"Got Cover-Url for {publicationId} -> {coverUrl}");
return coverUrl;
}
@ -259,14 +269,16 @@ public class MangaDex : Connector
return null;
string author = result["data"]!["attributes"]!["name"]!.GetValue<string>();
logger?.WriteLine(this.GetType().ToString(), $"Got author {authorId} -> {author}");
return author;
}
public override void DownloadCover(Publication publication)
public override void CloneCoverFromCache(Publication publication, TrangaSettings settings)
{
logger?.WriteLine(this.GetType().ToString(), $"Download cover {publication.sortName}");
logger?.WriteLine(this.GetType().ToString(), $"Cloning cover {publication.sortName}");
//Check if Publication already has a Folder and cover
string publicationFolder = Path.Join(downloadLocation, publication.folderName);
if(!Directory.Exists(publicationFolder))
Directory.CreateDirectory(publicationFolder);
DirectoryInfo dirInfo = new (publicationFolder);
@ -276,20 +288,26 @@ public class MangaDex : Connector
return;
}
if (publication.posterUrl is null || publication.posterUrl!.Contains("http"))
{
logger?.WriteLine(this.GetType().ToString(), $"No Poster-URL in publication");
return;
}
string fileInCache = Path.Join(settings.coverImageCache, publication.coverFileNameInCache);
string newFilePath = Path.Join(publicationFolder, $"cover.{Path.GetFileName(fileInCache).Split('.')[^1]}" );
logger?.WriteLine(this.GetType().ToString(), $"Cloning cover {fileInCache} -> {newFilePath}");
File.Copy(fileInCache, newFilePath, true);
}
//Get file-extension (jpg, png)
string[] split = publication.posterUrl.Split('.');
string extension = split[^1];
private string SaveImage(string url)
{
string[] split = url.Split('/');
string filename = split[^1];
string saveImagePath = Path.Join(imageCachePath, filename);
string outFolderPath = Path.Join(downloadLocation, publication.folderName);
Directory.CreateDirectory(outFolderPath);
if (File.Exists(saveImagePath))
return filename;
//Download cover-Image
DownloadImage(publication.posterUrl, Path.Join(downloadLocation, publication.folderName, $"cover.{extension}"), this.downloadClient, (byte)RequestType.AtHomeServer);
DownloadClient.RequestResult coverResult = downloadClient.MakeRequest(url, (byte)RequestType.AtHomeServer);
using MemoryStream ms = new();
coverResult.result.CopyTo(ms);
File.WriteAllBytes(saveImagePath, ms.ToArray());
logger?.WriteLine(this.GetType().ToString(), $"Saving image to {saveImagePath}");
return filename;
}
}

View File

@ -1,4 +1,5 @@
using System.Net.Http.Headers;
using System.Net;
using System.Net.Http.Headers;
using System.Text.Json.Nodes;
using Logging;
using Newtonsoft.Json;
@ -45,9 +46,17 @@ public class Komga
{
logger?.WriteLine(this.GetType().ToString(), $"Getting Libraries");
Stream data = NetClient.MakeRequest($"{baseUrl}/api/v1/libraries", auth);
if (data == Stream.Null)
{
logger?.WriteLine(this.GetType().ToString(), $"No libraries returned");
return Array.Empty<KomgaLibrary>();
}
JsonArray? result = JsonSerializer.Deserialize<JsonArray>(data);
if (result is null)
{
logger?.WriteLine(this.GetType().ToString(), $"No libraries returned");
return Array.Empty<KomgaLibrary>();
}
HashSet<KomgaLibrary> ret = new();
@ -89,37 +98,51 @@ public class Komga
{
public static Stream MakeRequest(string url, string auth)
{
HttpClient client = new();
HttpRequestMessage requestMessage = new HttpRequestMessage
HttpClientHandler clientHandler = new ();
clientHandler.ServerCertificateCustomValidationCallback = (message, cert, chain, sslPolicyErrors) => true;
HttpClient client = new(clientHandler);
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", auth);
HttpRequestMessage requestMessage = new ()
{
Method = HttpMethod.Get,
RequestUri = new Uri(url),
Headers =
{
{ "Accept", "application/json" },
{ "Authorization", new AuthenticationHeaderValue("Basic", auth).ToString() }
}
RequestUri = new Uri(url)
};
HttpResponseMessage response = client.Send(requestMessage);
Stream resultString = response.IsSuccessStatusCode ? response.Content.ReadAsStream() : Stream.Null;
return resultString;
Stream ret;
if (response.StatusCode is HttpStatusCode.Unauthorized)
{
ret = MakeRequest(response.RequestMessage!.RequestUri!.AbsoluteUri, auth);
}else
return response.IsSuccessStatusCode ? response.Content.ReadAsStream() : Stream.Null;
return ret;
}
public static bool MakePost(string url, string auth)
{
HttpClient client = new();
HttpRequestMessage requestMessage = new HttpRequestMessage
HttpClientHandler clientHandler = new HttpClientHandler();
clientHandler.ServerCertificateCustomValidationCallback = (message, cert, chain, sslPolicyErrors) => true;
HttpClient client = new(clientHandler)
{
Method = HttpMethod.Post,
RequestUri = new Uri(url),
Headers =
DefaultRequestHeaders =
{
{ "Accept", "application/json" },
{ "Authorization", new AuthenticationHeaderValue("Basic", auth).ToString() }
}
};
HttpRequestMessage requestMessage = new HttpRequestMessage
{
Method = HttpMethod.Post,
RequestUri = new Uri(url)
};
HttpResponseMessage response = client.Send(requestMessage);
return response.IsSuccessStatusCode;
bool ret;
if (response.StatusCode is HttpStatusCode.Unauthorized)
{
ret = MakePost(response.RequestMessage!.RequestUri!.AbsoluteUri, auth);
}else
return response.IsSuccessStatusCode;
return ret;
}
}
}

View File

@ -1,4 +1,5 @@
using System.Text;
using System.Text.RegularExpressions;
using Newtonsoft.Json;
namespace Tranga;
@ -15,6 +16,7 @@ public readonly struct Publication
public string? description { get; }
public string[] tags { get; }
public string? posterUrl { get; }
public string? coverFileNameInCache { get; }
public Dictionary<string,string> links { get; }
public int? year { get; }
public string? originalLanguage { get; }
@ -23,20 +25,25 @@ public readonly struct Publication
public string publicationId { get; }
public string internalId { get; }
public Publication(string sortName, string? author, string? description, Dictionary<string,string> altTitles, string[] tags, string? posterUrl, Dictionary<string,string>? links, int? year, string? originalLanguage, string status, string publicationId)
private static readonly Regex LegalCharacters = new Regex(@"([A-z]*[0-9]* *\.*-*,*\]*\[*'*\'*\)*\(*~*!*)*");
public Publication(string sortName, string? author, string? description, Dictionary<string,string> altTitles, string[] tags, string? posterUrl, string? coverFileNameInCache, Dictionary<string,string>? links, int? year, string? originalLanguage, string status, string publicationId)
{
this.sortName = sortName;
this.author = author;
this.description = description;
this.altTitles = altTitles;
this.tags = tags;
this.coverFileNameInCache = coverFileNameInCache;
this.posterUrl = posterUrl;
this.links = links ?? new Dictionary<string, string>();
this.year = year;
this.originalLanguage = originalLanguage;
this.status = status;
this.publicationId = publicationId;
this.folderName = string.Concat(sortName.Split(Path.GetInvalidPathChars().Concat(Path.GetInvalidFileNameChars()).ToArray()));
this.folderName = string.Concat(LegalCharacters.Matches(sortName));
while (this.folderName.EndsWith('.'))
this.folderName = this.folderName.Substring(0, this.folderName.Length - 1);
string onlyLowerLetters = string.Concat(this.sortName.ToLower().Where(Char.IsLetter));
this.internalId = Convert.ToBase64String(Encoding.ASCII.GetBytes($"{onlyLowerLetters}{this.year}"));
}

View File

@ -37,7 +37,7 @@ public static class TaskExecutor
switch (trangaTask.task)
{
case TrangaTask.Task.DownloadNewChapters:
DownloadNewChapters(connector!, (Publication)trangaTask.publication!, trangaTask.language, ref taskManager._chapterCollection);
DownloadNewChapters(connector!, (Publication)trangaTask.publication!, trangaTask.language, ref taskManager._chapterCollection, taskManager.settings);
break;
case TrangaTask.Task.UpdateChapters:
UpdateChapters(connector!, (Publication)trangaTask.publication!, trangaTask.language, ref taskManager._chapterCollection);
@ -90,7 +90,7 @@ public static class TaskExecutor
/// <param name="publication">Publication to check</param>
/// <param name="language">Language to receive chapters for</param>
/// <param name="chapterCollection"></param>
private static void DownloadNewChapters(Connector connector, Publication publication, string language, ref Dictionary<Publication, List<Chapter>> chapterCollection)
private static void DownloadNewChapters(Connector connector, Publication publication, string language, ref Dictionary<Publication, List<Chapter>> chapterCollection, TrangaSettings settings)
{
//Check if Publication already has a Folder
string publicationFolder = Path.Join(connector.downloadLocation, publication.folderName);
@ -98,7 +98,7 @@ public static class TaskExecutor
Directory.CreateDirectory(publicationFolder);
List<Chapter> newChapters = UpdateChapters(connector, publication, language, ref chapterCollection);
connector.DownloadCover(publication);
connector.CloneCoverFromCache(publication, settings);
string seriesInfoPath = Path.Join(publicationFolder, "series.json");
if(!File.Exists(seriesInfoPath))

View File

@ -21,11 +21,12 @@ public class TaskManager
/// <param name="downloadFolderPath">Local path to save data (Manga) to</param>
/// <param name="workingDirectory">Path to the working directory</param>
/// <param name="imageCachePath">Path to the cover-image cache</param>
/// <param name="komgaBaseUrl">The Url of the Komga-instance that you want to update</param>
/// <param name="komgaUsername">The Komga username</param>
/// <param name="komgaPassword">The Komga password</param>
/// <param name="logger"></param>
public TaskManager(string downloadFolderPath, string workingDirectory, string? komgaBaseUrl = null, string? komgaUsername = null, string? komgaPassword = null, Logger? logger = null)
public TaskManager(string downloadFolderPath, string workingDirectory, string imageCachePath, string? komgaBaseUrl = null, string? komgaUsername = null, string? komgaPassword = null, Logger? logger = null)
{
this.logger = logger;
_allTasks = new HashSet<TrangaTask>();
@ -37,7 +38,7 @@ public class TaskManager
this.settings = new TrangaSettings(downloadFolderPath, workingDirectory, newKomga);
ExportData();
this._connectors = new Connector[]{ new MangaDex(downloadFolderPath, logger) };
this._connectors = new Connector[]{ new MangaDex(downloadFolderPath, imageCachePath, logger) };
foreach(Connector cConnector in this._connectors)
_taskQueue.Add(cConnector, new List<TrangaTask>());
@ -58,7 +59,7 @@ public class TaskManager
public TaskManager(TrangaSettings settings, Logger? logger = null)
{
this.logger = logger;
this._connectors = new Connector[]{ new MangaDex(settings.downloadLocation, logger) };
this._connectors = new Connector[]{ new MangaDex(settings.downloadLocation, settings.coverImageCache, logger) };
foreach(Connector cConnector in this._connectors)
_taskQueue.Add(cConnector, new List<TrangaTask>());
_allTasks = new HashSet<TrangaTask>();
@ -140,14 +141,11 @@ public class TaskManager
TrangaTask newTask;
if (task == TrangaTask.Task.UpdateKomgaLibrary)
{
newTask = new TrangaTask(task, null, null, reoccurrence, language);
//Check if same task already exists
// ReSharper disable once SimplifyLinqExpressionUseAll readabilty
if (!_allTasks.Any(trangaTask => trangaTask.task == task))
{
_allTasks.Add(newTask);
}
newTask = new TrangaTask(task, null, null, reoccurrence);
logger?.WriteLine(this.GetType().ToString(), $"Removing old {task}-Task.");
//Only one UpdateKomgaLibrary Task
_allTasks.RemoveWhere(trangaTask => trangaTask.task is TrangaTask.Task.UpdateKomgaLibrary);
_allTasks.Add(newTask);
}
else
{

View File

@ -9,6 +9,7 @@ public class TrangaSettings
[JsonIgnore]public string settingsFilePath => Path.Join(workingDirectory, "settings.json");
[JsonIgnore]public string tasksFilePath => Path.Join(workingDirectory, "tasks.json");
[JsonIgnore]public string knownPublicationsPath => Path.Join(workingDirectory, "knownPublications.json");
[JsonIgnore] public string coverImageCache => Path.Join(workingDirectory, "imageCache");
public Komga? komga { get; set; }
public TrangaSettings(string downloadLocation, string workingDirectory, Komga? komga)

4
Website/Dockerfile Normal file
View File

@ -0,0 +1,4 @@
FROM nginx:alpine3.17-slim
COPY . /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

View File

@ -1,4 +1,23 @@
const apiUri = "http://localhost:5177";
let apiUri = `http://${window.location.host.split(':')[0]}:6531`
if(getCookie("apiUri") != ""){
apiUri = getCookie("apiUri");
}
function getCookie(cname) {
let name = cname + "=";
let decodedCookie = decodeURIComponent(document.cookie);
let ca = decodedCookie.split(';');
for(let i = 0; i < ca.length; i++) {
let c = ca[i];
while (c.charAt(0) == ' ') {
c = c.substring(1);
}
if (c.indexOf(name) == 0) {
return c.substring(name.length, c.length);
}
}
return "";
}
async function GetData(uri){
let request = await fetch(uri, {
@ -47,7 +66,7 @@ async function GetTaskTypes(){
return json;
}
async function GetRunningTasks(){
var uri = apiUri + "/Tranga/GetRunningTasks";
var uri = apiUri + "/Tasks/GetRunningTasks";
let json = await GetData(uri);
return json;
}
@ -99,3 +118,9 @@ function DequeueTask(taskType, connectorName, publicationId){
var uri = apiUri + `/Queue/Dequeue?taskType=${taskType}&connectorName=${connectorName}&publicationId=${publicationId}`;
DeleteData(uri);
}
async function GetQueue(){
var uri = apiUri + "/Queue/GetList";
let json = await GetData(uri);
return json;
}

BIN
Website/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

View File

@ -6,92 +6,116 @@
<link rel="stylesheet" href="style.css">
</head>
<body>
<topbar>
<titlebox>
<img src="media/blahaj.png">
<span>Tranga</span>
</titlebox>
<spacer></spacer>
<searchdiv>
<input id="searchbox" placeholder="Filter" type="text">
</searchdiv>
<img id="settingscog" src="media/settings-cogwheel.svg" height="100%" alt="settingscog">
</topbar>
<viewport>
<sidebar>
<background-placeholder></background-placeholder>
<wrapper>
<topbar>
<titlebox>
<img src="media/blahaj.png">
<span>Tranga</span>
</titlebox>
<spacer></spacer>
<p style="text-align: center">Made with Blåhaj 🦈</p>
</sidebar>
<content>
<div id="addPublication">
<p>+</p>
<searchdiv>
<input id="searchbox" placeholder="Filter" type="text">
</searchdiv>
<img id="settingscog" src="media/settings-cogwheel.svg" height="100%" alt="settingscog">
</topbar>
<viewport>
<content>
<div id="addPublication">
<p>+</p>
</div>
<publication>
<img src="media/cover.jpg">
<publication-information>
<connector-name class="pill">MangaDex</connector-name>
<publication-name>Tensei Pandemic</publication-name>
</publication-information>
</publication>
</content>
<popup id="addTaskPopup">
<blur-background id="blurBackgroundTaskPopup"></blur-background>
<addtask-window>
<window-titlebar>
<p>Add Task</p>
<img id="closePopupImg" src="media/close-x.svg" alt="Close">
</window-titlebar>
<window-content>
<addtask-settings>
<addtask-setting><label for="selectReccurrence">Recurrence</label><input id="selectReccurrence" type="time" value="01:00:00" step="3600"></addtask-setting>
<addtask-setting><label for="connectors">Connector</label>
<select id="connectors">
<option value=""></option>
</select>
</addtask-setting>
<addtask-setting><label for="searchPublicationQuery">Search Title</label><input id="searchPublicationQuery" type="text"></addtask-setting>
<input type="submit" value="Search" onclick="NewSearch();">
</addtask-settings>
<div id="taskSelectOutput"></div>
</window-content>
</addtask-window>
</popup>
<popup id="publicationViewerPopup">
<blur-background id="blurBackgroundPublicationPopup"></blur-background>
<publication-viewer>
<img id="pubviewcover" src="media/cover.jpg" alt="cover">
<publication-information>
<publication-name id="publicationViewerName">Tensei Pandemic</publication-name>
<publication-author id="publicationViewerAuthor">Imamura Hinata</publication-author>
<publication-description id="publicationViewerDescription">Imamura Hinata is a high school boy with a cute appearance.
Since his trauma with the first love, he wanted to be more manly than anybody else. But one day he woke up to something different…
The total opposite of his ideal male body!
Pandemic love comedy!
</publication-description>
<publication-interactions>
<publication-starttask>Start Task ▶️</publication-starttask>
<publication-delete>Delete Task ❌</publication-delete>
<publication-add>Add Task </publication-add>
</publication-interactions>
</publication-information>
</publication-viewer>
</popup>
<popup id="settingsPopup">
<blur-background id="blurBackgroundSettingsPopup"></blur-background>
<settings>
<span style="font-weight: bold; text-align: center; font-size: 16pt;">Settings</span>
<div>
<p class="title">Download Location:</p>
<span id="downloadLocation"></span>
</div>
<div>
<p class="title">API-URI</p>
<label for="settingApiUri"></label><input placeholder="https://" type="text" id="settingApiUri">
</div>
<komga-settings>
<span class="title">Komga</span>
<div>Configured: <span id="komgaConfigured">✅❌</span></div>
<label for="komgaUrl"></label><input placeholder="URL" id="komgaUrl" type="text">
<label for="komgaUsername"></label><input placeholder="Username" id="komgaUsername" type="text">
<label for="komgaPassword"></label><input placeholder="Password" id="komgaPassword" type="password">
<label for="komgaUpdateTime" style="margin-right: 5px;">Update Time</label><input id="komgaUpdateTime" type="time" value="00:01:00" step="10">
<input type="submit" value="Update" onclick="UpdateKomgaSettings()">
</komga-settings>
</settings>
</popup>
</viewport>
<footer>
<div>
<img src="media/running.svg" alt="running"><div id="tasksRunningTag">0</div>
</div>
<publication>
<img src="media/cover.jpg">
<publication-information>
<connector-name class="pill">MangaDex</connector-name>
<publication-name>Tensei Pandemic</publication-name>
</publication-information>
</publication>
</content>
<popup id="addTaskPopup">
<blur-background id="blurBackgroundTaskPopup"></blur-background>
<addtask-window>
<window-titlebar>
<p>Add Task</p>
<img id="closePopupImg" src="media/close-x.svg" alt="Close">
</window-titlebar>
<window-content>
<addtask-settings>
<addtask-setting><label for="selectReccurrence">Recurrence</label><input id="selectReccurrence" type="time" value="01:00" step="3600"></addtask-setting>
<addtask-setting><label for="connectors">Connector</label>
<select id="connectors">
<option value=""></option>
</select>
</addtask-setting>
<addtask-setting><label for="searchPublicationQuery">Search Title</label><input id="searchPublicationQuery" type="text"></addtask-setting>
<input type="submit" value="Search" onclick="NewSearch();">
</addtask-settings>
<div id="taskSelectOutput"></div>
</window-content>
</addtask-window>
</popup>
<popup id="publicationViewerPopup">
<blur-background id="blurBackgroundPublicationPopup"></blur-background>
<publication-viewer>
<img id="pubviewcover" src="media/cover.jpg" alt="cover">
<publication-information>
<publication-name id="publicationViewerName">Tensei Pandemic</publication-name>
<publication-author id="publicationViewerAuthor">Imamura Hinata</publication-author>
<publication-description id="publicationViewerDescription">Imamura Hinata is a high school boy with a cute appearance.
Since his trauma with the first love, he wanted to be more manly than anybody else. But one day he woke up to something different…
The total opposite of his ideal male body!
Pandemic love comedy!
</publication-description>
<publication-delete>Delete Task ❌</publication-delete>
<publication-add>Add Task </publication-add>
</publication-information>
</publication-viewer>
</popup>
</viewport>
<settingstab id="settingstab">
<span class="title">Download Location:</span>
<span id="downloadLocation"></span>
<komga-settings>
<span class="title">Komga</span>
<div>Configured: <span id="komgaConfigured">✅❌</span></div>
<label for="komgaUrl"></label><input placeholder="URL" id="komgaUrl" type="text">
<label for="komgaUsername"></label><input placeholder="Username" id="komgaUsername" type="text">
<label for="komgaPassword"></label><input placeholder="Password" id="komgaPassword" type="password">
<div><label for="komgaUpdateTime" style="margin-right: 5px;">Update Time</label><input id="komgaUpdateTime" type="time" value="00:01"></div>
<input type="submit" value="Update" onclick="UpdateSettingsClick()">
</komga-settings>
</settingstab>
<div>
<img src="media/queue.svg" alt="queue"><div id="tasksQueuedTag">0</div>
</div>
<div>
<img src="media/tasks.svg" alt="queue"><div id="totalTasksTag">0</div>
</div>
<p id="madeWith">Made with Blåhaj 🦈</p>
</footer>
</wrapper>
<footer-tag-popup>
<footer-tag-content>
<footer-tag-task-name>Test</footer-tag-task-name>
</footer-tag-content>
</footer-tag-popup>
<script src="apiConnector.js"></script>
<script src="interaction.js"></script>

View File

@ -1,31 +1,11 @@
const slideInRight = [
{ right: "-20rem" },
{ right: "0" }
];
const slideInRightTiming = {
duration: 200,
iterations: 1,
fill: "forwards",
easing: "ease-out"
}
const slideOutRightTiming = {
direction: "reverse",
duration: 200,
iterations: 1,
fill: "forwards",
easing: "ease-in"
}
let publications = [];
let publications = [];
let tasks = [];
let toEditId;
const searchPublicationQuery = document.querySelector("#searchPublicationQuery");
const selectPublication = document.querySelector("#taskSelectOutput");
const connectorSelect = document.querySelector("#connectors");
const settingsTab = document.querySelector("#settingstab");
const settingsPopup = document.querySelector("#settingsPopup");
const settingsCog = document.querySelector("#settingscog");
const selectRecurrence = document.querySelector("#selectReccurrence");
const tasksContent = document.querySelector("content");
@ -38,6 +18,7 @@ const publicationViewerAuthor = document.querySelector("#publicationViewerAuthor
const pubviewcover = document.querySelector("#pubviewcover");
const publicationDelete = document.querySelector("publication-delete");
const publicationAdd = document.querySelector("publication-add");
const publicationTaskStart = document.querySelector("publication-starttask");
const closetaskpopup = document.querySelector("#closePopupImg");
const settingDownloadLocation = document.querySelector("#downloadLocation");
const settingKomgaUrl = document.querySelector("#komgaUrl");
@ -45,14 +26,39 @@ const settingKomgaUser = document.querySelector("#komgaUsername");
const settingKomgaPass = document.querySelector("#komgaPassword");
const settingKomgaTime = document.querySelector("#komgaUpdateTime");
const settingKomgaConfigured = document.querySelector("#komgaConfigured");
const settingApiUri = document.querySelector("#settingApiUri");
const tagTasksRunning = document.querySelector("#tasksRunningTag");
const tagTasksQueued = document.querySelector("#tasksQueuedTag");
const tagTasksTotal = document.querySelector("#totalTasksTag");
const tagTaskPopup = document.querySelector("footer-tag-popup");
const tagTasksPopupContent = document.querySelector("footer-tag-content");
settingsCog.addEventListener("click", () => OpenSettings());
document.querySelector("#blurBackgroundSettingsPopup").addEventListener("click", () => HideSettings());
closetaskpopup.addEventListener("click", () => HideAddTaskPopup());
document.querySelector("#blurBackgroundTaskPopup").addEventListener("click", () => HideAddTaskPopup());
document.querySelector("#blurBackgroundPublicationPopup").addEventListener("click", () => HidePublicationPopup());
publicationDelete.addEventListener("click", () => DeleteTaskClick());
publicationAdd.addEventListener("click", () => AddTaskClick());
publicationTaskStart.addEventListener("click", () => StartTaskClick());
settingApiUri.addEventListener("keypress", (event) => {
if(event.key === "Enter"){
apiUri = settingApiUri.value;
setTimeout(() => GetSettingsClick(), 100);
document.cookie = `apiUri=${apiUri};`;
}
});
searchPublicationQuery.addEventListener("keypress", (event) => {
if(event.key === "Enter"){
NewSearch();
}
});
tagTasksRunning.addEventListener("mouseover", (event) => ShowRunningTasks(event));
tagTasksRunning.addEventListener("mouseout", () => CloseTasksPopup());
tagTasksQueued.addEventListener("mouseover", (event) => ShowQueuedTasks(event));
tagTasksQueued.addEventListener("mouseout", () => CloseTasksPopup());
tagTasksTotal.addEventListener("mouseover", (event) => ShowAllTasks(event));
tagTasksTotal.addEventListener("mouseout", () => CloseTasksPopup());
let availableConnectors;
GetAvailableControllers()
@ -66,11 +72,6 @@ GetAvailableControllers()
})
);
searchPublicationQuery.addEventListener("keypress", (event) => {
if(event.key === "Enter"){
NewSearch();
}
});
function NewSearch(){
//Disable inputs
@ -103,7 +104,7 @@ function CreatePublication(publication, connector){
var publicationElement = document.createElement('publication');
publicationElement.setAttribute("id", publication.internalId);
var img = document.createElement('img');
img.src = publication.posterUrl;
img.src = `imageCache/${publication.coverFileNameInCache}`;
publicationElement.appendChild(img);
var info = document.createElement('publication-information');
var connectorName = document.createElement('connector-name');
@ -131,13 +132,10 @@ function AddTaskClick(){
HidePublicationPopup();
}
let slideIn = true;
function slide() {
if (slideIn)
settingsTab.animate(slideInRight, slideInRightTiming);
else
settingsTab.animate(slideInRight, slideOutRightTiming);
slideIn = !slideIn;
function StartTaskClick(){
var toEditTask = tasks.filter(task => task.publication.internalId == toEditId)[0];
StartTask("DownloadNewChapters", toEditTask.connectorName, toEditId);
HidePublicationPopup();
}
function ResetContent(){
@ -154,30 +152,39 @@ function ResetContent(){
tasksContent.appendChild(add);
}
function ShowPublicationViewerWindow(publicationId, event, add){
//Show popup
publicationViewerPopup.style.display = "block";
//Set position to mouse-position
publicationViewerWindow.style.top = `${event.clientY - 60}px`;
publicationViewerWindow.style.left = `${event.clientX}px`;
if(event.clientY < window.innerHeight - publicationViewerWindow.offsetHeight)
publicationViewerWindow.style.top = `${event.clientY}px`;
else
publicationViewerWindow.style.top = `${event.clientY - publicationViewerWindow.offsetHeight}px`;
if(event.clientX < window.innerWidth - publicationViewerWindow.offsetWidth)
publicationViewerWindow.style.left = `${event.clientX}px`;
else
publicationViewerWindow.style.left = `${event.clientX - publicationViewerWindow.offsetWidth}px`;
//Edit information inside the window
var publication = publications.filter(pub => pub.internalId === publicationId)[0];
publicationViewerName.innerText = publication.sortName;
publicationViewerDescription.innerText = publication.description;
publicationViewerAuthor.innerText = publication.author;
pubviewcover.src = publication.posterUrl;
pubviewcover.src = `imageCache/${publication.coverFileNameInCache}`;
toEditId = publicationId;
//Check what action should be listed
if(add){
publicationAdd.style.display = "block";
publicationAdd.style.display = "initial";
publicationDelete.style.display = "none";
publicationTaskStart.style.display = "none";
}
else{
publicationAdd.style.display = "none";
publicationDelete.style.display = "block";
publicationDelete.style.display = "initial";
publicationTaskStart.style.display = "initial";
}
//Show popup
publicationViewerPopup.style.display = "block";
}
function HidePublicationPopup(){
@ -206,14 +213,21 @@ const fadeInTiming = {
function OpenSettings(){
GetSettingsClick();
slide();
settingsPopup.style.display = "flex";
}
function HideSettings(){
settingsPopup.style.display = "none";
}
function GetSettingsClick(){
settingApiUri.value = "";
settingKomgaUrl.value = "";
settingKomgaUser.value = "";
settingKomgaPass.value = "";
settingApiUri.placeholder = apiUri;
GetSettings().then(json => {
settingDownloadLocation.innerText = json.downloadLocation;
if(json.komga != null)
@ -221,6 +235,7 @@ function GetSettingsClick(){
});
GetKomgaTask().then(json => {
settingKomgaTime.value = json[0].reoccurrence;
if(json.length > 0)
settingKomgaConfigured.innerText = "✅";
else
@ -228,18 +243,80 @@ function GetSettingsClick(){
});
}
function UpdateSettingsClick(){
var auth = utf8_to_b64(`${settingKomgaUser.value}:${settingKomgaPass.value}`);
console.log(auth);
UpdateSettings("", settingKomgaUrl.value, auth);
function UpdateKomgaSettings(){
if(settingKomgaUser.value != "" && settingKomgaPass != ""){
var auth = utf8_to_b64(`${settingKomgaUser.value}:${settingKomgaPass.value}`);
console.log(auth);
if(settingKomgaUrl.value != "")
UpdateSettings("", settingKomgaUrl.value, auth);
else
UpdateSettings("", settingKomgaUrl.placeholder, auth);
}
CreateTask("UpdateKomgaLibrary", settingKomgaTime.value, "","","");
setTimeout(() => GetSettingsClick(), 500);
setTimeout(() => GetSettingsClick(), 100);
}
function utf8_to_b64( str ) {
return window.btoa(unescape(encodeURIComponent( str )));
}
function ShowRunningTasks(event){
GetRunningTasks()
.then(json => {
tagTasksPopupContent.replaceChildren();
json.forEach(task => {
console.log(task);
if(task.publication != null){
var taskname = document.createElement("footer-tag-task-name");
taskname.innerText = task.publication.sortName;
tagTasksPopupContent.appendChild(taskname);
}
});
if(tagTasksPopupContent.children.length > 0){
tagTaskPopup.style.display = "block";
tagTaskPopup.style.left = `${tagTasksRunning.offsetLeft - 20}px`;
}
});
}
function ShowQueuedTasks(event){
GetQueue()
.then(json => {
tagTasksPopupContent.replaceChildren();
json.forEach(task => {
var taskname = document.createElement("footer-tag-task-name");
taskname.innerText = task.publication.sortName;
tagTasksPopupContent.appendChild(taskname);
});
if(json.length > 0){
tagTaskPopup.style.display = "block";
tagTaskPopup.style.left = `${tagTasksQueued.offsetLeft- 20}px`;
}
});
}
function ShowAllTasks(event){
GetDownloadTasks()
.then(json => {
tagTasksPopupContent.replaceChildren();
json.forEach(task => {
var taskname = document.createElement("footer-tag-task-name");
taskname.innerText = task.publication.sortName;
tagTasksPopupContent.appendChild(taskname);
});
if(json.length > 0){
tagTaskPopup.style.display = "block";
tagTaskPopup.style.left = `${tagTasksTotal.offsetLeft - 20}px`;
}
});
}
function CloseTasksPopup(){
tagTaskPopup.style.display = "none";
}
//Resets the tasks shown
ResetContent();
//Get Tasks and show them
@ -251,6 +328,21 @@ GetDownloadTasks()
tasks.push(task);
}));
GetRunningTasks()
.then(json => {
tagTasksRunning.innerText = json.length;
});
GetDownloadTasks()
.then(json => {
tagTasksTotal.innerText = json.length;
});
GetQueue()
.then(json => {
tagTasksQueued.innerText = json.length;
})
setInterval(() => {
//Tasks from API
var cTasks = [];
@ -273,5 +365,19 @@ setInterval(() => {
}
);
GetRunningTasks()
.then(json => {
tagTasksRunning.innerText = json.length;
});
GetDownloadTasks()
.then(json => {
tagTasksTotal.innerText = json.length;
});
GetQueue()
.then(json => {
tagTasksQueued.innerText = json.length;
})
}, 1000);

7
Website/media/queue.svg Normal file
View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="none">
<g fill="#000000">

After

Width:  |  Height:  |  Size: 545 B

53
Website/media/running.svg Normal file
View File

@ -0,0 +1,53 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg fill="#000000" version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
width="800px" height="800px" viewBox="0 0 235.504 235.504"
xml:space="preserve">
<g>
<g>
<path d="M195.209,81.456l-49.227-0.15c0.737-0.886,1.351-1.868,2.284-2.583c3.282-2.497,3.911-7.166,1.427-10.438
c-2.501-3.266-7.161-3.919-10.443-1.423c-4.873,3.715-8.388,8.704-10.255,14.389l-22.191-0.064
c-9.508,0-19.588,7.398-22.938,16.851l-16.877,47.479c-1.775,5.013-1.338,9.966,1.207,13.568
c2.412,3.427,6.384,5.318,11.187,5.358l45.126,0.136c-1.509,5.186-4.701,9.622-9.352,12.424
c-4.891,2.957-10.636,3.814-16.172,2.444c-3.994-0.998-8.031,1.442-9.027,5.418c-0.99,4.012,1.445,8.035,5.432,9.032
c2.927,0.738,5.879,1.091,8.808,1.091c6.516,0,12.93-1.788,18.645-5.23c8.312-5.013,14.172-12.979,16.484-22.409
c0.232-0.905,0.232-1.823,0.124-2.713l28.296,0.092h0.049c2.925,0,5.854-0.89,8.684-2.147c0.2,0.493,0.32,1.014,0.661,1.471
c3.335,4.677,4.629,10.343,3.688,15.993c-0.95,5.627-4.028,10.536-8.688,13.862c-3.351,2.376-4.14,7.037-1.755,10.379
c1.466,2.04,3.751,3.122,6.062,3.122c1.491,0,3.006-0.429,4.312-1.367c7.919-5.61,13.16-13.966,14.771-23.52
c1.603-9.565-0.613-19.203-6.28-27.122c-0.48-0.693-1.134-1.19-1.779-1.659c1.318-1.831,2.501-3.763,3.238-5.854l16.863-47.464
c1.795-5.018,1.351-9.969-1.194-13.58C203.954,83.387,200.015,81.47,195.209,81.456z M201.979,98.405l-16.868,47.464
c-0.981,2.757-2.941,5.214-5.213,7.329c-0.337,0.16-0.706,0.229-1.026,0.465c-0.673,0.485-1.182,1.122-1.639,1.747
c-2.962,1.996-6.288,3.339-9.434,3.339v2.989l-0.044-2.989l-33.194-0.101c-0.232-0.076-0.424-0.261-0.661-0.324
c-1.435-0.353-2.805-0.145-4.095,0.309l-29.768-0.101l1.192-3.358c0.549-1.547-0.269-3.25-1.813-3.795
c-1.521-0.553-3.25,0.24-3.799,1.804l-1.899,5.334l-14.318-0.044c-2.805,0-5.063-0.998-6.336-2.813
c-1.437-2.032-1.603-4.921-0.463-8.144l16.877-47.478c2.48-6.979,10.417-12.868,17.356-12.868l12.217,0.038l-1.963,5.536
c-0.555,1.549,0.262,3.25,1.805,3.797c0.331,0.12,0.661,0.174,0.998,0.174c1.227,0,2.372-0.768,2.793-1.986l2.497-7.019
c0.064-0.164-0.048-0.322-0.016-0.487h2.512c-0.905,7.758,1.163,15.42,5.947,21.638c5.903,7.687,14.852,11.726,23.873,11.726
c6.371,0,12.771-2.001,18.186-6.129c3.266-2.488,3.911-7.167,1.426-10.441c-2.508-3.267-7.161-3.901-10.455-1.415
c-6.612,5.056-16.146,3.775-21.223-2.809c-2.445-3.194-3.487-7.133-2.958-11.117c0.061-0.503,0.353-0.916,0.481-1.402
l52.216,0.156c2.806,0,5.054,1.004,6.324,2.811C202.928,92.241,203.105,95.223,201.979,98.405z"/>
<path d="M107.997,127.194c-1.531-0.553-3.248,0.244-3.799,1.791l-4.302,12.099c-0.551,1.543,0.265,3.242,1.813,3.795
c0.331,0.116,0.659,0.16,0.998,0.16c1.214,0,2.372-0.765,2.801-1.976l4.294-12.099
C110.369,129.446,109.551,127.728,107.997,127.194z"/>
<path d="M116.6,103.014c-1.529-0.541-3.25,0.252-3.805,1.805l-4.298,12.088c-0.547,1.547,0.261,3.252,1.799,3.799
c0.329,0.12,0.659,0.172,1,0.172c1.222,0,2.368-0.769,2.809-1.983l4.294-12.09C118.955,105.268,118.139,103.555,116.6,103.014z"/>
<path d="M232.527,90.428l-14.896-0.038l0,0c-1.639,0-2.974,1.327-2.997,2.976c0,1.639,1.342,2.981,2.981,2.989l14.896,0.042l0,0
c1.643,0,2.978-1.331,2.993-2.979C235.504,91.763,234.17,90.436,232.527,90.428z"/>
<path d="M220.333,80.436c0.629,0,1.242-0.188,1.771-0.583l11.994-8.83c1.326-0.974,1.611-2.842,0.645-4.168
c-0.965-1.327-2.845-1.611-4.163-0.637l-11.998,8.833c-1.323,0.974-1.607,2.841-0.642,4.167
C218.513,80.003,219.418,80.436,220.333,80.436z"/>
<path d="M209.152,56.279c-1.547-0.549-3.25,0.269-3.787,1.805l-4.997,14.036c-0.537,1.547,0.26,3.252,1.803,3.807
c0.337,0.12,0.674,0.172,0.994,0.172c1.242,0,2.385-0.757,2.821-1.986l4.985-14.036C211.516,58.541,210.695,56.846,209.152,56.279
z"/>
<path d="M17.587,100.894h55.208c1.641,0,2.976-1.343,2.976-2.981c0-1.641-1.334-2.988-2.976-2.988H17.587
c-1.641,0-2.988,1.338-2.988,2.988C14.599,99.559,15.946,100.894,17.587,100.894z"/>
<path d="M68.471,119.328c0-1.641-1.345-2.987-2.986-2.987H10.283c-1.639,0-2.981,1.338-2.981,2.987
c0,1.639,1.342,2.974,2.981,2.974h55.202C67.119,122.301,68.471,120.967,68.471,119.328z"/>
<path d="M58.188,137.758H2.974c-1.641,0-2.974,1.335-2.974,2.989c0,1.64,1.333,2.974,2.974,2.974h55.214
c1.639,0,2.981-1.334,2.981-2.974C61.162,139.093,59.827,137.758,58.188,137.758z"/>
<path d="M169.611,28.097c11.821,0,21.403,9.584,21.403,21.41c0,11.82-9.582,21.408-21.403,21.408
c-11.822,0-21.412-9.588-21.412-21.408C148.199,37.681,157.789,28.097,169.611,28.097z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.6 KiB

10
Website/media/tasks.svg Normal file
View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg fill="#000000" height="800px" width="800px" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
viewBox="0 0 24 24" enable-background="new 0 0 24 24" xml:space="preserve">
<g id="task">
<path d="M4,23.4l-3.7-3.7l1.4-1.4L4,20.6l4.3-4.3l1.4,1.4L4,23.4z M24,21H12v-2h12V21z M4,15.4l-3.7-3.7l1.4-1.4L4,12.6l4.3-4.3
l1.4,1.4L4,15.4z M24,13H12v-2h12V13z M4,7.4L0.3,3.7l1.4-1.4L4,4.6l4.3-4.3l1.4,1.4L4,7.4z M24,5H12V3h12V5z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 603 B

View File

@ -1,5 +1,5 @@
:root{
--background-color: #eee;
--background-color: #030304;
--second-background-color: #fff;
--primary-color: #f5a9b8;
--secondary-color: #5bcefa;
@ -11,15 +11,19 @@
body{
padding: 0;
margin: 0;
display: flex;
flex-flow: column;
flex-wrap: nowrap;
height: 100vh;
background-color: var(--background-color);
font-family: "Inter", sans-serif;
overflow-x: hidden;
}
wrapper {
display: flex;
flex-flow: column;
flex-wrap: nowrap;
height: 100vh;
}
background-placeholder{
background-color: var(--second-background-color);
opacity: 1;
@ -35,6 +39,8 @@ topbar {
align-items: center;
height: var(--topbar-height);
background-color: var(--secondary-color);
z-index: 100;
box-shadow: 0 0 20px black;
}
titlebox {
@ -70,9 +76,9 @@ searchdiv{
}
#searchbox {
padding: 6px 8px;
padding: 3px 10px;
border: 0;
border-radius: 3px;
border-radius: 4px;
font-size: 14pt;
width: 250px;
}
@ -80,7 +86,7 @@ searchdiv{
#settingscog {
cursor: pointer;
margin: 0px 30px;
height: calc(100% - 40px);
height: 50%;
filter: invert(100%) sepia(0%) saturate(7465%) hue-rotate(115deg) brightness(116%) contrast(101%);
}
@ -90,22 +96,45 @@ viewport {
flex-flow: row;
flex-wrap: nowrap;
flex-grow: 1;
height: 100%;
overflow-y: scroll;
}
sidebar{
position: relative;
width: 20rem;
margin-bottom: 20px;
border-radius: 0 0 5px 0;
footer {
display: flex;
flex-direction: column;
flex-direction: row;
flex-wrap: nowrap;
width: 100%;
height: 40px;
align-items: center;
justify-content: center;
background-color: var(--primary-color);
align-content: center;
}
footer > div {
height: 100%;
margin: 0 30px;
display: flex;
flex-direction: row;
flex-wrap: nowrap;
align-items: center;
}
footer > div > *{
height: 40%;
margin: 0 5px;
}
#madeWith {
flex-grow: 1;
text-align: right;
margin-right: 20px;
}
content {
position: relative;
flex-grow: 1;
margin: 0 10px 10px 10px;
border-radius: 5px;
display: flex;
flex-direction: row;
@ -114,28 +143,39 @@ content {
align-content: start;
}
settingstab{
position: absolute;
right: -20rem;
bottom: 0;
background-color: rgba(0,0,0,0.5);
width: 20rem;
height: calc(100% - var(--topbar-height) - 40px);
margin-bottom: 10px;
border-radius: 5px 0 0 5px;
settings {
width: 50%;
background-color: var(--accent-color);
display: flex;
flex-direction: column;
flex-wrap: nowrap;
z-index: 10;
position: absolute;
left: 25%;
top: 25%;
border-radius: 5px;
padding: 10px 0;
}
settingstab > * {
margin: 0 20px;
#settingsPopup{
z-index: 10;
}
settingstab .title {
font-size: 14pt;
settings > * {
margin: 0 20%;
}
settings input {
margin: 3px 0;
padding: 3px;
border-radius: 3px;
border: 1px solid rgba(0,0,0,0.2);
width: 100%;
}
settings .title {
font-weight: bolder;
margin-top: 20px;
font-size: 14pt;
margin: 15px 0 2px 0;
}
komga-settings {
@ -145,16 +185,6 @@ komga-settings {
flex-wrap: nowrap;
}
komga-settings > * {
margin: 2px 0;
}
komga-settings input {
padding: 3px;
border-radius: 3px;
border: 0;
}
#addPublication {
cursor: pointer;
background-color: var(--secondary-color);
@ -182,7 +212,8 @@ komga-settings input {
font-size: 12pt;
border-radius: 9pt;
background-color: var(--primary-color);
padding: 2pt 20px;
padding: 2pt 17px;
color: black;
}
publication{
@ -202,7 +233,7 @@ publication::after{
left: 0; top: 0;
border-radius: 5px;
width: 100%; height: 100%;
background: linear-gradient(rgba(0, 0, 0, 0.5),rgba(0, 0, 0, 0.1));
background: linear-gradient(rgba(0,0,0,0.8), rgba(0, 0, 0, 0.7),rgba(0, 0, 0, 0.2));
}
publication-information {
@ -233,7 +264,9 @@ publication img {
left: 0;
width: 100%;
height: 100%;
object-fit: cover;
z-index: 0;
border-radius: 5px;
}
popup{
@ -242,7 +275,7 @@ popup{
min-height: 100%;
top: 0;
left: 0;
position: absolute;
position: fixed;
z-index: 2;
}
@ -267,7 +300,6 @@ addtask-window {
max-height: 80%;
padding: 0;
background-color: var(--accent-color);
z-index: 5;
border-radius: 5px;
}
@ -338,12 +370,12 @@ addtask-settings addtask-setting{
}
#publicationViewerPopup{
z-index: 6;
z-index: 5;
}
publication-viewer{
display: block;
width: 500px;
width: 450px;
height: 300px;
position: absolute;
top: 200px;
@ -351,9 +383,6 @@ publication-viewer{
background-color: var(--accent-color);
border-radius: 5px;
overflow: hidden;
}
publication-viewer{
padding: 30px;
}
@ -371,8 +400,9 @@ publication-viewer img {
position: absolute;
left: 0;
top: 0;
min-height: 100%;
max-width: 100%;
height: 100%;
width: 100%;
object-fit: cover;
border-radius: 5px;
z-index: 0;
}
@ -402,20 +432,69 @@ publication-viewer publication-information publication-description {
overflow-x: scroll;
}
publication-viewer publication-information publication-delete {
publication-viewer publication-information publication-interactions {
display: flex;
flex-direction: row;
position: absolute;
bottom: 0px;
right: 0px;
color: red;
margin: 20px;
justify-content: end;
align-items: start;
bottom: 0;
left: 0;
width: 100%;
}
publication-viewer publication-information publication-interactions > * {
margin: 0 10px 10px 10px;
font-size: 16pt;
}
publication-viewer publication-information publication-add {
position: absolute;
bottom: 0px;
right: 0px;
color: limegreen;
margin: 20px;
font-size: 16pt;
publication-viewer publication-information publication-interactions publication-starttask {
color: var(--secondary-color);
}
publication-viewer publication-information publication-interactions publication-delete {
color: red;
}
publication-viewer publication-information publication-interactions publication-add {
color: limegreen;
}
footer-tag-popup {
display: none;
padding: 2px 4px;
position: fixed;
bottom: 58px;
left: 20px;
background-color: var(--second-background-color);
z-index: 8;
border-radius: 5px;
max-height: 400px;
}
footer-tag-content{
position: relative;
max-height: 400px;
display: flex;
flex-direction: column;
flex-wrap: nowrap;
overflow-y: scroll;
}
footer-tag-content > * {
margin: 2px 0;
}
footer-tag-popup::before{
content: "";
width: 0;
height: 0;
position: absolute;
border-right: 10px solid var(--second-background-color);
border-left: 10px solid transparent;
border-top: 10px solid var(--second-background-color);
border-bottom: 10px solid transparent;
left: 0px;
bottom: -17px;
border-radius: 0 0 0 5px;
}

19
docker-compose.yaml Normal file
View File

@ -0,0 +1,19 @@
services:
tranga-api:
image: glax/tranga-api:latest
container_name: tranga-api
volumes:
- ./tranga:/usr/share/Tranga-API #1 when replacing ./tranga replace #2 with same value
- ./Manga:/Manga
ports:
- 6531:80
restart: unless-stopped
tranga-website:
image: glax/tranga-website:latest
container_name: tranga-website
volumes:
- ./tranga/imageCache:/usr/share/nginx/html/imageCache:ro #2 when replacing Point to same value as #1/imageCache
ports:
- 9555:80
depends_on:
- tranga-api

BIN
screenshots/addtask.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

BIN
screenshots/overview.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

BIN
screenshots/settings.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB