Localization - First Pass (#2174)
* Started designing the backend localization service * Worked in Transloco for initial PoC * Worked in Transloco for initial PoC * Translated the login screen * translated dashboard screen * Started work on the backend * Fixed a logic bug * translated edit-user screen * Hooked up the backend for having a locale property. * Hooked up the ability to view the available locales and switch to them. * Made the localization service languages be derived from what's in langs/ directory. * Fixed up localization switching * Switched when we check for a license on UI bootstrap * Tweaked some code * Fixed the bug where dashboard wasn't loading and made it so language switching is working. * Fixed a bug on dashboard with languagePath * Converted user-scrobble-history.component.html * Converted spoiler.component.html * Converted review-series-modal.component.html * Converted review-card-modal.component.html * Updated the readme * Translated using Weblate (English) Currently translated at 100.0% (54 of 54 strings) Translation: Kavita/ui Translate-URL: https://hosted.weblate.org/projects/kavita/ui/en/ * Converted review-card.component.html * Deleted dead component * Converted want-to-read.component.html * Added translation using Weblate (Korean) * Translated using Weblate (Spanish) Currently translated at 40.7% (22 of 54 strings) Translation: Kavita/ui Translate-URL: https://hosted.weblate.org/projects/kavita/ui/es/ * Translated using Weblate (Korean) Currently translated at 62.9% (34 of 54 strings) Translation: Kavita/ui Translate-URL: https://hosted.weblate.org/projects/kavita/ui/ko/ * Converted user-preferences.component.html * Translated using Weblate (Korean) Currently translated at 92.5% (50 of 54 strings) Translation: Kavita/ui Translate-URL: https://hosted.weblate.org/projects/kavita/ui/ko/ * Converted user-holds.component.html * Converted theme-manager.component.html * Converted restriction-selector.component.html * Converted manage-devices.component.html * Converted edit-device.component.html * Converted change-password.component.html * Converted change-email.component.html * Converted change-age-restriction.component.html * Converted api-key.component.html * Converted anilist-key.component.html * Converted typeahead.component.html * Converted user-stats-info-cards.component.html * Converted user-stats.component.html * Converted top-readers.component.html * Converted some pipes and ensure translation is loaded before the app. * Finished all but one pipe for localization * Converted directory-picker.component.html * Converted library-access-modal.component.html * Converted a few components * Converted a few components * Converted a few components * Converted a few components * Converted a few components * Merged weblate in * ... -> … update * Updated the readme * Updateded all fonts to be woff2 * Cleaned up some strings to increase re-use * Removed an old flow (that doesn't exist in backend any longer) from when we introduced emails on Kavita. * Converted Series detail * Lots more converted * Lots more converted & hooked up the ability to flatten during prod build the language files. * Lots more converted * Lots more converted & fixed a bunch of broken pipes due to inject() * Lots more converted * Lots more converted * Lots more converted & fixed some bad keys * Lots more converted * Fixed some bugs with admin dasbhoard nested tabs not rendering on first load due to not using onpush change detection * Fixed up some localization errors and fixed forgot password error when the user doesn't have change password permission * Fixed a stupid build issue again * Started adding errors for interceptor and backend. * Finished off manga-reader * More translations * Few fixes * Fixed a bug where character tag badges weren't showing the name on chapter info * All components are translated * All toasts are translated * All confirm/alerts are translated * Trying something new for the backend * Migrated the localization strings for the backend into a new file. * Updated the localization service to be able to do backend localization with fallback to english. * Cleaned up some external reviews code to reduce looping * Localized AccountController.cs * 60% done with controllers * All controllers are done * All KavitaExceptions are covered * Some shakeout fixes * Prep for initial merge * Everything is done except options and basic shakeout proves response times are good. Unit tests are broken. * Fixed up the unit tests * All unit tests are now working * Removed some quantifier * I'm not sure I can support localization for some Volume/Chapter/Book strings within the codebase. --------- Co-authored-by: Robbie Davis <robbie@therobbiedavis.com> Co-authored-by: majora2007 <kavitareader@gmail.com> Co-authored-by: expertjun <jtrobin@naver.com> Co-authored-by: ThePromidius <thepromidiusyt@gmail.com>
This commit is contained in:
parent
670bf82c38
commit
3b23d63234
389 changed files with 13652 additions and 7925 deletions
|
|
@ -301,7 +301,7 @@ public class ArchiveService : IArchiveService
|
|||
|
||||
if (!_directoryService.CopyFilesToDirectory(files, tempLocation))
|
||||
{
|
||||
throw new KavitaException("Unable to copy files to temp directory archive download.");
|
||||
throw new KavitaException("bad-copy-files-for-download");
|
||||
}
|
||||
|
||||
var zipPath = Path.Join(_directoryService.TempDirectory, $"kavita_{tempFolder}_{dateString}.zip");
|
||||
|
|
@ -314,7 +314,7 @@ public class ArchiveService : IArchiveService
|
|||
catch (AggregateException ex)
|
||||
{
|
||||
_logger.LogError(ex, "There was an issue creating temp archive");
|
||||
throw new KavitaException("There was an issue creating temp archive");
|
||||
throw new KavitaException("generic-create-temp-archive");
|
||||
}
|
||||
|
||||
return zipPath;
|
||||
|
|
|
|||
|
|
@ -1121,7 +1121,7 @@ public class BookService : IBookService
|
|||
if (doc.ParseErrors.Any())
|
||||
{
|
||||
LogBookErrors(book, contentFileRef, doc);
|
||||
throw new KavitaException("The file is malformed! Cannot read.");
|
||||
throw new KavitaException("epub-malformed");
|
||||
}
|
||||
_logger.LogError("{FilePath} has no body tag! Generating one for support. Book may be skewed", book.FilePath);
|
||||
doc.DocumentNode.SelectSingleNode("/html").AppendChild(HtmlNode.CreateNode("<body></body>"));
|
||||
|
|
@ -1137,7 +1137,7 @@ public class BookService : IBookService
|
|||
"There was an issue reading one of the pages for", ex);
|
||||
}
|
||||
|
||||
throw new KavitaException("Could not find the appropriate html for that page");
|
||||
throw new KavitaException("epub-html-missing");
|
||||
}
|
||||
|
||||
private static void CreateToCChapter(EpubBookRef book, EpubNavigationItemRef navigationItem, IList<BookChapterItem> nestedChapters,
|
||||
|
|
|
|||
|
|
@ -52,12 +52,12 @@ public class CollectionTagService : ICollectionTagService
|
|||
public async Task<bool> UpdateTag(CollectionTagDto dto)
|
||||
{
|
||||
var existingTag = await _unitOfWork.CollectionTagRepository.GetTagAsync(dto.Id);
|
||||
if (existingTag == null) throw new KavitaException("This tag does not exist");
|
||||
if (existingTag == null) throw new KavitaException("collection-doesnt-exist");
|
||||
|
||||
var title = dto.Title.Trim();
|
||||
if (string.IsNullOrEmpty(title)) throw new KavitaException("Title cannot be empty");
|
||||
if (string.IsNullOrEmpty(title)) throw new KavitaException("collection-tag-title-required");
|
||||
if (!title.Equals(existingTag.Title) && await TagExistsByName(dto.Title))
|
||||
throw new KavitaException("A tag with this name already exists");
|
||||
throw new KavitaException("collection-tag-duplicate");
|
||||
|
||||
existingTag.SeriesMetadatas ??= new List<SeriesMetadata>();
|
||||
existingTag.Title = title;
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ public class DeviceService : IDeviceService
|
|||
{
|
||||
userWithDevices.Devices ??= new List<Device>();
|
||||
var existingDevice = userWithDevices.Devices.SingleOrDefault(d => d.Name!.Equals(dto.Name));
|
||||
if (existingDevice != null) throw new KavitaException("A device with this name already exists");
|
||||
if (existingDevice != null) throw new KavitaException("device-duplicate");
|
||||
|
||||
existingDevice = new DeviceBuilder(dto.Name)
|
||||
.WithPlatform(dto.Platform)
|
||||
|
|
@ -70,7 +70,7 @@ public class DeviceService : IDeviceService
|
|||
try
|
||||
{
|
||||
var existingDevice = userWithDevices.Devices.SingleOrDefault(d => d.Id == dto.Id);
|
||||
if (existingDevice == null) throw new KavitaException("This device doesn't exist yet. Please create first");
|
||||
if (existingDevice == null) throw new KavitaException("device-not-created");
|
||||
|
||||
existingDevice.Name = dto.Name;
|
||||
existingDevice.Platform = dto.Platform;
|
||||
|
|
@ -108,11 +108,11 @@ public class DeviceService : IDeviceService
|
|||
public async Task<bool> SendTo(IReadOnlyList<int> chapterIds, int deviceId)
|
||||
{
|
||||
var device = await _unitOfWork.DeviceRepository.GetDeviceById(deviceId);
|
||||
if (device == null) throw new KavitaException("Device doesn't exist");
|
||||
if (device == null) throw new KavitaException("device-doesnt-exist");
|
||||
|
||||
var files = await _unitOfWork.ChapterRepository.GetFilesForChaptersAsync(chapterIds);
|
||||
if (files.Any(f => f.Format is not (MangaFormat.Epub or MangaFormat.Pdf)) && device.Platform == DevicePlatform.Kindle)
|
||||
throw new KavitaException("Cannot Send non Epub or Pdf to devices as not supported on Kindle");
|
||||
throw new KavitaException("send-to-permission");
|
||||
|
||||
|
||||
device.UpdateLastUsed();
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ public interface IDirectoryService
|
|||
string ConfigDirectory { get; }
|
||||
string SiteThemeDirectory { get; }
|
||||
string FaviconDirectory { get; }
|
||||
string LocalizationDirectory { get; }
|
||||
/// <summary>
|
||||
/// Original BookmarkDirectory. Only used for resetting directory. Use <see cref="ServerSettingKey.BackupDirectory"/> for actual path.
|
||||
/// </summary>
|
||||
|
|
@ -79,6 +80,7 @@ public class DirectoryService : IDirectoryService
|
|||
public string BookmarkDirectory { get; }
|
||||
public string SiteThemeDirectory { get; }
|
||||
public string FaviconDirectory { get; }
|
||||
public string LocalizationDirectory { get; }
|
||||
private readonly ILogger<DirectoryService> _logger;
|
||||
private const RegexOptions MatchOptions = RegexOptions.Compiled | RegexOptions.IgnoreCase;
|
||||
|
||||
|
|
@ -95,22 +97,23 @@ public class DirectoryService : IDirectoryService
|
|||
{
|
||||
_logger = logger;
|
||||
FileSystem = fileSystem;
|
||||
CoverImageDirectory = FileSystem.Path.Join(FileSystem.Directory.GetCurrentDirectory(), "config", "covers");
|
||||
CacheDirectory = FileSystem.Path.Join(FileSystem.Directory.GetCurrentDirectory(), "config", "cache");
|
||||
LogDirectory = FileSystem.Path.Join(FileSystem.Directory.GetCurrentDirectory(), "config", "logs");
|
||||
TempDirectory = FileSystem.Path.Join(FileSystem.Directory.GetCurrentDirectory(), "config", "temp");
|
||||
ConfigDirectory = FileSystem.Path.Join(FileSystem.Directory.GetCurrentDirectory(), "config");
|
||||
BookmarkDirectory = FileSystem.Path.Join(FileSystem.Directory.GetCurrentDirectory(), "config", "bookmarks");
|
||||
SiteThemeDirectory = FileSystem.Path.Join(FileSystem.Directory.GetCurrentDirectory(), "config", "themes");
|
||||
FaviconDirectory = FileSystem.Path.Join(FileSystem.Directory.GetCurrentDirectory(), "config", "favicons");
|
||||
|
||||
ExistOrCreate(SiteThemeDirectory);
|
||||
ExistOrCreate(ConfigDirectory);
|
||||
CoverImageDirectory = FileSystem.Path.Join(FileSystem.Directory.GetCurrentDirectory(), "config", "covers");
|
||||
ExistOrCreate(CoverImageDirectory);
|
||||
CacheDirectory = FileSystem.Path.Join(FileSystem.Directory.GetCurrentDirectory(), "config", "cache");
|
||||
ExistOrCreate(CacheDirectory);
|
||||
LogDirectory = FileSystem.Path.Join(FileSystem.Directory.GetCurrentDirectory(), "config", "logs");
|
||||
ExistOrCreate(LogDirectory);
|
||||
TempDirectory = FileSystem.Path.Join(FileSystem.Directory.GetCurrentDirectory(), "config", "temp");
|
||||
ExistOrCreate(TempDirectory);
|
||||
BookmarkDirectory = FileSystem.Path.Join(FileSystem.Directory.GetCurrentDirectory(), "config", "bookmarks");
|
||||
ExistOrCreate(BookmarkDirectory);
|
||||
SiteThemeDirectory = FileSystem.Path.Join(FileSystem.Directory.GetCurrentDirectory(), "config", "themes");
|
||||
ExistOrCreate(SiteThemeDirectory);
|
||||
FaviconDirectory = FileSystem.Path.Join(FileSystem.Directory.GetCurrentDirectory(), "config", "favicons");
|
||||
ExistOrCreate(FaviconDirectory);
|
||||
LocalizationDirectory = FileSystem.Path.Join(FileSystem.Directory.GetCurrentDirectory(), "I18N");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
146
API/Services/LocalizationService.cs
Normal file
146
API/Services/LocalizationService.cs
Normal file
|
|
@ -0,0 +1,146 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
using API.Data;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
|
||||
namespace API.Services;
|
||||
#nullable enable
|
||||
|
||||
|
||||
public interface ILocalizationService
|
||||
{
|
||||
Task<string> Get(string locale, string key, params object[] args);
|
||||
Task<string> Translate(int userId, string key, params object[] args);
|
||||
IEnumerable<string> GetLocales();
|
||||
}
|
||||
|
||||
public class LocalizationService : ILocalizationService
|
||||
{
|
||||
private readonly IDirectoryService _directoryService;
|
||||
private readonly IMemoryCache _cache;
|
||||
private readonly IUnitOfWork _unitOfWork;
|
||||
|
||||
/// <summary>
|
||||
/// The locales for the UI
|
||||
/// </summary>
|
||||
private readonly string _localizationDirectoryUi;
|
||||
|
||||
private readonly MemoryCacheEntryOptions _cacheOptions;
|
||||
|
||||
|
||||
public LocalizationService(IDirectoryService directoryService,
|
||||
IHostEnvironment environment, IMemoryCache cache, IUnitOfWork unitOfWork)
|
||||
{
|
||||
_directoryService = directoryService;
|
||||
_cache = cache;
|
||||
_unitOfWork = unitOfWork;
|
||||
if (environment.IsDevelopment())
|
||||
{
|
||||
_localizationDirectoryUi = directoryService.FileSystem.Path.Join(
|
||||
directoryService.FileSystem.Directory.GetCurrentDirectory(),
|
||||
"UI/Web/src/assets/langs");
|
||||
} else if (environment.EnvironmentName.Equals("Testing", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
_localizationDirectoryUi = directoryService.FileSystem.Path.Join(
|
||||
directoryService.FileSystem.Directory.GetCurrentDirectory(),
|
||||
"/../../../../../UI/Web/src/assets/langs");
|
||||
}
|
||||
else
|
||||
{
|
||||
_localizationDirectoryUi = directoryService.FileSystem.Path.Join(
|
||||
directoryService.FileSystem.Directory.GetCurrentDirectory(),
|
||||
"wwwroot", "assets/langs");
|
||||
}
|
||||
|
||||
_cacheOptions = new MemoryCacheEntryOptions()
|
||||
.SetSize(1)
|
||||
.SetAbsoluteExpiration(TimeSpan.FromMinutes(15));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Loads a language, if language is blank, falls back to english
|
||||
/// </summary>
|
||||
/// <param name="languageCode"></param>
|
||||
/// <returns></returns>
|
||||
public async Task<Dictionary<string, string>?> LoadLanguage(string languageCode)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(languageCode)) languageCode = "en";
|
||||
var languageFile = _directoryService.FileSystem.Path.Join(_directoryService.LocalizationDirectory, languageCode + ".json");
|
||||
if (!_directoryService.FileSystem.FileInfo.New(languageFile).Exists)
|
||||
throw new ArgumentException($"Language {languageCode} does not exist");
|
||||
|
||||
var json = await _directoryService.FileSystem.File.ReadAllTextAsync(languageFile);
|
||||
return JsonSerializer.Deserialize<Dictionary<string, string>>(json);
|
||||
}
|
||||
|
||||
public async Task<string> Get(string locale, string key, params object[] args)
|
||||
{
|
||||
|
||||
// Check if the translation for the given locale is cached
|
||||
var cacheKey = $"{locale}_{key}";
|
||||
if (!_cache.TryGetValue(cacheKey, out string? translatedString))
|
||||
{
|
||||
// Load the locale JSON file
|
||||
var translationData = await LoadLanguage(locale);
|
||||
|
||||
// Find the translation for the given key
|
||||
if (translationData != null && translationData.TryGetValue(key, out var value))
|
||||
{
|
||||
translatedString = value;
|
||||
|
||||
// Cache the translation for subsequent requests
|
||||
_cache.Set(cacheKey, translatedString, _cacheOptions);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (string.IsNullOrEmpty(translatedString))
|
||||
{
|
||||
if (!locale.Equals("en"))
|
||||
{
|
||||
return await Get("en", key, args);
|
||||
}
|
||||
return key;
|
||||
}
|
||||
|
||||
// Format the translated string with arguments
|
||||
if (args.Length > 0)
|
||||
{
|
||||
translatedString = string.Format(translatedString, args);
|
||||
}
|
||||
|
||||
return translatedString;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a translated string for a given user's locale, falling back to english or the key if missing
|
||||
/// </summary>
|
||||
/// <param name="userId"></param>
|
||||
/// <param name="key"></param>
|
||||
/// <param name="args"></param>
|
||||
/// <returns></returns>
|
||||
public async Task<string> Translate(int userId, string key, params object[] args)
|
||||
{
|
||||
var userLocale = await _unitOfWork.UserRepository.GetLocale(userId);
|
||||
return await Get(userLocale, key, args);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Returns all available locales that exist on both the Frontend and the Backend
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public IEnumerable<string> GetLocales()
|
||||
{
|
||||
return
|
||||
_directoryService.GetFilesWithExtension(_directoryService.FileSystem.Path.GetFullPath(_localizationDirectoryUi), @"\.json")
|
||||
.Select(f => _directoryService.FileSystem.Path.GetFileName(f).Replace(".json", string.Empty))
|
||||
.Union(_directoryService.GetFilesWithExtension(_directoryService.LocalizationDirectory, @"\.json")
|
||||
.Select(f => _directoryService.FileSystem.Path.GetFileName(f).Replace(".json", string.Empty)))
|
||||
.Distinct();
|
||||
}
|
||||
}
|
||||
|
|
@ -164,7 +164,7 @@ public class LicenseService : ILicenseService
|
|||
var serverSetting = await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.LicenseKey);
|
||||
var lic = await RegisterLicense(license, email);
|
||||
if (string.IsNullOrWhiteSpace(lic))
|
||||
throw new KavitaException("Unable to register license due to error. Reach out to Kavita+ Support");
|
||||
throw new KavitaException("unable-to-register-k+");
|
||||
serverSetting.Value = lic;
|
||||
_unitOfWork.SettingsRepository.Update(serverSetting);
|
||||
await _unitOfWork.CommitAsync();
|
||||
|
|
|
|||
|
|
@ -60,6 +60,7 @@ public class ScrobblingService : IScrobblingService
|
|||
private readonly IEventHub _eventHub;
|
||||
private readonly ILogger<ScrobblingService> _logger;
|
||||
private readonly ILicenseService _licenseService;
|
||||
private readonly ILocalizationService _localizationService;
|
||||
|
||||
public const string AniListWeblinkWebsite = "https://anilist.co/manga/";
|
||||
public const string MalWeblinkWebsite = "https://myanimelist.net/manga/";
|
||||
|
|
@ -87,13 +88,15 @@ public class ScrobblingService : IScrobblingService
|
|||
|
||||
|
||||
public ScrobblingService(IUnitOfWork unitOfWork, ITokenService tokenService,
|
||||
IEventHub eventHub, ILogger<ScrobblingService> logger, ILicenseService licenseService)
|
||||
IEventHub eventHub, ILogger<ScrobblingService> logger, ILicenseService licenseService,
|
||||
ILocalizationService localizationService)
|
||||
{
|
||||
_unitOfWork = unitOfWork;
|
||||
_tokenService = tokenService;
|
||||
_eventHub = eventHub;
|
||||
_logger = logger;
|
||||
_licenseService = licenseService;
|
||||
_localizationService = localizationService;
|
||||
|
||||
FlurlHttp.ConfigureClient(Configuration.KavitaPlusApiUrl, cli =>
|
||||
cli.Settings.HttpClientFactory = new UntrustedCertClientFactory());
|
||||
|
|
@ -184,11 +187,11 @@ public class ScrobblingService : IScrobblingService
|
|||
var token = await GetTokenForProvider(userId, ScrobbleProvider.AniList);
|
||||
if (await HasTokenExpired(token, ScrobbleProvider.AniList))
|
||||
{
|
||||
throw new KavitaException("AniList Credentials have expired or not set");
|
||||
throw new KavitaException(await _localizationService.Translate(userId, "unable-to-register-k+"));
|
||||
}
|
||||
|
||||
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId, SeriesIncludes.Metadata | SeriesIncludes.Library);
|
||||
if (series == null) throw new KavitaException("Series not found");
|
||||
if (series == null) throw new KavitaException(await _localizationService.Translate(userId, "series-doesnt-exist"));
|
||||
var library = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(series.LibraryId);
|
||||
if (library is not {AllowScrobbling: true}) return;
|
||||
if (library.Type == LibraryType.Comic) return;
|
||||
|
|
@ -229,11 +232,11 @@ public class ScrobblingService : IScrobblingService
|
|||
var token = await GetTokenForProvider(userId, ScrobbleProvider.AniList);
|
||||
if (await HasTokenExpired(token, ScrobbleProvider.AniList))
|
||||
{
|
||||
throw new KavitaException("AniList Credentials have expired or not set");
|
||||
throw new KavitaException(await _localizationService.Translate(userId, "anilist-cred-expired"));
|
||||
}
|
||||
|
||||
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId, SeriesIncludes.Metadata | SeriesIncludes.Library);
|
||||
if (series == null) throw new KavitaException("Series not found");
|
||||
if (series == null) throw new KavitaException(await _localizationService.Translate(userId, "series-doesnt-exist"));
|
||||
var library = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(series.LibraryId);
|
||||
if (library is not {AllowScrobbling: true}) return;
|
||||
if (library.Type == LibraryType.Comic) return;
|
||||
|
|
@ -273,11 +276,11 @@ public class ScrobblingService : IScrobblingService
|
|||
var token = await GetTokenForProvider(userId, ScrobbleProvider.AniList);
|
||||
if (await HasTokenExpired(token, ScrobbleProvider.AniList))
|
||||
{
|
||||
throw new KavitaException("AniList Credentials have expired or not set");
|
||||
throw new KavitaException(await _localizationService.Translate(userId, "anilist-cred-expired"));
|
||||
}
|
||||
|
||||
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId, SeriesIncludes.Metadata | SeriesIncludes.Library);
|
||||
if (series == null) throw new KavitaException("Series not found");
|
||||
if (series == null) throw new KavitaException(await _localizationService.Translate(userId, "series-doesnt-exist"));
|
||||
if (await _unitOfWork.UserRepository.HasHoldOnSeries(userId, seriesId))
|
||||
{
|
||||
_logger.LogInformation("Series {SeriesName} is on UserId {UserId}'s hold list. Not scrobbling", series.Name, userId);
|
||||
|
|
@ -338,11 +341,11 @@ public class ScrobblingService : IScrobblingService
|
|||
var token = await GetTokenForProvider(userId, ScrobbleProvider.AniList);
|
||||
if (await HasTokenExpired(token, ScrobbleProvider.AniList))
|
||||
{
|
||||
throw new KavitaException("AniList Credentials have expired or not set");
|
||||
throw new KavitaException(await _localizationService.Translate(userId, "anilist-cred-expired"));
|
||||
}
|
||||
|
||||
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId, SeriesIncludes.Metadata | SeriesIncludes.Library);
|
||||
if (series == null) throw new KavitaException("Series not found");
|
||||
if (series == null) throw new KavitaException(await _localizationService.Translate(userId, "series-doesnt-exist"));
|
||||
var library = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(series.LibraryId);
|
||||
if (library is not {AllowScrobbling: true}) return;
|
||||
if (library.Type == LibraryType.Comic) return;
|
||||
|
|
|
|||
|
|
@ -117,7 +117,7 @@ public class ReaderService : IReaderService
|
|||
{
|
||||
var seenVolume = new Dictionary<int, bool>();
|
||||
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId);
|
||||
if (series == null) throw new KavitaException("Series suddenly doesn't exist, cannot mark as read");
|
||||
if (series == null) throw new KavitaException("series-doesnt-exist");
|
||||
foreach (var chapter in chapters)
|
||||
{
|
||||
var userProgress = GetUserProgressForChapter(user, chapter);
|
||||
|
|
@ -202,8 +202,9 @@ public class ReaderService : IReaderService
|
|||
|
||||
if (user.Progresses == null)
|
||||
{
|
||||
throw new KavitaException("Progresses must exist on user");
|
||||
throw new KavitaException("progress-must-exist");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
userProgress =
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ using System.IO;
|
|||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using System.Xml.Serialization;
|
||||
using API.Comparators;
|
||||
using API.Data;
|
||||
using API.Data.Repositories;
|
||||
|
|
@ -17,7 +18,6 @@ using API.Services.Tasks.Scanner.Parser;
|
|||
using API.SignalR;
|
||||
using Kavita.Common;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
|
||||
namespace API.Services;
|
||||
|
||||
|
|
@ -49,7 +49,7 @@ public interface IReadingListService
|
|||
/// <summary>
|
||||
/// Methods responsible for management of Reading Lists
|
||||
/// </summary>
|
||||
/// <remarks>If called from API layer, expected for <see cref="UserHasReadingListAccess(int, String)"/> to be called beforehand</remarks>
|
||||
/// <remarks>If called from API layer, expected for <see cref="UserHasReadingListAccess(int, string)"/> to be called beforehand</remarks>
|
||||
public class ReadingListService : IReadingListService
|
||||
{
|
||||
private readonly IUnitOfWork _unitOfWork;
|
||||
|
|
@ -69,13 +69,13 @@ public class ReadingListService : IReadingListService
|
|||
public static string FormatTitle(ReadingListItemDto item)
|
||||
{
|
||||
var title = string.Empty;
|
||||
if (item.ChapterNumber == Tasks.Scanner.Parser.Parser.DefaultChapter && item.VolumeNumber != Tasks.Scanner.Parser.Parser.DefaultVolume) {
|
||||
if (item.ChapterNumber == Parser.DefaultChapter && item.VolumeNumber != Parser.DefaultVolume) {
|
||||
title = $"Volume {item.VolumeNumber}";
|
||||
}
|
||||
|
||||
if (item.SeriesFormat == MangaFormat.Epub) {
|
||||
var specialTitle = Tasks.Scanner.Parser.Parser.CleanSpecialTitle(item.ChapterNumber);
|
||||
if (specialTitle == Tasks.Scanner.Parser.Parser.DefaultChapter)
|
||||
var specialTitle = Parser.CleanSpecialTitle(item.ChapterNumber);
|
||||
if (specialTitle == Parser.DefaultChapter)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(item.ChapterTitleName))
|
||||
{
|
||||
|
|
@ -83,7 +83,7 @@ public class ReadingListService : IReadingListService
|
|||
}
|
||||
else
|
||||
{
|
||||
title = $"Volume {Tasks.Scanner.Parser.Parser.CleanSpecialTitle(item.VolumeNumber)}";
|
||||
title = $"Volume {Parser.CleanSpecialTitle(item.VolumeNumber)}";
|
||||
}
|
||||
} else {
|
||||
title = $"Volume {specialTitle}";
|
||||
|
|
@ -92,12 +92,12 @@ public class ReadingListService : IReadingListService
|
|||
|
||||
var chapterNum = item.ChapterNumber;
|
||||
if (!string.IsNullOrEmpty(chapterNum) && !JustNumbers.Match(item.ChapterNumber).Success) {
|
||||
chapterNum = Tasks.Scanner.Parser.Parser.CleanSpecialTitle(item.ChapterNumber);
|
||||
chapterNum = Parser.CleanSpecialTitle(item.ChapterNumber);
|
||||
}
|
||||
|
||||
if (title != string.Empty) return title;
|
||||
|
||||
if (item.ChapterNumber == Tasks.Scanner.Parser.Parser.DefaultChapter &&
|
||||
if (item.ChapterNumber == Parser.DefaultChapter &&
|
||||
!string.IsNullOrEmpty(item.ChapterTitleName))
|
||||
{
|
||||
title = item.ChapterTitleName;
|
||||
|
|
@ -124,13 +124,13 @@ public class ReadingListService : IReadingListService
|
|||
var hasExisting = userWithReadingList.ReadingLists.Any(l => l.Title.Equals(title));
|
||||
if (hasExisting)
|
||||
{
|
||||
throw new KavitaException("A list of this name already exists");
|
||||
throw new KavitaException("reading-list-name-exists");
|
||||
}
|
||||
|
||||
var readingList = new ReadingListBuilder(title).Build();
|
||||
userWithReadingList.ReadingLists.Add(readingList);
|
||||
|
||||
if (!_unitOfWork.HasChanges()) throw new KavitaException("There was a problem creating list");
|
||||
if (!_unitOfWork.HasChanges()) throw new KavitaException("generic-reading-list-create");
|
||||
await _unitOfWork.CommitAsync();
|
||||
return readingList;
|
||||
}
|
||||
|
|
@ -144,10 +144,10 @@ public class ReadingListService : IReadingListService
|
|||
public async Task UpdateReadingList(ReadingList readingList, UpdateReadingListDto dto)
|
||||
{
|
||||
dto.Title = dto.Title.Trim();
|
||||
if (string.IsNullOrEmpty(dto.Title)) throw new KavitaException("Title must be set");
|
||||
if (string.IsNullOrEmpty(dto.Title)) throw new KavitaException("reading-list-title-required");
|
||||
|
||||
if (!dto.Title.Equals(readingList.Title) && await _unitOfWork.ReadingListRepository.ReadingListExists(dto.Title))
|
||||
throw new KavitaException("Reading list already exists");
|
||||
throw new KavitaException("reading-list-name-exists");
|
||||
|
||||
readingList.Summary = dto.Summary;
|
||||
readingList.Title = dto.Title.Trim();
|
||||
|
|
@ -192,7 +192,7 @@ public class ReadingListService : IReadingListService
|
|||
/// <summary>
|
||||
/// Removes all entries that are fully read from the reading list. This commits
|
||||
/// </summary>
|
||||
/// <remarks>If called from API layer, expected for <see cref="UserHasReadingListAccess(int, String)"/> to be called beforehand</remarks>
|
||||
/// <remarks>If called from API layer, expected for <see cref="UserHasReadingListAccess(int, string)"/> to be called beforehand</remarks>
|
||||
/// <param name="readingListId">Reading List Id</param>
|
||||
/// <param name="user">User</param>
|
||||
/// <returns></returns>
|
||||
|
|
@ -404,7 +404,7 @@ public class ReadingListService : IReadingListService
|
|||
|
||||
var existingChapterExists = readingList.Items.Select(rli => rli.ChapterId).ToHashSet();
|
||||
var chaptersForSeries = (await _unitOfWork.ChapterRepository.GetChaptersByIdsAsync(chapterIds, ChapterIncludes.Volumes))
|
||||
.OrderBy(c => Tasks.Scanner.Parser.Parser.MinNumberFromRange(c.Volume.Name))
|
||||
.OrderBy(c => Parser.MinNumberFromRange(c.Volume.Name))
|
||||
.ThenBy(x => double.Parse(x.Number), _chapterSortComparerForInChapterSorting)
|
||||
.ToList();
|
||||
|
||||
|
|
@ -529,7 +529,7 @@ public class ReadingListService : IReadingListService
|
|||
/// <param name="cblReading"></param>
|
||||
public async Task<CblImportSummaryDto> ValidateCblFile(int userId, CblReadingList cblReading)
|
||||
{
|
||||
var importSummary = new CblImportSummaryDto()
|
||||
var importSummary = new CblImportSummaryDto
|
||||
{
|
||||
CblName = cblReading.Name,
|
||||
Success = CblImportResult.Success,
|
||||
|
|
@ -542,20 +542,20 @@ public class ReadingListService : IReadingListService
|
|||
if (await _unitOfWork.ReadingListRepository.ReadingListExists(cblReading.Name))
|
||||
{
|
||||
importSummary.Success = CblImportResult.Fail;
|
||||
importSummary.Results.Add(new CblBookResult()
|
||||
importSummary.Results.Add(new CblBookResult
|
||||
{
|
||||
Reason = CblImportReason.NameConflict,
|
||||
ReadingListName = cblReading.Name
|
||||
});
|
||||
}
|
||||
|
||||
var uniqueSeries = cblReading.Books.Book.Select(b => Tasks.Scanner.Parser.Parser.Normalize(b.Series)).Distinct().ToList();
|
||||
var uniqueSeries = cblReading.Books.Book.Select(b => Parser.Normalize(b.Series)).Distinct().ToList();
|
||||
var userSeries =
|
||||
(await _unitOfWork.SeriesRepository.GetAllSeriesByNameAsync(uniqueSeries, userId, SeriesIncludes.Chapters)).ToList();
|
||||
if (!userSeries.Any())
|
||||
{
|
||||
// Report that no series exist in the reading list
|
||||
importSummary.Results.Add(new CblBookResult()
|
||||
importSummary.Results.Add(new CblBookResult
|
||||
{
|
||||
Reason = CblImportReason.AllSeriesMissing
|
||||
});
|
||||
|
|
@ -569,7 +569,7 @@ public class ReadingListService : IReadingListService
|
|||
importSummary.Success = CblImportResult.Fail;
|
||||
foreach (var conflict in conflicts)
|
||||
{
|
||||
importSummary.Results.Add(new CblBookResult()
|
||||
importSummary.Results.Add(new CblBookResult
|
||||
{
|
||||
Reason = CblImportReason.SeriesCollision,
|
||||
Series = conflict.Name,
|
||||
|
|
@ -593,7 +593,7 @@ public class ReadingListService : IReadingListService
|
|||
{
|
||||
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId, AppUserIncludes.ReadingListsWithItems);
|
||||
_logger.LogDebug("Importing {ReadingListName} CBL for User {UserName}", cblReading.Name, user!.UserName);
|
||||
var importSummary = new CblImportSummaryDto()
|
||||
var importSummary = new CblImportSummaryDto
|
||||
{
|
||||
CblName = cblReading.Name,
|
||||
Success = CblImportResult.Success,
|
||||
|
|
@ -601,13 +601,13 @@ public class ReadingListService : IReadingListService
|
|||
SuccessfulInserts = new List<CblBookResult>()
|
||||
};
|
||||
|
||||
var uniqueSeries = cblReading.Books.Book.Select(b => Tasks.Scanner.Parser.Parser.Normalize(b.Series)).Distinct().ToList();
|
||||
var uniqueSeries = cblReading.Books.Book.Select(b => Parser.Normalize(b.Series)).Distinct().ToList();
|
||||
var userSeries =
|
||||
(await _unitOfWork.SeriesRepository.GetAllSeriesByNameAsync(uniqueSeries, userId, SeriesIncludes.Chapters)).ToList();
|
||||
var allSeries = userSeries.ToDictionary(s => Tasks.Scanner.Parser.Parser.Normalize(s.Name));
|
||||
var allSeriesLocalized = userSeries.ToDictionary(s => Tasks.Scanner.Parser.Parser.Normalize(s.LocalizedName));
|
||||
var allSeries = userSeries.ToDictionary(s => Parser.Normalize(s.Name));
|
||||
var allSeriesLocalized = userSeries.ToDictionary(s => Parser.Normalize(s.LocalizedName));
|
||||
|
||||
var readingListNameNormalized = Tasks.Scanner.Parser.Parser.Normalize(cblReading.Name);
|
||||
var readingListNameNormalized = Parser.Normalize(cblReading.Name);
|
||||
// Get all the user's reading lists
|
||||
var allReadingLists = (user.ReadingLists).ToDictionary(s => s.NormalizedTitle);
|
||||
if (!allReadingLists.TryGetValue(readingListNameNormalized, out var readingList))
|
||||
|
|
@ -620,7 +620,7 @@ public class ReadingListService : IReadingListService
|
|||
// Reading List exists, check if we own it
|
||||
if (user.ReadingLists.All(l => l.NormalizedTitle != readingListNameNormalized))
|
||||
{
|
||||
importSummary.Results.Add(new CblBookResult()
|
||||
importSummary.Results.Add(new CblBookResult
|
||||
{
|
||||
Reason = CblImportReason.NameConflict
|
||||
});
|
||||
|
|
@ -632,7 +632,7 @@ public class ReadingListService : IReadingListService
|
|||
readingList.Items ??= new List<ReadingListItem>();
|
||||
foreach (var (book, i) in cblReading.Books.Book.Select((value, i) => ( value, i )))
|
||||
{
|
||||
var normalizedSeries = Tasks.Scanner.Parser.Parser.Normalize(book.Series);
|
||||
var normalizedSeries = Parser.Normalize(book.Series);
|
||||
if (!allSeries.TryGetValue(normalizedSeries, out var bookSeries) && !allSeriesLocalized.TryGetValue(normalizedSeries, out bookSeries))
|
||||
{
|
||||
importSummary.Results.Add(new CblBookResult(book)
|
||||
|
|
@ -644,7 +644,7 @@ public class ReadingListService : IReadingListService
|
|||
}
|
||||
// Prioritize lookup by Volume then Chapter, but allow fallback to just Chapter
|
||||
var bookVolume = string.IsNullOrEmpty(book.Volume)
|
||||
? Tasks.Scanner.Parser.Parser.DefaultVolume
|
||||
? Parser.DefaultVolume
|
||||
: book.Volume;
|
||||
var matchingVolume = bookSeries.Volumes.Find(v => bookVolume == v.Name) ?? bookSeries.Volumes.Find(v => v.Number == 0);
|
||||
if (matchingVolume == null)
|
||||
|
|
@ -660,7 +660,7 @@ public class ReadingListService : IReadingListService
|
|||
|
||||
// We need to handle chapter 0 or empty string when it's just a volume
|
||||
var bookNumber = string.IsNullOrEmpty(book.Number)
|
||||
? Tasks.Scanner.Parser.Parser.DefaultChapter
|
||||
? Parser.DefaultChapter
|
||||
: book.Number;
|
||||
var chapter = matchingVolume.Chapters.FirstOrDefault(c => c.Number == bookNumber);
|
||||
if (chapter == null)
|
||||
|
|
@ -720,7 +720,7 @@ public class ReadingListService : IReadingListService
|
|||
private static IList<Series> FindCblImportConflicts(IEnumerable<Series> userSeries)
|
||||
{
|
||||
var dict = new HashSet<string>();
|
||||
return userSeries.Where(series => !dict.Add(Tasks.Scanner.Parser.Parser.Normalize(series.Name))).ToList();
|
||||
return userSeries.Where(series => !dict.Add(Parser.Normalize(series.Name))).ToList();
|
||||
}
|
||||
|
||||
private static bool IsCblEmpty(CblReadingList cblReading, CblImportSummaryDto importSummary,
|
||||
|
|
@ -729,7 +729,7 @@ public class ReadingListService : IReadingListService
|
|||
readingListFromCbl = new CblImportSummaryDto();
|
||||
if (cblReading.Books == null || cblReading.Books.Book.Count == 0)
|
||||
{
|
||||
importSummary.Results.Add(new CblBookResult()
|
||||
importSummary.Results.Add(new CblBookResult
|
||||
{
|
||||
Reason = CblImportReason.EmptyFile
|
||||
});
|
||||
|
|
@ -755,7 +755,7 @@ public class ReadingListService : IReadingListService
|
|||
|
||||
public static CblReadingList LoadCblFromPath(string path)
|
||||
{
|
||||
var reader = new System.Xml.Serialization.XmlSerializer(typeof(CblReadingList));
|
||||
var reader = new XmlSerializer(typeof(CblReadingList));
|
||||
using var file = new StreamReader(path);
|
||||
var cblReadingList = (CblReadingList) reader.Deserialize(file);
|
||||
file.Close();
|
||||
|
|
|
|||
|
|
@ -30,6 +30,12 @@ public interface ISeriesService
|
|||
Task<bool> DeleteMultipleSeries(IList<int> seriesIds);
|
||||
Task<bool> UpdateRelatedSeries(UpdateRelatedSeriesDto dto);
|
||||
Task<RelatedSeriesDto> GetRelatedSeries(int userId, int seriesId);
|
||||
Task<string> FormatChapterTitle(int userId, ChapterDto chapter, LibraryType libraryType, bool withHash = true);
|
||||
Task<string> FormatChapterTitle(int userId, Chapter chapter, LibraryType libraryType, bool withHash = true);
|
||||
|
||||
Task<string> FormatChapterTitle(int userId, bool isSpecial, LibraryType libraryType, string? chapterTitle,
|
||||
bool withHash);
|
||||
Task<string> FormatChapterName(int userId, LibraryType libraryType, bool withHash = false);
|
||||
}
|
||||
|
||||
public class SeriesService : ISeriesService
|
||||
|
|
@ -39,15 +45,17 @@ public class SeriesService : ISeriesService
|
|||
private readonly ITaskScheduler _taskScheduler;
|
||||
private readonly ILogger<SeriesService> _logger;
|
||||
private readonly IScrobblingService _scrobblingService;
|
||||
private readonly ILocalizationService _localizationService;
|
||||
|
||||
public SeriesService(IUnitOfWork unitOfWork, IEventHub eventHub, ITaskScheduler taskScheduler,
|
||||
ILogger<SeriesService> logger, IScrobblingService scrobblingService)
|
||||
ILogger<SeriesService> logger, IScrobblingService scrobblingService, ILocalizationService localizationService)
|
||||
{
|
||||
_unitOfWork = unitOfWork;
|
||||
_eventHub = eventHub;
|
||||
_taskScheduler = taskScheduler;
|
||||
_logger = logger;
|
||||
_scrobblingService = scrobblingService;
|
||||
_localizationService = localizationService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -382,16 +390,17 @@ public class SeriesService : ISeriesService
|
|||
var series = await _unitOfWork.SeriesRepository.GetSeriesDtoByIdAsync(seriesId, userId);
|
||||
var libraryIds = _unitOfWork.LibraryRepository.GetLibraryIdsForUserIdAsync(userId);
|
||||
if (!libraryIds.Contains(series.LibraryId))
|
||||
throw new UnauthorizedAccessException("User does not have access to the library this series belongs to");
|
||||
throw new UnauthorizedAccessException("user-no-access-library-from-series");
|
||||
|
||||
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId);
|
||||
if (user!.AgeRestriction != AgeRating.NotApplicable)
|
||||
{
|
||||
var seriesMetadata = await _unitOfWork.SeriesRepository.GetSeriesMetadata(seriesId);
|
||||
if (seriesMetadata!.AgeRating > user.AgeRestriction)
|
||||
throw new UnauthorizedAccessException("User is not allowed to view this series due to age restrictions");
|
||||
throw new UnauthorizedAccessException("series-restricted-age-restriction");
|
||||
}
|
||||
|
||||
|
||||
var libraryType = await _unitOfWork.LibraryRepository.GetLibraryTypeAsync(series.LibraryId);
|
||||
var volumes = (await _unitOfWork.VolumeRepository.GetVolumesDtoAsync(seriesId, userId))
|
||||
.OrderBy(v => Tasks.Scanner.Parser.Parser.MinNumberFromRange(v.Name))
|
||||
|
|
@ -401,13 +410,14 @@ public class SeriesService : ISeriesService
|
|||
var processedVolumes = new List<VolumeDto>();
|
||||
if (libraryType == LibraryType.Book)
|
||||
{
|
||||
var volumeLabel = await _localizationService.Translate(userId, "volume-num", string.Empty);
|
||||
foreach (var volume in volumes)
|
||||
{
|
||||
volume.Chapters = volume.Chapters.OrderBy(d => double.Parse(d.Number), ChapterSortComparer.Default).ToList();
|
||||
var firstChapter = volume.Chapters.First();
|
||||
// On Books, skip volumes that are specials, since these will be shown
|
||||
if (firstChapter.IsSpecial) continue;
|
||||
RenameVolumeName(firstChapter, volume, libraryType);
|
||||
RenameVolumeName(firstChapter, volume, libraryType, volumeLabel);
|
||||
processedVolumes.Add(volume);
|
||||
}
|
||||
}
|
||||
|
|
@ -431,7 +441,7 @@ public class SeriesService : ISeriesService
|
|||
|
||||
foreach (var chapter in chapters)
|
||||
{
|
||||
chapter.Title = FormatChapterTitle(chapter, libraryType);
|
||||
chapter.Title = await FormatChapterTitle(userId, chapter, libraryType);
|
||||
if (!chapter.IsSpecial) continue;
|
||||
|
||||
if (!string.IsNullOrEmpty(chapter.TitleName)) chapter.Title = chapter.TitleName;
|
||||
|
|
@ -481,7 +491,7 @@ public class SeriesService : ISeriesService
|
|||
return !chapter.IsSpecial && !chapter.Number.Equals(Tasks.Scanner.Parser.Parser.DefaultChapter);
|
||||
}
|
||||
|
||||
public static void RenameVolumeName(ChapterDto firstChapter, VolumeDto volume, LibraryType libraryType)
|
||||
public static void RenameVolumeName(ChapterDto firstChapter, VolumeDto volume, LibraryType libraryType, string volumeLabel = "Volume")
|
||||
{
|
||||
if (libraryType == LibraryType.Book)
|
||||
{
|
||||
|
|
@ -496,19 +506,19 @@ public class SeriesService : ISeriesService
|
|||
{
|
||||
volume.Name += $" - {firstChapter.TitleName}";
|
||||
}
|
||||
else
|
||||
{
|
||||
volume.Name += $"";
|
||||
}
|
||||
// else
|
||||
// {
|
||||
// volume.Name += $"";
|
||||
// }
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
volume.Name = $"Volume {volume.Name}";
|
||||
volume.Name = $"{volumeLabel} {volume.Name}".Trim();
|
||||
}
|
||||
|
||||
|
||||
private static string FormatChapterTitle(bool isSpecial, LibraryType libraryType, string? chapterTitle, bool withHash)
|
||||
public async Task<string> FormatChapterTitle(int userId, bool isSpecial, LibraryType libraryType, string? chapterTitle, bool withHash)
|
||||
{
|
||||
if (string.IsNullOrEmpty(chapterTitle)) throw new ArgumentException("Chapter Title cannot be null");
|
||||
|
||||
|
|
@ -520,32 +530,33 @@ public class SeriesService : ISeriesService
|
|||
var hashSpot = withHash ? "#" : string.Empty;
|
||||
return libraryType switch
|
||||
{
|
||||
LibraryType.Book => $"Book {chapterTitle}",
|
||||
LibraryType.Comic => $"Issue {hashSpot}{chapterTitle}",
|
||||
LibraryType.Manga => $"Chapter {chapterTitle}",
|
||||
_ => "Chapter "
|
||||
LibraryType.Book => await _localizationService.Translate(userId, "book-num", chapterTitle),
|
||||
LibraryType.Comic => await _localizationService.Translate(userId, "issue-num", hashSpot, chapterTitle),
|
||||
LibraryType.Manga => await _localizationService.Translate(userId, "chapter-num", chapterTitle),
|
||||
_ => await _localizationService.Translate(userId, "chapter-num", ' ')
|
||||
};
|
||||
}
|
||||
|
||||
public static string FormatChapterTitle(ChapterDto chapter, LibraryType libraryType, bool withHash = true)
|
||||
public async Task<string> FormatChapterTitle(int userId, ChapterDto chapter, LibraryType libraryType, bool withHash = true)
|
||||
{
|
||||
return FormatChapterTitle(chapter.IsSpecial, libraryType, chapter.Title, withHash);
|
||||
return await FormatChapterTitle(userId, chapter.IsSpecial, libraryType, chapter.Title, withHash);
|
||||
}
|
||||
|
||||
public static string FormatChapterTitle(Chapter chapter, LibraryType libraryType, bool withHash = true)
|
||||
public async Task<string> FormatChapterTitle(int userId, Chapter chapter, LibraryType libraryType, bool withHash = true)
|
||||
{
|
||||
return FormatChapterTitle(chapter.IsSpecial, libraryType, chapter.Title, withHash);
|
||||
return await FormatChapterTitle(userId, chapter.IsSpecial, libraryType, chapter.Title, withHash);
|
||||
}
|
||||
|
||||
public static string FormatChapterName(LibraryType libraryType, bool withHash = false)
|
||||
public async Task<string> FormatChapterName(int userId, LibraryType libraryType, bool withHash = false)
|
||||
{
|
||||
return libraryType switch
|
||||
var hashSpot = withHash ? "#" : string.Empty;
|
||||
return (libraryType switch
|
||||
{
|
||||
LibraryType.Manga => "Chapter",
|
||||
LibraryType.Comic => withHash ? "Issue #" : "Issue",
|
||||
LibraryType.Book => "Book",
|
||||
_ => "Chapter"
|
||||
};
|
||||
LibraryType.Book => await _localizationService.Translate(userId, "book-num", string.Empty),
|
||||
LibraryType.Comic => await _localizationService.Translate(userId, "issue-num", hashSpot, string.Empty),
|
||||
LibraryType.Manga => await _localizationService.Translate(userId, "chapter-num", string.Empty),
|
||||
_ => await _localizationService.Translate(userId, "chapter-num", ' ')
|
||||
}).Trim();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -40,10 +40,10 @@ public class ThemeService : IThemeService
|
|||
public async Task<string> GetContent(int themeId)
|
||||
{
|
||||
var theme = await _unitOfWork.SiteThemeRepository.GetThemeDto(themeId);
|
||||
if (theme == null) throw new KavitaException("Theme file missing or invalid");
|
||||
if (theme == null) throw new KavitaException("theme-doesnt-exist");
|
||||
var themeFile = _directoryService.FileSystem.Path.Join(_directoryService.SiteThemeDirectory, theme.FileName);
|
||||
if (string.IsNullOrEmpty(themeFile) || !_directoryService.FileSystem.File.Exists(themeFile))
|
||||
throw new KavitaException("Theme file missing or invalid");
|
||||
throw new KavitaException("theme-doesnt-exist");
|
||||
|
||||
return await _directoryService.FileSystem.File.ReadAllTextAsync(themeFile);
|
||||
}
|
||||
|
|
@ -151,7 +151,7 @@ public class ThemeService : IThemeService
|
|||
try
|
||||
{
|
||||
var theme = await _unitOfWork.SiteThemeRepository.GetThemeDto(themeId);
|
||||
if (theme == null) throw new KavitaException("Theme file missing or invalid");
|
||||
if (theme == null) throw new KavitaException("theme-doesnt-exist");
|
||||
|
||||
foreach (var siteTheme in await _unitOfWork.SiteThemeRepository.GetThemes())
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue