diff --git a/API.Tests/API.Tests.csproj b/API.Tests/API.Tests.csproj index a6d94e708..732696f75 100644 --- a/API.Tests/API.Tests.csproj +++ b/API.Tests/API.Tests.csproj @@ -11,7 +11,7 @@ - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/API/API.csproj b/API/API.csproj index e6444da45..f43c56602 100644 --- a/API/API.csproj +++ b/API/API.csproj @@ -64,7 +64,7 @@ - + @@ -81,7 +81,7 @@ - + @@ -92,15 +92,15 @@ - + - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/API/Controllers/LicenseController.cs b/API/Controllers/LicenseController.cs index 29d4824a7..195610f06 100644 --- a/API/Controllers/LicenseController.cs +++ b/API/Controllers/LicenseController.cs @@ -31,12 +31,7 @@ public class LicenseController( [ResponseCache(CacheProfileName = ResponseCacheProfiles.LicenseCache)] public async Task> HasValidLicense(bool forceCheck = false) { - var ret = await licenseService.HasActiveLicense(forceCheck); - if (ret) - { - await taskScheduler.ScheduleKavitaPlusTasks(); - } - return Ok(ret); + return Ok(await licenseService.HasActiveLicense(forceCheck)); } /// diff --git a/API/Controllers/MetadataController.cs b/API/Controllers/MetadataController.cs index f87456baf..8ce85e2ac 100644 --- a/API/Controllers/MetadataController.cs +++ b/API/Controllers/MetadataController.cs @@ -190,23 +190,12 @@ public class MetadataController(IUnitOfWork unitOfWork, ILocalizationService loc [ResponseCache(CacheProfileName = ResponseCacheProfiles.KavitaPlus, VaryByQueryKeys = ["seriesId"])] public async Task> GetKavitaPlusSeriesDetailData(int seriesId) { - var seriesDetail = new SeriesDetailPlusDto(); if (!await licenseService.HasActiveLicense()) { - seriesDetail.Recommendations = null; - seriesDetail.Ratings = Enumerable.Empty(); - return Ok(seriesDetail); + return Ok(null); } - seriesDetail = await metadataService.GetSeriesDetail(User.GetUserId(), seriesId); - - // Temp solution, needs to be updated with new API - // seriesDetail.Ratings = await ratingService.GetRatings(seriesId); - // seriesDetail.Reviews = await reviewService.GetReviewsForSeries(User.GetUserId(), seriesId); - // seriesDetail.Recommendations = - // await recommendationService.GetRecommendationsForSeries(User.GetUserId(), seriesId); - - return Ok(seriesDetail); + return Ok(await metadataService.GetSeriesDetail(User.GetUserId(), seriesId)); } } diff --git a/API/Data/Repositories/LibraryRepository.cs b/API/Data/Repositories/LibraryRepository.cs index 7c61bc890..a28f44441 100644 --- a/API/Data/Repositories/LibraryRepository.cs +++ b/API/Data/Repositories/LibraryRepository.cs @@ -106,6 +106,7 @@ public class LibraryRepository : ILibraryRepository return await _context.Library .Include(l => l.AppUsers) .Includes(includes) + .AsSplitQuery() .ToListAsync(); } diff --git a/API/Data/Repositories/SeriesRepository.cs b/API/Data/Repositories/SeriesRepository.cs index 5d2dc30b7..dd5a8118c 100644 --- a/API/Data/Repositories/SeriesRepository.cs +++ b/API/Data/Repositories/SeriesRepository.cs @@ -4,6 +4,7 @@ using System.Globalization; using System.Linq; using System.Text.RegularExpressions; using System.Threading.Tasks; +using API.Constants; using API.Data.ManualMigrations; using API.Data.Misc; using API.Data.Scanner; @@ -31,6 +32,7 @@ using API.Services.Tasks; using API.Services.Tasks.Scanner; using AutoMapper; using AutoMapper.QueryableExtensions; +using Microsoft.AspNetCore.Identity; using Microsoft.EntityFrameworkCore; using SQLite; @@ -152,14 +154,16 @@ public class SeriesRepository : ISeriesRepository { private readonly DataContext _context; private readonly IMapper _mapper; + private readonly UserManager _userManager; private readonly Regex _yearRegex = new Regex(@"\d{4}", RegexOptions.Compiled, Services.Tasks.Scanner.Parser.Parser.RegexTimeout); - public SeriesRepository(DataContext context, IMapper mapper) + public SeriesRepository(DataContext context, IMapper mapper, UserManager userManager) { _context = context; _mapper = mapper; + _userManager = userManager; } public void Add(Series series) @@ -462,14 +466,18 @@ public class SeriesRepository : ISeriesRepository .SelectMany(v => v.Chapters) .SelectMany(c => c.Files.Select(f => f.Id)); - result.Files = await _context.MangaFile - .Where(m => EF.Functions.Like(m.FilePath, $"%{searchQuery}%") && fileIds.Contains(m.Id)) - .AsSplitQuery() - .Take(maxRecords) - .OrderBy(f => f.FilePath) - .ProjectTo(_mapper.ConfigurationProvider) - .ToListAsync(); - + // Need to check if an admin + var user = await _context.AppUser.FirstAsync(u => u.Id == userId); + if (await _userManager.IsInRoleAsync(user, PolicyConstants.AdminRole)) + { + result.Files = await _context.MangaFile + .Where(m => EF.Functions.Like(m.FilePath, $"%{searchQuery}%") && fileIds.Contains(m.Id)) + .AsSplitQuery() + .Take(maxRecords) + .OrderBy(f => f.FilePath) + .ProjectTo(_mapper.ConfigurationProvider) + .ToListAsync(); + } result.Chapters = await _context.Chapter .Include(c => c.Files) diff --git a/API/Data/UnitOfWork.cs b/API/Data/UnitOfWork.cs index 49178cc57..ff44be8e8 100644 --- a/API/Data/UnitOfWork.cs +++ b/API/Data/UnitOfWork.cs @@ -48,7 +48,7 @@ public class UnitOfWork : IUnitOfWork _userManager = userManager; } - public ISeriesRepository SeriesRepository => new SeriesRepository(_context, _mapper); + public ISeriesRepository SeriesRepository => new SeriesRepository(_context, _mapper, _userManager); public IUserRepository UserRepository => new UserRepository(_context, _userManager, _mapper); public ILibraryRepository LibraryRepository => new LibraryRepository(_context, _mapper); diff --git a/API/Services/Plus/ExternalMetadataService.cs b/API/Services/Plus/ExternalMetadataService.cs index 158dd2a89..9b94b4ae8 100644 --- a/API/Services/Plus/ExternalMetadataService.cs +++ b/API/Services/Plus/ExternalMetadataService.cs @@ -114,11 +114,19 @@ public class ExternalMetadataService : IExternalMetadataService Reviews = result.Reviews }; } - catch (Exception e) + catch (FlurlHttpException ex) { - _logger.LogError(e, "An error happened during the request to Kavita+ API"); - return null; + if (ex.StatusCode == 404) + { + return null; + } } + catch (Exception ex) + { + _logger.LogError(ex, "An error happened during the request to Kavita+ API"); + } + + return null; } private async Task ProcessRecommendations(Series series, AppUser user, IEnumerable recs) diff --git a/API/Services/TaskScheduler.cs b/API/Services/TaskScheduler.cs index c6dfc233e..09f1a155d 100644 --- a/API/Services/TaskScheduler.cs +++ b/API/Services/TaskScheduler.cs @@ -151,9 +151,8 @@ public class TaskScheduler : ITaskScheduler { // KavitaPlus based (needs license check) var license = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.LicenseKey)).Value; - if (!await _licenseService.HasActiveSubscription(license)) + if (string.IsNullOrEmpty(license) || !await _licenseService.HasActiveSubscription(license)) { - return; } RecurringJob.AddOrUpdate(CheckScrobblingTokens, () => _scrobblingService.CheckExternalAccessTokens(), Cron.Daily, RecurringJobOptions); diff --git a/API/Services/Tasks/Scanner/ProcessSeries.cs b/API/Services/Tasks/Scanner/ProcessSeries.cs index 7815a5bcf..ce2b577dd 100644 --- a/API/Services/Tasks/Scanner/ProcessSeries.cs +++ b/API/Services/Tasks/Scanner/ProcessSeries.cs @@ -122,10 +122,16 @@ public class ProcessSeries : IProcessSeries } catch (Exception ex) { - _logger.LogError(ex, "There was an exception finding existing series for {SeriesName} with Localized name of {LocalizedName} for library {LibraryId}. This indicates you have duplicate series with same name or localized name in the library. Correct this and rescan", firstInfo.Series, firstInfo.LocalizedSeries, library.Id); + var series2 = await _unitOfWork.SeriesRepository.GetFullSeriesByAnyName(firstInfo.LocalizedSeries, string.Empty, library.Id, firstInfo.Format, false); + var details = $"Series 1: {firstInfo.Series} Series 2: {series2.Name}" + "\n" + + $"Localized: {firstInfo.LocalizedSeries} Localized: {series2.LocalizedName}" + "\n" + + $"Filename: {_directoryService.FileSystem.FileInfo.New(firstInfo.FullFilePath).Directory} Filename: {series2.FolderPath}"; + _logger.LogError(ex, "Scanner found a Series {SeriesName} which matched another Series {LocalizedName} in a different folder parallel to Library {LibraryName} root folder. This is not allowed. Please correct", + firstInfo.Series, firstInfo.LocalizedSeries, library.Name); + await _eventHub.SendMessageAsync(MessageFactory.Error, - MessageFactory.ErrorEvent($"There was an exception finding existing series for {firstInfo.Series} with Localized name of {firstInfo.LocalizedSeries} for library {library.Id}", - "This indicates you have duplicate series with same name or localized name in the library. Correct this and rescan.")); + MessageFactory.ErrorEvent($"Scanner found a Series {firstInfo.Series} which matched another Series {firstInfo.LocalizedSeries} in a different folder parallel to Library {library.Name} root folder. This is not allowed. Please correct", + details)); return; } diff --git a/API/Startup.cs b/API/Startup.cs index 939bfb586..21c4fa459 100644 --- a/API/Startup.cs +++ b/API/Startup.cs @@ -349,16 +349,26 @@ public class Startup opts.IncludeQueryInRequestPath = true; }); + var allowIframing = Configuration.AllowIFraming; + app.Use(async (context, next) => { context.Response.Headers[HeaderNames.Vary] = new[] { "Accept-Encoding" }; - // Don't let the site be iframed outside the same origin (clickjacking) - context.Response.Headers.XFrameOptions = Configuration.XFrameOptions; - // Setup CSP to ensure we load assets only from these origins - context.Response.Headers.Add("Content-Security-Policy", "frame-ancestors 'none';"); + if (!allowIframing) + { + // Don't let the site be iframed outside the same origin (clickjacking) + context.Response.Headers.XFrameOptions = "SAMEORIGIN"; + + // Setup CSP to ensure we load assets only from these origins + context.Response.Headers.Add("Content-Security-Policy", "frame-ancestors 'none';"); + } + else + { + logger.LogCritical("appsetting.json has allow iframing on! This may allow for clickjacking on the server. User beware"); + } await next(); }); diff --git a/API/config/appsettings.Development.json b/API/config/appsettings.Development.json index 90faa9e5f..0f9f05491 100644 --- a/API/config/appsettings.Development.json +++ b/API/config/appsettings.Development.json @@ -3,6 +3,5 @@ "Port": 5000, "IpAddresses": "", "BaseUrl": "/test/", - "Cache": 90, - "XFrameOrigins": "SAMEORIGIN" -} \ No newline at end of file + "Cache": 90 +} diff --git a/Kavita.Common/Configuration.cs b/Kavita.Common/Configuration.cs index e2af4c32d..ca0fc40ec 100644 --- a/Kavita.Common/Configuration.cs +++ b/Kavita.Common/Configuration.cs @@ -13,7 +13,6 @@ public static class Configuration public const string DefaultBaseUrl = "/"; public const int DefaultHttpPort = 5000; public const int DefaultTimeOutSecs = 90; - public const string DefaultXFrameOptions = "SAMEORIGIN"; public const long DefaultCacheMemory = 75; private static readonly string AppSettingsFilename = Path.Join("config", GetAppSettingFilename()); @@ -49,7 +48,7 @@ public static class Configuration set => SetCacheSize(GetAppSettingFilename(), value); } - public static string XFrameOptions => GetXFrameOptions(GetAppSettingFilename()); + public static bool AllowIFraming => GetAllowIFraming(GetAppSettingFilename()); private static string GetAppSettingFilename() { @@ -293,26 +292,21 @@ public static class Configuration #endregion - #region XFrameOrigins - private static string GetXFrameOptions(string filePath) + #region AllowIFraming + private static bool GetAllowIFraming(string filePath) { - if (OsInfo.IsDocker) - { - return DefaultBaseUrl; - } - try { var json = File.ReadAllText(filePath); var jsonObj = JsonSerializer.Deserialize(json); - return !string.IsNullOrEmpty(jsonObj.XFrameOrigins) ? jsonObj.XFrameOrigins : DefaultXFrameOptions; + return jsonObj.AllowIFraming; } catch (Exception ex) { Console.WriteLine("Error reading app settings: " + ex.Message); } - return DefaultXFrameOptions; + return false; } #endregion @@ -328,6 +322,6 @@ public static class Configuration // ReSharper disable once MemberHidesStaticFromOuterClass public long Cache { get; set; } = DefaultCacheMemory; // ReSharper disable once MemberHidesStaticFromOuterClass - public string XFrameOrigins { get; set; } = DefaultXFrameOptions; + public bool AllowIFraming { get; set; } = false; } } diff --git a/Kavita.Common/Kavita.Common.csproj b/Kavita.Common/Kavita.Common.csproj index 477e20ae3..4830a7d65 100644 --- a/Kavita.Common/Kavita.Common.csproj +++ b/Kavita.Common/Kavita.Common.csproj @@ -14,7 +14,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/UI/Web/hash-localization.js b/UI/Web/hash-localization.js new file mode 100644 index 000000000..63c2f73b3 --- /dev/null +++ b/UI/Web/hash-localization.js @@ -0,0 +1,24 @@ +const crypto = require('crypto'); +const fs = require('fs'); +const glob = require('glob'); + +const jsonFilesDir = 'dist/browser/assets/langs/'; // Adjust the path to your JSON files +const outputDir = 'dist/browser/assets/langs'; // Directory to store minified files + +function generateChecksum(str, algorithm, encoding) { + return crypto + .createHash(algorithm || 'md5') + .update(str, 'utf8') + .digest(encoding || 'hex'); +} + +const result = {}; + +glob.sync(`${jsonFilesDir}**/*.json`).forEach(path => { + const [_, lang] = path.split('dist\\browser\\assets\\langs\\'); + const content = fs.readFileSync(path, { encoding: 'utf-8' }); + result[lang.replace('.json', '')] = generateChecksum(content); +}); + +fs.writeFileSync('./i18n-cache-busting.json', JSON.stringify(result)); +fs.writeFileSync(`dist/browser/i18n-cache-busting.json`, JSON.stringify(result)); diff --git a/UI/Web/i18n-cache-busting.json b/UI/Web/i18n-cache-busting.json new file mode 100644 index 000000000..bba3cf429 --- /dev/null +++ b/UI/Web/i18n-cache-busting.json @@ -0,0 +1 @@ +{"zh_Hant":"05191aaae25a26a8597559e8318f97db","zh_Hans":"ca663a190b259b41ac365b6b5537558e","uk":"ccf59f571821ab842882378395ccf48c","tr":"5d6427179210cc370400b816c9d1116d","th":"1e27a1e1cadb2b9f92d85952bffaab95","sk":"24de417448b577b4899e917b70a43263","ru":"c547f0995c167817dd2408e4e9279de2","pt_BR":"44c7cd3da6baad38887fb03ac4ec5581","pt":"af4162a48f01c5260d6436e7e000c5ef","pl":"c6488fdb9a1ecfe5cde6bd1c264902aa","nl":"3ff322f7b24442bd6bceb5c692146d4f","nb_NO":"99914b932bd37a50b983c5e7c90ae93b","ms":"9fdfcc11a2e8a58a4baa691b93d93ff7","ko":"447e24f9f60e1b9f36bc0b087d059dbd","ja":"2f46a5ad1364a71255dd76c0094a9264","it":"4ef0a0ef56bab4650eda37e0dd841982","id":"0beab79883a28035c393768fb8c8ecbd","hi":"d850bb49ec6b5a5ccf9986823f095ab8","fr":"b41b7065960b431a9833140e9014189e","es":"151b6c17ef7382da9f0f22b87a346b7d","en":"d07463979db4cc7ab6e0089889cfc730","de":"f8e3ec31790044be07e222703ed0575a","cs":"0f0c433b9fd641977e89a42e92e4b884"} \ No newline at end of file diff --git a/UI/Web/package.json b/UI/Web/package.json index d758b1ae0..9a289ad94 100644 --- a/UI/Web/package.json +++ b/UI/Web/package.json @@ -3,10 +3,11 @@ "version": "0.7.12.1", "scripts": { "ng": "ng", - "start": "ng serve", - "build": "ng build", + "start": "npm run cache-langs && ng serve", + "build": "npm run cache-langs && ng build", "minify-langs": "node minify-json.js", - "prod": "ng build --configuration production && npm run minify-langs", + "cache-langs": "node hash-localization.js", + "prod": "ng build --configuration production && npm run minify-langs && npm run cache-langs", "explore": "ng build --stats-json && webpack-bundle-analyzer dist/stats.json", "lint": "ng lint", "e2e": "ng e2e" diff --git a/UI/Web/src/app/app.component.ts b/UI/Web/src/app/app.component.ts index 351e2ff10..85a43eb6d 100644 --- a/UI/Web/src/app/app.component.ts +++ b/UI/Web/src/app/app.component.ts @@ -93,6 +93,8 @@ export class AppComponent implements OnInit { // Bootstrap anything that's needed this.themeService.getThemes().subscribe(); this.libraryService.getLibraryNames().pipe(take(1), shareReplay({refCount: true, bufferSize: 1})).subscribe(); + // On load, make an initial call for valid license + this.accountService.hasValidLicense().subscribe(); } } } diff --git a/UI/Web/src/app/cards/card-item/card-item.component.ts b/UI/Web/src/app/cards/card-item/card-item.component.ts index b38ebdf8a..b9d20e602 100644 --- a/UI/Web/src/app/cards/card-item/card-item.component.ts +++ b/UI/Web/src/app/cards/card-item/card-item.component.ts @@ -277,7 +277,6 @@ export class CardItemComponent implements OnInit { }); this.download$ = this.downloadService.activeDownloads$.pipe(takeUntilDestroyed(this.destroyRef), map((events) => { - console.log('Card Item download obv called for entity: ', this.entity); return this.downloadService.mapToEntityType(events, this.entity); })); } diff --git a/UI/Web/src/app/nav/_components/events-widget/events-widget.component.ts b/UI/Web/src/app/nav/_components/events-widget/events-widget.component.ts index 1c6bb7acb..100790efb 100644 --- a/UI/Web/src/app/nav/_components/events-widget/events-widget.component.ts +++ b/UI/Web/src/app/nav/_components/events-widget/events-widget.component.ts @@ -61,7 +61,7 @@ export class EventsWidgetComponent implements OnInit, OnDestroy { activeEvents: number = 0; - debugMode: boolean = true; + debugMode: boolean = false; protected readonly EVENTS = EVENTS; diff --git a/UI/Web/src/app/series-detail/_components/series-detail/series-detail.component.ts b/UI/Web/src/app/series-detail/_components/series-detail/series-detail.component.ts index f534f42e9..52007246b 100644 --- a/UI/Web/src/app/series-detail/_components/series-detail/series-detail.component.ts +++ b/UI/Web/src/app/series-detail/_components/series-detail/series-detail.component.ts @@ -692,16 +692,6 @@ export class SeriesDetailComponent implements OnInit, AfterContentChecked { } } - // loadRecommendations() { - // this.seriesService.getRecommendationsForSeries(this.seriesId).subscribe(rec => { - // rec.ownedSeries.map(r => { - // this.seriesService.getMetadata(r.id).subscribe(m => r.summary = m.summary); - // }); - // this.combinedRecs = [...rec.ownedSeries, ...rec.externalSeries]; - // this.hasRecommendations = this.combinedRecs.length > 0; - // this.cdRef.markForCheck(); - // }); - // } loadPlusMetadata(seriesId: number) { this.metadataService.getSeriesMetadataFromPlus(seriesId).subscribe(data => { diff --git a/UI/Web/src/app/user-settings/anilist-key/anilist-key.component.html b/UI/Web/src/app/user-settings/anilist-key/anilist-key.component.html index 92c89f73f..237ebf1c1 100644 --- a/UI/Web/src/app/user-settings/anilist-key/anilist-key.component.html +++ b/UI/Web/src/app/user-settings/anilist-key/anilist-key.component.html @@ -9,8 +9,8 @@ {{t('token-valid')}} } @else { - - {{t('token-not-valid')}} + + {{t('token-expired')}} } diff --git a/UI/Web/src/app/user-settings/user-preferences/user-preferences.component.html b/UI/Web/src/app/user-settings/user-preferences/user-preferences.component.html index 1e35824be..2d538d2cf 100644 --- a/UI/Web/src/app/user-settings/user-preferences/user-preferences.component.html +++ b/UI/Web/src/app/user-settings/user-preferences/user-preferences.component.html @@ -436,8 +436,10 @@ } @defer (when tab.fragment === FragmentID.Scrobbling; prefetch on idle) { - - + @if(hasActiveLicense) { + + + } } diff --git a/UI/Web/src/app/user-settings/user-preferences/user-preferences.component.ts b/UI/Web/src/app/user-settings/user-preferences/user-preferences.component.ts index 6516be783..31d749d46 100644 --- a/UI/Web/src/app/user-settings/user-preferences/user-preferences.component.ts +++ b/UI/Web/src/app/user-settings/user-preferences/user-preferences.component.ts @@ -81,6 +81,19 @@ enum FragmentID { }) export class UserPreferencesComponent implements OnInit, OnDestroy { + private readonly destroyRef = inject(DestroyRef); + private readonly accountService = inject(AccountService); + private readonly toastr = inject(ToastrService); + private readonly bookService = inject(BookService); + private readonly titleService = inject(Title); + private readonly route = inject(ActivatedRoute); + private readonly settingsService = inject(SettingsService); + private readonly router = inject(Router); + private readonly cdRef = inject(ChangeDetectorRef); + private readonly localizationService = inject(LocalizationService); + protected readonly AccordionPanelID = AccordionPanelID; + protected readonly FragmentID = FragmentID; + readingDirectionsTranslated = readingDirections.map(this.translatePrefOptions); scalingOptionsTranslated = scalingOptions.map(this.translatePrefOptions); pageSplitOptionsTranslated = pageSplitOptions.map(this.translatePrefOptions); @@ -115,19 +128,9 @@ export class UserPreferencesComponent implements OnInit, OnDestroy { opdsEnabled: boolean = false; opdsUrl: string = ''; makeUrl: (val: string) => string = (val: string) => { return this.opdsUrl; }; + hasActiveLicense = false; + - private readonly destroyRef = inject(DestroyRef); - private readonly accountService = inject(AccountService); - private readonly toastr = inject(ToastrService); - private readonly bookService = inject(BookService); - private readonly titleService = inject(Title); - private readonly route = inject(ActivatedRoute); - private readonly settingsService = inject(SettingsService); - private readonly router = inject(Router); - private readonly cdRef = inject(ChangeDetectorRef); - private readonly localizationService = inject(LocalizationService); - protected readonly AccordionPanelID = AccordionPanelID; - protected readonly FragmentID = FragmentID; constructor() { @@ -144,9 +147,12 @@ export class UserPreferencesComponent implements OnInit, OnDestroy { this.cdRef.markForCheck(); }); - this.accountService.hasValidLicense().subscribe(res => { + + + this.accountService.hasValidLicense$.pipe(take(1), takeUntilDestroyed(this.destroyRef)).subscribe(res => { if (res) { this.tabs.push({title: 'scrobbling-tab', fragment: FragmentID.Scrobbling}); + this.hasActiveLicense = true; this.cdRef.markForCheck(); } @@ -159,7 +165,7 @@ export class UserPreferencesComponent implements OnInit, OnDestroy { } this.cdRef.markForCheck(); }); - }) + }); this.settingsService.getOpdsEnabled().subscribe(res => { diff --git a/UI/Web/src/assets/langs/en.json b/UI/Web/src/assets/langs/en.json index bc952e62d..25e1507a6 100644 --- a/UI/Web/src/assets/langs/en.json +++ b/UI/Web/src/assets/langs/en.json @@ -284,6 +284,7 @@ "scrobbling-providers": { "title": "Scrobbling Providers", "requires": "This feature requires an active {{product}} license", + "token-valid": "Token Valid", "token-expired": "Token Expired", "no-token-set": "No Token Set", "token-set": "Token Set", diff --git a/UI/Web/src/httpLoader.ts b/UI/Web/src/httpLoader.ts index d3c2de6b7..85cb2ec95 100644 --- a/UI/Web/src/httpLoader.ts +++ b/UI/Web/src/httpLoader.ts @@ -1,7 +1,7 @@ import {Injectable} from "@angular/core"; import {HttpClient} from "@angular/common/http"; import {Translation, TranslocoLoader} from "@ngneat/transloco"; - +import cacheBusting from 'i18n-cache-busting.json'; // allowSyntheticDefaultImports must be true @Injectable({ providedIn: 'root' }) export class HttpLoader implements TranslocoLoader { @@ -9,6 +9,7 @@ export class HttpLoader implements TranslocoLoader { getTranslation(langPath: string) { const tokens = langPath.split('/'); - return this.http.get(`assets/langs/${tokens[tokens.length - 1]}.json`); + const langCode = tokens[tokens.length - 1]; + return this.http.get(`assets/langs/${langCode}.json?v=${(cacheBusting as { [key: string]: string })[langCode]}`); } } diff --git a/UI/Web/tsconfig.json b/UI/Web/tsconfig.json index 62185ece0..66fe74ab0 100644 --- a/UI/Web/tsconfig.json +++ b/UI/Web/tsconfig.json @@ -19,6 +19,7 @@ "target": "ES2022", "module": "ES2022", "useDefineForClassFields": false, + "allowSyntheticDefaultImports": true, "lib": [ "ES2022", "dom" @@ -31,9 +32,10 @@ "strictTemplates": true, "esModuleInterop": true, "allowSyntheticDefaultImports": true, + "resolveJsonModule": true, "extendedDiagnostics": { "nullishCoalescingNotNullable": "warning", } - + } } diff --git a/openapi.json b/openapi.json index e8cc0e0c6..d3396ce87 100644 --- a/openapi.json +++ b/openapi.json @@ -7,7 +7,7 @@ "name": "GPL-3.0", "url": "https://github.com/Kareadita/Kavita/blob/develop/LICENSE" }, - "version": "0.7.12.4" + "version": "0.7.12.5" }, "servers": [ {