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": [
{