Compare commits
20 commits
develop
...
bugfix/web
Author | SHA1 | Date | |
---|---|---|---|
![]() |
d7fc15ce81 | ||
![]() |
5e2bf7fc84 | ||
![]() |
ca548e8634 | ||
![]() |
880361685b | ||
![]() |
851acb74e0 | ||
![]() |
ad7d7f82e6 | ||
![]() |
d5128b989a | ||
![]() |
63bc25f12d | ||
![]() |
f2befdd7be | ||
![]() |
d48e59384f | ||
![]() |
c6da752d2d | ||
![]() |
da13d0d04d | ||
![]() |
47ec5f0171 | ||
![]() |
7b35990e40 | ||
![]() |
0a2604dbd4 | ||
![]() |
89452cc924 | ||
![]() |
2dbaf555a5 | ||
![]() |
65bc37892d | ||
![]() |
3c486e7c8a | ||
![]() |
d2c3bd7f1c |
32 changed files with 511 additions and 152 deletions
|
@ -44,6 +44,17 @@ public class SmartFilterHelperTests
|
|||
AssertStatementSame(list[0], FilterField.Genres, FilterComparison.Equal, "95");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Test_Decode2()
|
||||
{
|
||||
const string encoded = """
|
||||
name=Test%202&stmts=comparison%253D10%25C2%25A6field%253D1%25C2%25A6value%253DA%EF%BF%BDcomparison%253D0%25C2%25A6field%253D19%25C2%25A6value%253D11&sortOptions=sortField%3D1%C2%A6isAscending%3DTrue&limitTo=0&combination=1
|
||||
""";
|
||||
|
||||
var filter = SmartFilterHelper.Decode(encoded);
|
||||
Assert.True(filter.SortOptions.IsAscending);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Test_EncodeDecode()
|
||||
{
|
||||
|
|
|
@ -83,6 +83,7 @@ public class MangaParserTests
|
|||
[InlineData("시즌34삽화2", "34")]
|
||||
[InlineData("Accel World Chapter 001 Volume 002", "2")]
|
||||
[InlineData("Accel World Volume 2", "2")]
|
||||
[InlineData("Nagasarete Airantou - Vol. 30 Ch. 187.5 - Vol.31 Omake", "30")]
|
||||
public void ParseVolumeTest(string filename, string expected)
|
||||
{
|
||||
Assert.Equal(expected, API.Services.Tasks.Scanner.Parser.Parser.ParseVolume(filename));
|
||||
|
@ -204,6 +205,7 @@ public class MangaParserTests
|
|||
[InlineData("죠시라쿠! 2년 후 1권", "죠시라쿠! 2년 후")]
|
||||
[InlineData("test 2 years 1권", "test 2 years")]
|
||||
[InlineData("test 2 years 1화", "test 2 years")]
|
||||
[InlineData("Nagasarete Airantou - Vol. 30 Ch. 187.5 - Vol.30 Omake", "Nagasarete Airantou")]
|
||||
public void ParseSeriesTest(string filename, string expected)
|
||||
{
|
||||
Assert.Equal(expected, API.Services.Tasks.Scanner.Parser.Parser.ParseSeries(filename));
|
||||
|
|
|
@ -2,13 +2,23 @@
|
|||
using Xunit;
|
||||
|
||||
namespace API.Tests.Services;
|
||||
#nullable enable
|
||||
|
||||
public class ScrobblingServiceTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData("https://anilist.co/manga/35851/Byeontaega-Doeja/", 35851)]
|
||||
public void CanParseWeblink(string link, long expectedId)
|
||||
[InlineData("https://anilist.co/manga/30105", 30105)]
|
||||
[InlineData("https://anilist.co/manga/30105/Kekkaishi/", 30105)]
|
||||
public void CanParseWeblink_AniList(string link, int? expectedId)
|
||||
{
|
||||
Assert.Equal(ScrobblingService.ExtractId<long>(link, ScrobblingService.AniListWeblinkWebsite), expectedId);
|
||||
Assert.Equal(ScrobblingService.ExtractId<int?>(link, ScrobblingService.AniListWeblinkWebsite), expectedId);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("https://mangadex.org/title/316d3d09-bb83-49da-9d90-11dc7ce40967/honzuki-no-gekokujou-shisho-ni-naru-tame-ni-wa-shudan-wo-erandeiraremasen-dai-3-bu-ryouchi-ni-hon-o", "316d3d09-bb83-49da-9d90-11dc7ce40967")]
|
||||
public void CanParseWeblink_MangaDex(string link, string expectedId)
|
||||
{
|
||||
Assert.Equal(ScrobblingService.ExtractId<string?>(link, ScrobblingService.MangaDexWeblinkWebsite), expectedId);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -26,6 +26,7 @@ using Microsoft.AspNetCore.Mvc;
|
|||
using Microsoft.AspNetCore.RateLimiting;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using SharpCompress;
|
||||
|
||||
namespace API.Controllers;
|
||||
|
||||
|
@ -137,8 +138,7 @@ public class AccountController : BaseApiController
|
|||
if (!result.Succeeded) return BadRequest(result.Errors);
|
||||
|
||||
// Assign default streams
|
||||
user.DashboardStreams = Seed.DefaultStreams.ToList();
|
||||
user.SideNavStreams = Seed.DefaultSideNavStreams.ToList();
|
||||
AddDefaultStreamsToUser(user);
|
||||
|
||||
var token = await _userManager.GenerateEmailConfirmationTokenAsync(user);
|
||||
if (string.IsNullOrEmpty(token)) return BadRequest(await _localizationService.Get("en", "confirm-token-gen"));
|
||||
|
@ -608,7 +608,8 @@ public class AccountController : BaseApiController
|
|||
}
|
||||
|
||||
// Create a new user
|
||||
var user = new AppUserBuilder(dto.Email, dto.Email, await _unitOfWork.SiteThemeRepository.GetDefaultTheme()).Build();
|
||||
var user = new AppUserBuilder(dto.Email, dto.Email,
|
||||
await _unitOfWork.SiteThemeRepository.GetDefaultTheme()).Build();
|
||||
_unitOfWork.UserRepository.Add(user);
|
||||
try
|
||||
{
|
||||
|
@ -616,9 +617,7 @@ public class AccountController : BaseApiController
|
|||
if (!result.Succeeded) return BadRequest(result.Errors);
|
||||
|
||||
// Assign default streams
|
||||
user.DashboardStreams = Seed.DefaultStreams.ToList();
|
||||
user.SideNavStreams = Seed.DefaultSideNavStreams.ToList();
|
||||
|
||||
AddDefaultStreamsToUser(user);
|
||||
|
||||
// Assign Roles
|
||||
var roles = dto.Roles;
|
||||
|
@ -657,7 +656,6 @@ public class AccountController : BaseApiController
|
|||
user.CreateSideNavFromLibrary(lib);
|
||||
}
|
||||
|
||||
_unitOfWork.UserRepository.Update(user);
|
||||
user.AgeRestriction = hasAdminRole ? AgeRating.NotApplicable : dto.AgeRestriction.AgeRating;
|
||||
user.AgeRestrictionIncludeUnknowns = hasAdminRole || dto.AgeRestriction.IncludeUnknowns;
|
||||
|
||||
|
@ -669,6 +667,7 @@ public class AccountController : BaseApiController
|
|||
}
|
||||
|
||||
user.ConfirmationToken = token;
|
||||
_unitOfWork.UserRepository.Update(user);
|
||||
await _unitOfWork.CommitAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
@ -702,7 +701,7 @@ public class AccountController : BaseApiController
|
|||
BackgroundJob.Enqueue(() => _emailService.SendConfirmationEmail(new ConfirmationEmailDto()
|
||||
{
|
||||
EmailAddress = dto.Email,
|
||||
InvitingUser = adminUser.UserName!,
|
||||
InvitingUser = adminUser.UserName,
|
||||
ServerConfirmationLink = emailLink
|
||||
}));
|
||||
}
|
||||
|
@ -721,6 +720,19 @@ public class AccountController : BaseApiController
|
|||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-invite-user"));
|
||||
}
|
||||
|
||||
private void AddDefaultStreamsToUser(AppUser user)
|
||||
{
|
||||
foreach (var newStream in Seed.DefaultStreams.Select(stream => _mapper.Map<AppUserDashboardStream, AppUserDashboardStream>(stream)))
|
||||
{
|
||||
user.DashboardStreams.Add(newStream);
|
||||
}
|
||||
|
||||
foreach (var stream in Seed.DefaultSideNavStreams.Select(stream => _mapper.Map<AppUserSideNavStream, AppUserSideNavStream>(stream)))
|
||||
{
|
||||
user.SideNavStreams.Add(stream);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Last step in authentication flow, confirms the email token for email
|
||||
/// </summary>
|
||||
|
|
|
@ -115,10 +115,6 @@ public class ReaderController : BaseApiController
|
|||
|
||||
try
|
||||
{
|
||||
if (new Random().Next(1, 10) > 5)
|
||||
{
|
||||
await Task.Delay(1000);
|
||||
}
|
||||
var chapter = await _cacheService.Ensure(chapterId, extractPdf);
|
||||
if (chapter == null) return NoContent();
|
||||
_logger.LogInformation("Fetching Page {PageNum} on Chapter {ChapterId}", page, chapterId);
|
||||
|
|
|
@ -76,48 +76,42 @@ public static class Seed
|
|||
},
|
||||
}.ToArray());
|
||||
|
||||
public static readonly ImmutableArray<AppUserSideNavStream> DefaultSideNavStreams = ImmutableArray.Create(new[]
|
||||
public static readonly ImmutableArray<AppUserSideNavStream> DefaultSideNavStreams = ImmutableArray.Create(
|
||||
new AppUserSideNavStream()
|
||||
{
|
||||
new AppUserSideNavStream()
|
||||
{
|
||||
Name = "want-to-read",
|
||||
StreamType = SideNavStreamType.WantToRead,
|
||||
Order = 1,
|
||||
IsProvided = true,
|
||||
Visible = true
|
||||
},
|
||||
new AppUserSideNavStream()
|
||||
{
|
||||
Name = "collections",
|
||||
StreamType = SideNavStreamType.Collections,
|
||||
Order = 2,
|
||||
IsProvided = true,
|
||||
Visible = true
|
||||
},
|
||||
new AppUserSideNavStream()
|
||||
{
|
||||
Name = "reading-lists",
|
||||
StreamType = SideNavStreamType.ReadingLists,
|
||||
Order = 3,
|
||||
IsProvided = true,
|
||||
Visible = true
|
||||
},
|
||||
new AppUserSideNavStream()
|
||||
{
|
||||
Name = "bookmarks",
|
||||
StreamType = SideNavStreamType.Bookmarks,
|
||||
Order = 4,
|
||||
IsProvided = true,
|
||||
Visible = true
|
||||
},
|
||||
new AppUserSideNavStream()
|
||||
{
|
||||
Name = "all-series",
|
||||
StreamType = SideNavStreamType.AllSeries,
|
||||
Order = 5,
|
||||
IsProvided = true,
|
||||
Visible = true
|
||||
}
|
||||
Name = "want-to-read",
|
||||
StreamType = SideNavStreamType.WantToRead,
|
||||
Order = 1,
|
||||
IsProvided = true,
|
||||
Visible = true
|
||||
}, new AppUserSideNavStream()
|
||||
{
|
||||
Name = "collections",
|
||||
StreamType = SideNavStreamType.Collections,
|
||||
Order = 2,
|
||||
IsProvided = true,
|
||||
Visible = true
|
||||
}, new AppUserSideNavStream()
|
||||
{
|
||||
Name = "reading-lists",
|
||||
StreamType = SideNavStreamType.ReadingLists,
|
||||
Order = 3,
|
||||
IsProvided = true,
|
||||
Visible = true
|
||||
}, new AppUserSideNavStream()
|
||||
{
|
||||
Name = "bookmarks",
|
||||
StreamType = SideNavStreamType.Bookmarks,
|
||||
Order = 4,
|
||||
IsProvided = true,
|
||||
Visible = true
|
||||
}, new AppUserSideNavStream()
|
||||
{
|
||||
Name = "all-series",
|
||||
StreamType = SideNavStreamType.AllSeries,
|
||||
Order = 5,
|
||||
IsProvided = true,
|
||||
Visible = true
|
||||
});
|
||||
|
||||
|
||||
|
|
|
@ -240,9 +240,10 @@ public class AutoMapperProfiles : Profile
|
|||
|
||||
CreateMap<AppUserSmartFilter, SmartFilterDto>();
|
||||
CreateMap<AppUserDashboardStream, DashboardStreamDto>();
|
||||
// CreateMap<AppUserDashboardStream, DashboardStreamDto>()
|
||||
// .ForMember(dest => dest.SmartFilterEncoded,
|
||||
// opt => opt.MapFrom(src => src.SmartFilter));
|
||||
|
||||
// This is for cloning to ensure the records don't get overwritten when setting from SeedData
|
||||
CreateMap<AppUserDashboardStream, AppUserDashboardStream>();
|
||||
CreateMap<AppUserSideNavStream, AppUserSideNavStream>();
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -27,6 +27,8 @@ public class PlusSeriesDtoBuilder : IEntityBuilder<PlusSeriesDto>
|
|||
ScrobblingService.MalWeblinkWebsite),
|
||||
GoogleBooksId = ScrobblingService.ExtractId<string?>(series.Metadata.WebLinks,
|
||||
ScrobblingService.GoogleBooksWeblinkWebsite),
|
||||
MangaDexId = ScrobblingService.ExtractId<string?>(series.Metadata.WebLinks,
|
||||
ScrobblingService.MangaDexWeblinkWebsite),
|
||||
VolumeCount = series.Volumes.Count,
|
||||
ChapterCount = series.Volumes.SelectMany(v => v.Chapters).Count(c => !c.IsSpecial),
|
||||
Year = series.Metadata.ReleaseYear
|
||||
|
|
|
@ -133,7 +133,7 @@ public static class SmartFilterHelper
|
|||
var sortFieldPart = parts.FirstOrDefault(part => part.StartsWith(SortFieldKey));
|
||||
var isAscendingPart = parts.FirstOrDefault(part => part.StartsWith(IsAscendingKey));
|
||||
|
||||
var isAscending = isAscendingPart?.Substring(11).Equals("true", StringComparison.OrdinalIgnoreCase) ?? false;
|
||||
var isAscending = isAscendingPart?.Trim().Replace(IsAscendingKey, string.Empty).Equals("true", StringComparison.OrdinalIgnoreCase) ?? false;
|
||||
if (sortFieldPart == null)
|
||||
{
|
||||
return new SortOptions();
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"register-user": "Quelque chose s'est mal passé lors de l'enregistrement de l'utilisateur",
|
||||
"denied": "Non autorisé",
|
||||
"denied": "Interdit",
|
||||
"permission-denied": "Vous n'êtes pas autorisé à cette opération",
|
||||
"disabled-account": "Votre compte a été désactivé. Veuillez contacter un administrateur.",
|
||||
"confirm-email": "Vous devez d'abord confirmer votre email",
|
||||
|
|
|
@ -170,5 +170,7 @@
|
|||
"external-sources": "Sorgenti Esterne",
|
||||
"external-source-required": "ApiKey e Host sono obbligatori",
|
||||
"external-source-already-in-use": "Esiste uno stream esistente con questa Sorgente Esterna",
|
||||
"smart-filter-already-in-use": "Esiste uno stream esistente con questo Filtro Intelligente"
|
||||
"smart-filter-already-in-use": "Esiste uno stream esistente con questo Filtro Intelligente",
|
||||
"dashboard-stream-doesnt-exist": "Dashboard Stream non esiste",
|
||||
"sidenav-stream-doesnt-exist": "SideNav Stream non esiste"
|
||||
}
|
||||
|
|
|
@ -25,6 +25,7 @@ public record PlusSeriesDto
|
|||
public int? AniListId { get; set; }
|
||||
public long? MalId { get; set; }
|
||||
public string? GoogleBooksId { get; set; }
|
||||
public string? MangaDexId { get; set; }
|
||||
public string SeriesName { get; set; }
|
||||
public string? AltSeriesName { get; set; }
|
||||
public MediaFormat MediaFormat { get; set; }
|
||||
|
|
|
@ -67,13 +67,14 @@ public class ScrobblingService : IScrobblingService
|
|||
public const string AniListWeblinkWebsite = "https://anilist.co/manga/";
|
||||
public const string MalWeblinkWebsite = "https://myanimelist.net/manga/";
|
||||
public const string GoogleBooksWeblinkWebsite = "https://books.google.com/books?id=";
|
||||
public const string MangaDexWeblinkWebsite = "https://mangadex.org/title/";
|
||||
|
||||
private static readonly IDictionary<string, int> WeblinkExtractionMap = new Dictionary<string, int>()
|
||||
{
|
||||
{AniListWeblinkWebsite, 0},
|
||||
{MalWeblinkWebsite, 0},
|
||||
{GoogleBooksWeblinkWebsite, 0},
|
||||
|
||||
{MangaDexWeblinkWebsite, 0},
|
||||
};
|
||||
|
||||
private const int ScrobbleSleepTime = 700; // We can likely tie this to AniList's 90 rate / min ((60 * 1000) / 90)
|
||||
|
@ -829,12 +830,12 @@ public class ScrobblingService : IScrobblingService
|
|||
if (!webLink.StartsWith(website)) continue;
|
||||
var tokens = webLink.Split(website)[1].Split('/');
|
||||
var value = tokens[index];
|
||||
if (typeof(T) == typeof(int))
|
||||
if (typeof(T) == typeof(int?))
|
||||
{
|
||||
if (int.TryParse(value, out var intValue))
|
||||
return (T)(object)intValue;
|
||||
}
|
||||
else if (typeof(T) == typeof(long))
|
||||
else if (typeof(T) == typeof(long?))
|
||||
{
|
||||
if (long.TryParse(value, out var longValue))
|
||||
return (T)(object)longValue;
|
||||
|
|
|
@ -34,7 +34,6 @@ public interface ITaskScheduler
|
|||
void ScanSiteThemes();
|
||||
void CovertAllCoversToEncoding();
|
||||
Task CleanupDbEntries();
|
||||
Task ScrobbleUpdates(int userId);
|
||||
|
||||
}
|
||||
public class TaskScheduler : ITaskScheduler
|
||||
|
@ -141,7 +140,6 @@ public class TaskScheduler : ITaskScheduler
|
|||
}
|
||||
|
||||
RecurringJob.AddOrUpdate(CleanupTaskId, () => _cleanupService.Cleanup(), Cron.Daily, RecurringJobOptions);
|
||||
RecurringJob.AddOrUpdate(CleanupDbTaskId, () => _cleanupService.CleanupDbEntries(), Cron.Daily, RecurringJobOptions);
|
||||
RecurringJob.AddOrUpdate(RemoveFromWantToReadTaskId, () => _cleanupService.CleanupWantToRead(), Cron.Daily, RecurringJobOptions);
|
||||
RecurringJob.AddOrUpdate(UpdateYearlyStatsTaskId, () => _statisticService.UpdateServerStatistics(), Cron.Monthly, RecurringJobOptions);
|
||||
|
||||
|
@ -272,16 +270,6 @@ public class TaskScheduler : ITaskScheduler
|
|||
await _cleanupService.CleanupDbEntries();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// TODO: Remove this for Release
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public async Task ScrobbleUpdates(int userId)
|
||||
{
|
||||
if (!await _licenseService.HasActiveLicense()) return;
|
||||
BackgroundJob.Enqueue(() => _scrobblingService.ProcessUpdatesSinceLastSync());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to call ScanLibraries on ScannerService, but if another scan task is in progress, will reschedule the invocation for 3 hours in future.
|
||||
/// </summary>
|
||||
|
|
|
@ -9,6 +9,7 @@ using API.Entities.Enums;
|
|||
using API.Logging;
|
||||
using API.SignalR;
|
||||
using Hangfire;
|
||||
using Kavita.Common.EnvironmentInfo;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace API.Services.Tasks;
|
||||
|
@ -91,7 +92,7 @@ public class BackupService : IBackupService
|
|||
await SendProgress(0.1F, "Copying core files");
|
||||
|
||||
var dateString = $"{DateTime.UtcNow.ToShortDateString()}_{DateTime.UtcNow.ToLongTimeString()}".Replace("/", "_").Replace(":", "_");
|
||||
var zipPath = _directoryService.FileSystem.Path.Join(backupDirectory, $"kavita_backup_{dateString}.zip");
|
||||
var zipPath = _directoryService.FileSystem.Path.Join(backupDirectory, $"kavita_backup_{dateString}_v{BuildInfo.Version}.zip");
|
||||
|
||||
if (File.Exists(zipPath))
|
||||
{
|
||||
|
|
|
@ -92,6 +92,8 @@ public class CleanupService : ICleanupService
|
|||
await CleanupLogs();
|
||||
await SendProgress(0.9F, "Cleaning progress events that exceed 100%");
|
||||
await EnsureChapterProgressIsCapped();
|
||||
await SendProgress(0.95F, "Cleaning abandoned database rows");
|
||||
await CleanupDbEntries();
|
||||
await SendProgress(1F, "Cleanup finished");
|
||||
_logger.LogInformation("Cleanup finished");
|
||||
}
|
||||
|
|
|
@ -109,9 +109,9 @@ public static class Parser
|
|||
new Regex(
|
||||
@"(?<Series>.*)(\b|_)v(?<Volume>\d+-?\d+)( |_)",
|
||||
MatchOptions, RegexTimeout),
|
||||
// NEEDLESS_Vol.4_-Simeon_6_v2[SugoiSugoi].rar
|
||||
// Nagasarete Airantou - Vol. 30 Ch. 187.5 - Vol.31 Omake
|
||||
new Regex(
|
||||
@"(?<Series>.*)(\b|_)(?!\[)(vol\.?)(?<Volume>\d+(-\d+)?)(?!\])",
|
||||
@"^(?<Series>.+?)(\s*Chapter\s*\d+)?(\s|_|\-\s)+(Vol(ume)?\.?(\s|_)?)(?<Volume>\d+(\.\d+)?)(.+?|$)",
|
||||
MatchOptions, RegexTimeout),
|
||||
// Historys Strongest Disciple Kenichi_v11_c90-98.zip or Dance in the Vampire Bund v16-17
|
||||
new Regex(
|
||||
|
@ -137,6 +137,7 @@ public static class Parser
|
|||
new Regex(
|
||||
@"(vol_)(?<Volume>\d+(\.\d)?)",
|
||||
MatchOptions, RegexTimeout),
|
||||
|
||||
// Chinese Volume: 第n卷 -> Volume n, 第n册 -> Volume n, 幽游白书完全版 第03卷 天下 or 阿衰online 第1册
|
||||
new Regex(
|
||||
@"第(?<Volume>\d+)(卷|册)",
|
||||
|
@ -197,16 +198,17 @@ public static class Parser
|
|||
new Regex(
|
||||
@"(?<Series>.*)(\b|_|-|\s)(?:sp)\d",
|
||||
MatchOptions, RegexTimeout),
|
||||
// [SugoiSugoi]_NEEDLESS_Vol.2_-_Disk_The_Informant_5_[ENG].rar, Yuusha Ga Shinda! - Vol.tbd Chapter 27.001 V2 Infection ①.cbz
|
||||
new Regex(
|
||||
@"^(?<Series>.*)( |_)Vol\.?(\d+|tbd)",
|
||||
MatchOptions, RegexTimeout),
|
||||
// Mad Chimera World - Volume 005 - Chapter 026.cbz (couldn't figure out how to get Volume negative lookaround working on below regex),
|
||||
// The Duke of Death and His Black Maid - Vol. 04 Ch. 054.5 - V4 Omake
|
||||
new Regex(
|
||||
@"(?<Series>.+?)(\s|_|-)+(?:Vol(ume|\.)?(\s|_|-)+\d+)(\s|_|-)+(?:(Ch|Chapter|Ch)\.?)(\s|_|-)+(?<Chapter>\d+)",
|
||||
MatchOptions,
|
||||
RegexTimeout),
|
||||
// [SugoiSugoi]_NEEDLESS_Vol.2_-_Disk_The_Informant_5_[ENG].rar, Yuusha Ga Shinda! - Vol.tbd Chapter 27.001 V2 Infection ①.cbz,
|
||||
// Nagasarete Airantou - Vol. 30 Ch. 187.5 - Vol.30 Omake
|
||||
new Regex(
|
||||
@"^(?<Series>.+?)(\s*Chapter\s*\d+)?(\s|_|\-\s)+Vol(ume)?\.?(\d+|tbd|\s\d).+?",
|
||||
MatchOptions, RegexTimeout),
|
||||
// Ichiban_Ushiro_no_Daimaou_v04_ch34_[VISCANS].zip, VanDread-v01-c01.zip
|
||||
new Regex(
|
||||
@"(?<Series>.*)(\b|_)v(?<Volume>\d+-?\d*)(\s|_|-)",
|
||||
|
@ -233,6 +235,7 @@ public static class Parser
|
|||
@"(?<Series>.+?):?(\s|\b|_|-)Chapter(\s|\b|_|-)\d+(\s|\b|_|-)(vol)(ume)",
|
||||
MatchOptions,
|
||||
RegexTimeout),
|
||||
|
||||
// [xPearse] Kyochuu Rettou Volume 1 [English] [Manga] [Volume Scans]
|
||||
new Regex(
|
||||
@"(?<Series>.+?):? (\b|_|-)(vol)(ume)",
|
||||
|
|
Binary file not shown.
|
@ -4,7 +4,7 @@
|
|||
<TargetFramework>net7.0</TargetFramework>
|
||||
<Company>kavitareader.com</Company>
|
||||
<Product>Kavita</Product>
|
||||
<AssemblyVersion>0.7.11.1</AssemblyVersion>
|
||||
<AssemblyVersion>0.7.11.2</AssemblyVersion>
|
||||
<NeutralLanguage>en</NeutralLanguage>
|
||||
<TieredPGO>true</TieredPGO>
|
||||
</PropertyGroup>
|
||||
|
@ -20,5 +20,4 @@
|
|||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
</Project>
|
|
@ -55,7 +55,7 @@ install methods and platforms.
|
|||
**Note: Kavita is under heavy development and is being updated all the time, so the tag for bleeding edge builds is `:nightly`. The `:latest` tag will be the latest stable release.**
|
||||
|
||||
## Feature Requests
|
||||
Got a great idea? Throw it up on our [Feature Request site](https://feats.kavitareader.com/) or vote on another idea. Please check the [Project Board](https://github.com/Kareadita/Kavita/projects) first for a list of planned features before you submit an idea.
|
||||
Got a great idea? Throw it up on our [Feature Request site](https://feats.kavitareader.com/) or vote on another idea. Please check the [Project Board](https://github.com/Kareadita/Kavita/projects?type=classic) first for a list of planned features before you submit an idea.
|
||||
|
||||
## Notice
|
||||
Kavita is being actively developed and should be considered beta software until the 1.0 release.
|
||||
|
|
|
@ -15,7 +15,8 @@ type UtcToLocalTimeFormat = 'full' | 'short' | 'shortDate' | 'shortTime';
|
|||
})
|
||||
export class UtcToLocalTimePipe implements PipeTransform {
|
||||
|
||||
transform(utcDate: string, format: UtcToLocalTimeFormat = 'short'): string {
|
||||
transform(utcDate: string | undefined | null, format: UtcToLocalTimeFormat = 'short'): string {
|
||||
if (utcDate === undefined || utcDate === null) return '';
|
||||
const browserLanguage = navigator.language;
|
||||
const dateTime = DateTime.fromISO(utcDate, { zone: 'utc' }).toLocal().setLocale(browserLanguage);
|
||||
|
||||
|
|
|
@ -58,7 +58,9 @@
|
|||
<td>
|
||||
{{task.title | titlecase}}
|
||||
</td>
|
||||
<td>{{task.lastExecutionUtc | utcToLocalTime | defaultValue }}</td>
|
||||
<td>
|
||||
{{task.lastExecutionUtc | utcToLocalTime | defaultValue }}
|
||||
</td>
|
||||
<td>{{task.cron}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
|
|
@ -31,7 +31,7 @@
|
|||
<img src="{{item.src}}" style="display: block"
|
||||
class="mx-auto {{pageNum === item.page && showDebugOutline() ? 'active': ''}} {{areImagesWiderThanWindow ? 'full-width' : ''}} {{initFinished ? '' : 'full-opacity'}}"
|
||||
*ngIf="item.page >= pageNum - bufferPages && item.page <= pageNum + bufferPages" rel="nofollow" alt="image"
|
||||
(load)="onImageLoad($event)" id="page-{{item.page}}" [attr.page]="item.page" ondragstart="return false;" onselectstart="return false;">
|
||||
(load)="onImageLoad($event)" (touchstart)="onTouchStart(item.page)" (click)="onTouchStart(item.page)" id="page-{{item.page}}" [attr.page]="item.page" ondragstart="return false;" onselectstart="return false;">
|
||||
</ng-container>
|
||||
|
||||
<div *ngIf="atBottom" class="spacer bottom" role="alert" (click)="loadNextChapter.emit()">
|
||||
|
|
|
@ -21,11 +21,10 @@
|
|||
|
||||
.text {
|
||||
z-index: 101;
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
img, .full-width {
|
||||
max-width: 100% !important;
|
||||
height: auto;
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import { DOCUMENT, NgIf, NgFor, AsyncPipe } from '@angular/common';
|
||||
import {AsyncPipe, DOCUMENT, NgFor, NgIf} from '@angular/common';
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
ChangeDetectorRef,
|
||||
Component, DestroyRef,
|
||||
Component,
|
||||
DestroyRef,
|
||||
ElementRef,
|
||||
EventEmitter,
|
||||
inject,
|
||||
|
@ -15,16 +16,16 @@ import {
|
|||
Renderer2,
|
||||
SimpleChanges
|
||||
} from '@angular/core';
|
||||
import { BehaviorSubject, fromEvent, ReplaySubject } from 'rxjs';
|
||||
import { debounceTime } from 'rxjs/operators';
|
||||
import { ScrollService } from 'src/app/_services/scroll.service';
|
||||
import { ReaderService } from '../../../_services/reader.service';
|
||||
import { PAGING_DIRECTION } from '../../_models/reader-enums';
|
||||
import { WebtoonImage } from '../../_models/webtoon-image';
|
||||
import { ManagaReaderService } from '../../_service/managa-reader.service';
|
||||
import {BehaviorSubject, fromEvent, ReplaySubject} from 'rxjs';
|
||||
import {debounceTime} from 'rxjs/operators';
|
||||
import {ScrollService} from 'src/app/_services/scroll.service';
|
||||
import {ReaderService} from '../../../_services/reader.service';
|
||||
import {PAGING_DIRECTION} from '../../_models/reader-enums';
|
||||
import {WebtoonImage} from '../../_models/webtoon-image';
|
||||
import {ManagaReaderService} from '../../_service/managa-reader.service';
|
||||
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
|
||||
import {TranslocoDirective} from "@ngneat/transloco";
|
||||
import {MangaReaderComponent} from "../manga-reader/manga-reader.component";
|
||||
import {Breakpoint, UtilityService} from "../../../shared/_services/utility.service";
|
||||
|
||||
/**
|
||||
* How much additional space should pass, past the original bottom of the document height before we trigger the next chapter load
|
||||
|
@ -64,6 +65,7 @@ const enum DEBUG_MODES {
|
|||
export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
|
||||
|
||||
private readonly mangaReaderService = inject(ManagaReaderService);
|
||||
private readonly utilityService = inject(UtilityService);
|
||||
private readonly readerService = inject(ReaderService);
|
||||
private readonly renderer = inject(Renderer2);
|
||||
private readonly scrollService = inject(ScrollService);
|
||||
|
@ -127,7 +129,7 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
|
|||
*/
|
||||
isScrolling: boolean = false;
|
||||
/**
|
||||
* Whether all prefetched images have loaded on the screen (not neccesarily in viewport)
|
||||
* Whether all prefetched images have loaded on the screen (not necessarily in viewport)
|
||||
*/
|
||||
allImagesLoaded: boolean = false;
|
||||
/**
|
||||
|
@ -159,9 +161,13 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
|
|||
*/
|
||||
debugMode: DEBUG_MODES = DEBUG_MODES.None;
|
||||
/**
|
||||
* Debug mode. Will filter out any messages in here so they don't hit the log
|
||||
* Debug mode. Will filter out any messages in here, so they don't hit the log
|
||||
*/
|
||||
debugLogFilter: Array<string> = ['[PREFETCH]', '[Intersection]', '[Visibility]', '[Image Load]'];
|
||||
/**
|
||||
* Total Height of all the images
|
||||
*/
|
||||
totalHeight: number = 0;
|
||||
|
||||
get minPageLoaded() {
|
||||
return Math.min(...Object.values(this.imagesLoaded));
|
||||
|
@ -206,12 +212,19 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
|
|||
.pipe(debounceTime(20), takeUntilDestroyed(this.destroyRef))
|
||||
.subscribe((event) => this.handleScrollEvent(event));
|
||||
|
||||
fromEvent(this.isFullscreenMode ? this.readerElemRef.nativeElement : this.document.body, 'scrollend')
|
||||
.pipe(debounceTime(20), takeUntilDestroyed(this.destroyRef))
|
||||
.subscribe((event) => this.handleScrollEndEvent(event));
|
||||
// Only attach this on non-mobile
|
||||
if (this.utilityService.getActiveBreakpoint() > Breakpoint.Tablet) {
|
||||
fromEvent(this.isFullscreenMode ? this.readerElemRef.nativeElement : this.document.body, 'scrollend')
|
||||
.pipe(debounceTime(20), takeUntilDestroyed(this.destroyRef))
|
||||
.subscribe((event) => this.handleScrollEndEvent(event));
|
||||
}
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
|
||||
this.totalHeight = this.mangaReaderService.maxHeight();
|
||||
this.cdRef.markForCheck();
|
||||
|
||||
this.initScrollHandler();
|
||||
|
||||
this.recalculateImageWidth();
|
||||
|
@ -220,7 +233,7 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
|
|||
this.goToPage.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(page => {
|
||||
const isSamePage = this.pageNum === page;
|
||||
if (isSamePage) { return; }
|
||||
this.debugLog('[GoToPage] jump has occured from ' + this.pageNum + ' to ' + page);
|
||||
this.debugLog('[GoToPage] jump has occurred from ' + this.pageNum + ' to ' + page);
|
||||
|
||||
if (this.pageNum < page) {
|
||||
this.scrollingDirection = PAGING_DIRECTION.FORWARD;
|
||||
|
@ -317,15 +330,25 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
|
|||
}
|
||||
|
||||
handleScrollEndEvent(event?: any) {
|
||||
if (!this.isScrolling) {
|
||||
if (this.isScrolling) { return; }
|
||||
|
||||
const closestImages = Array.from(document.querySelectorAll('img[id^="page-"]')) as HTMLImageElement[];
|
||||
const img = this.findClosestVisibleImage(closestImages);
|
||||
const closestImages = Array.from(document.querySelectorAll('img[id^="page-"]')) as HTMLImageElement[];
|
||||
const img = this.findClosestVisibleImage(closestImages);
|
||||
|
||||
if (img != null) {
|
||||
this.setPageNum(parseInt(img.getAttribute('page') || this.pageNum + '', 10));
|
||||
}
|
||||
const newPageNum = parseInt(img?.getAttribute('page') || this.pageNum + '', 10);
|
||||
|
||||
// When a scroll end occurs on the last pre-fetched page, the next load event can cause the page number to be set to the end
|
||||
// of the pages, thus jumping the user a ton of pages.
|
||||
console.log('scroll end page delta: ', Math.abs(newPageNum - this.pageNum));
|
||||
if (Math.abs(newPageNum - this.pageNum) <= 10 && newPageNum != this.pageNum) {
|
||||
console.log('Closest page is: ', newPageNum)
|
||||
this.setPageNum(newPageNum);
|
||||
}
|
||||
|
||||
// if (img != null) {
|
||||
// console.log('Closest page is: ', img.getAttribute('page'))
|
||||
// this.setPageNum(parseInt(img.getAttribute('page') || this.pageNum + '', 10));
|
||||
// }
|
||||
}
|
||||
|
||||
getTotalHeight() {
|
||||
|
@ -354,6 +377,8 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
|
|||
if (this.scrollingDirection === PAGING_DIRECTION.FORWARD) {
|
||||
const totalHeight = this.getTotalHeight();
|
||||
const totalScroll = this.getTotalScroll();
|
||||
const pageDeltaToTriggerBottomLoad = 2;
|
||||
|
||||
|
||||
// If we were at top but have started scrolling down past page 0, remove top spacer
|
||||
if (this.atTop && this.pageNum > 0) {
|
||||
|
@ -361,7 +386,15 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
|
|||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
if (totalScroll === totalHeight && !this.atBottom) {
|
||||
// If we scroll faster than the images can load, we can end up in a situation where we hit the bottom before we should,
|
||||
// We can either check that the page num is within 10 of the total pages
|
||||
// or we could insert a div that equal the space of the image heights combined to ensure the scroll bar
|
||||
// Same situation, but when the next load of pages, we hit this condition. hence I'm adding a check on the page number to ensure
|
||||
if (totalScroll === totalHeight && !this.atBottom && Math.abs(this.pageNum - this.totalPages) <= pageDeltaToTriggerBottomLoad) {
|
||||
this.debugLog('total Scroll: ', totalScroll);
|
||||
this.debugLog('total height: ', totalHeight);
|
||||
this.debugLog('page delta: ', Math.abs(this.pageNum - this.totalPages));
|
||||
this.debugLog('We hit the bottom of the viewport!');
|
||||
this.atBottom = true;
|
||||
this.cdRef.markForCheck();
|
||||
this.setPageNum(this.totalPages);
|
||||
|
@ -471,7 +504,8 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
|
|||
const rect = image.getBoundingClientRect();
|
||||
|
||||
// Calculate the distance of the current image to the top of the viewport.
|
||||
const distanceToTop = Math.abs(rect.top);
|
||||
const distanceToTop = Math.abs(rect.top) - rect.height / 2;
|
||||
this.debugLog(`Image ${image.getAttribute('page')} is ${distanceToTop}px from top`)
|
||||
|
||||
// Check if the image is visible within the viewport.
|
||||
if (distanceToTop < closestDistanceToTop) {
|
||||
|
@ -502,9 +536,18 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
|
|||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
/**
|
||||
* When the user touches/clicks an image, let's set it as the active page. This ensures their touch always sets the active page
|
||||
* @param pageNum - Page of the image
|
||||
*/
|
||||
onTouchStart(pageNum: number) {
|
||||
this.debugLog('touch start: ', pageNum);
|
||||
this.setPageNum(pageNum);
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback for an image onLoad. At this point the image is already rendered in DOM (may not be visible)
|
||||
* This will be used to scroll to current page for intial load
|
||||
* This will be used to scroll to current page for initial load
|
||||
* @param event
|
||||
*/
|
||||
onImageLoad(event: any) {
|
||||
|
@ -530,6 +573,7 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
|
|||
this.currentPageElem = this.document.querySelector('img#page-' + this.pageNum);
|
||||
// There needs to be a bit of time before we scroll
|
||||
if (this.currentPageElem && !this.isElementVisible(this.currentPageElem)) {
|
||||
this.debugLog('image load, scrolling to page', this.pageNum);
|
||||
this.scrollToCurrentPage();
|
||||
} else {
|
||||
this.initFinished = true;
|
||||
|
@ -553,7 +597,7 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
|
|||
this.debugLog('[Intersection] Page ' + imagePage + ' is visible: ', entry.isIntersecting);
|
||||
if (entry.isIntersecting) {
|
||||
this.debugLog('[Intersection] ! Page ' + imagePage + ' just entered screen');
|
||||
this.prefetchWebtoonImages(imagePage);
|
||||
this.setPageNum(imagePage);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -36,7 +36,7 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<app-loading [loading]="isLoading || !(currentImage$ | async)?.complete" [absolute]="true"></app-loading>
|
||||
<app-loading [loading]="isLoading || (!(currentImage$ | async)?.complete && this.readerMode !== ReaderMode.Webtoon)" [absolute]="true"></app-loading>
|
||||
<div class="reading-area"
|
||||
ngSwipe (swipeEnd)="onSwipeEnd($event)" (swipeMove)="onSwipeMove($event)"
|
||||
[ngStyle]="{'background-color': backgroundColor, 'height': readerMode === ReaderMode.Webtoon ? 'inherit' : 'calc(var(--vh)*100)'}" #readingArea>
|
||||
|
|
|
@ -51,7 +51,7 @@
|
|||
</div>
|
||||
<app-image height="100%" maxHeight="400px" objectFit="contain" background="none" [imageUrl]="seriesImage"></app-image>
|
||||
<ng-container *ngIf="series.pagesRead < series.pages && hasReadingProgress && currentlyReadingChapter && !currentlyReadingChapter.isSpecial">
|
||||
<div class="progress-banner" ngbTooltip="{{(series.pagesRead / series.pages) | number:'1.0-1'}}% Read">
|
||||
<div class="progress-banner" ngbTooltip="{{(series.pagesRead / series.pages) * 100 | number:'1.0-1'}}% Read">
|
||||
<ngb-progressbar type="primary" height="5px" [value]="series.pagesRead" [max]="series.pages"></ngb-progressbar>
|
||||
</div>
|
||||
<div class="under-image">
|
||||
|
|
|
@ -254,7 +254,8 @@
|
|||
"edit": "{{common.edit}}",
|
||||
"cancel": "{{common.cancel}}",
|
||||
"save": "{{common.save}}",
|
||||
"token-input-label": "{{service}} Le token vient ici"
|
||||
"token-input-label": "{{service}} Le token vient ici",
|
||||
"title": "Fournisseur d'analyse de contenu"
|
||||
},
|
||||
"typeahead": {
|
||||
"locked-field": "Ce champ est vérouillé",
|
||||
|
@ -372,7 +373,8 @@
|
|||
"cover-artist": "Artiste de la couverture",
|
||||
"character": "Personnage",
|
||||
"artist": "Artiste",
|
||||
"inker": "Encreur"
|
||||
"inker": "Encreur",
|
||||
"publisher": "Éditeur"
|
||||
},
|
||||
"manga-format-pipe": {
|
||||
"epub": "EPUB",
|
||||
|
@ -388,15 +390,35 @@
|
|||
},
|
||||
"reset-password": {
|
||||
"description": "Entrez l'email de votre compte. Kavita vous enverra un e-mail s'il est valide dans le dossier, sinon demandez à l'administrateur le lien des journaux.",
|
||||
"title": "Réinitialisation de mot de passe"
|
||||
"title": "Réinitialisation de mot de passe",
|
||||
"submit": "{{common.submit}}",
|
||||
"valid-email": "{{validation.valid-email}}",
|
||||
"email-label": "{{common.email}}",
|
||||
"required-field": "{{validation.required-field}}"
|
||||
},
|
||||
"all-series": {
|
||||
"title": "Toutes les Séries"
|
||||
"title": "Toutes les Séries",
|
||||
"series-count": "{{common.series-count}}"
|
||||
},
|
||||
"series-metadata-detail": {
|
||||
"collections-title": "{{side-nav.collections}}",
|
||||
"tags-title": "Étiquettes",
|
||||
"characters-title": "Personnages"
|
||||
"characters-title": "Personnages",
|
||||
"reading-lists-title": "{{side-nav.reading-lists}}",
|
||||
"colorists-title": "Coloristes",
|
||||
"writers-title": "Écrivains",
|
||||
"publishers-title": "Éditeurs",
|
||||
"see-less": "Voir moins",
|
||||
"see-more": "Voir plus",
|
||||
"genres-title": "Genres",
|
||||
"inkers-title": "Encreurs",
|
||||
"pencillers-title": "Crayonneurs",
|
||||
"links-title": "Liens",
|
||||
"editors-title": "Éditeurs",
|
||||
"promoted": "{{common.promoted}}",
|
||||
"cover-artists-title": "Artistes de couverture",
|
||||
"letterers-title": "Lettreurs",
|
||||
"translators-title": "Traducteurs"
|
||||
},
|
||||
"side-nav": {
|
||||
"home": "Accueil",
|
||||
|
@ -404,7 +426,12 @@
|
|||
"collections": "Collections",
|
||||
"reading-lists": "Listes de lecture",
|
||||
"bookmarks": "Marque-pages",
|
||||
"all-series": "Toutes les Séries"
|
||||
"all-series": "Toutes les Séries",
|
||||
"back": "Retour",
|
||||
"more": "Plus",
|
||||
"clear": "{{common.clear}}",
|
||||
"filter-label": "{{common.filter}}",
|
||||
"donate": "Donner"
|
||||
},
|
||||
"all-collections": {
|
||||
"title": "Collections"
|
||||
|
@ -514,7 +541,9 @@
|
|||
"r18-plus": "Interdit aux moins de 18 ans",
|
||||
"kids-to-adults": "Enfants aux adultes",
|
||||
"rating-pending": "Classification en attente",
|
||||
"everyone": "Tous publics"
|
||||
"everyone": "Tous publics",
|
||||
"ma15-plus": "15+",
|
||||
"mature-17-plus": "17+"
|
||||
},
|
||||
"server-stats": {
|
||||
"tags": "Étiquettes",
|
||||
|
@ -543,10 +572,23 @@
|
|||
"reset-password-modal": {
|
||||
"title": "Réinitialiser le mot de passe de {nom}",
|
||||
"error-label": "Erreur : ",
|
||||
"new-password-label": "Nouveau mot de passe"
|
||||
"new-password-label": "Nouveau mot de passe",
|
||||
"close": "{{common.close}}",
|
||||
"cancel": "{{common.cancel}}",
|
||||
"save": "{{common.save}}"
|
||||
},
|
||||
"invite-user": {
|
||||
"title": "Inviter un utilisateur"
|
||||
"title": "Inviter un utilisateur",
|
||||
"required-field": "{{common.required-field}}",
|
||||
"setup-user-account": "Configuration du compte utilisateur",
|
||||
"invite-url-label": "URL d'invitation",
|
||||
"close": "{{common.close}}",
|
||||
"cancel": "{{common.cancel}}",
|
||||
"email": "{{common.email}}",
|
||||
"inviting": "Invitation en cours…",
|
||||
"setup-user-account-tooltip": "Copier et coller dans un nouvel onglet. Vous devrait peut-être vous déconnecter.",
|
||||
"setup-user-title": "Utilisateurs invités",
|
||||
"invite": "Inviter"
|
||||
},
|
||||
"announcements": {
|
||||
"title": "Annonces"
|
||||
|
@ -557,10 +599,21 @@
|
|||
"activate-delete": "Supprimer",
|
||||
"no-license-key": "Aucune clé de licence",
|
||||
"license-not-valid": "Licence non valide",
|
||||
"activate-description": "Entrez la clé de licence et l'email utilisé pour s'inscrire avec Stripe"
|
||||
"activate-description": "Entrez la clé de licence et l'email utilisé pour s'inscrire avec Stripe",
|
||||
"manage": "Gérer",
|
||||
"activate": "Activer",
|
||||
"buy": "Acheter",
|
||||
"loading": "{{common.loading}}",
|
||||
"cancel": "{{common.cancel}}",
|
||||
"title": "Licence Kavita+",
|
||||
"renew": "Renouveler",
|
||||
"check": "Vérifier",
|
||||
"activate-save": "{{common.save}}",
|
||||
"activate-email-label": "{{common.email}}",
|
||||
"edit": "{{common.edit}}"
|
||||
},
|
||||
"book-reader": {
|
||||
"bookmarks-header": "Signets",
|
||||
"bookmarks-header": "{{side-nav.bookmarks}}",
|
||||
"incognito-mode-label": "Navigation privée",
|
||||
"page-label": "Page",
|
||||
"prev-page": "Page précédente",
|
||||
|
@ -581,15 +634,213 @@
|
|||
"virtual-pages": "pages virtuelles",
|
||||
"toc-header": "TdM",
|
||||
"pagination-header": "Section",
|
||||
"prev-chapter": "Chapitre/Volume précédent"
|
||||
"prev-chapter": "Chapitre/Volume précédent",
|
||||
"close-reader": "Fermer le lecteur"
|
||||
},
|
||||
"book-line-overlay": {
|
||||
"bookmark": "Signet",
|
||||
"copy": "Copier",
|
||||
"bookmark-label": "Nom du signet"
|
||||
"bookmark-label": "Nom du signet",
|
||||
"close": "{{common.close}}",
|
||||
"required-field": "{{common.required-field}}",
|
||||
"save": "{{common.save}}"
|
||||
},
|
||||
"register": {
|
||||
"description": "Remplissez le formulaire pour enregistrer un compte administrateur",
|
||||
"title": "S'inscrire"
|
||||
"title": "S'inscrire",
|
||||
"email-label": "{{common.email}}",
|
||||
"required-field": "{{validation.required-field}}",
|
||||
"password-validation": "{{validation.password-validation}}",
|
||||
"register": "S'enregistrer",
|
||||
"username-label": "{{common.username}}",
|
||||
"valid-email": "{{validation.valid-email}}",
|
||||
"password-label": "{{common.password}}",
|
||||
"email-tooltip": "L'e-mail ne doit pas nécessairement être une véritable adresse, mais donne accès au mot de passe oublié. Il n'est pas envoyé en dehors du serveur, sauf si mot de passe oublié est utilisé sans un hôte de service de messagerie personnalisé."
|
||||
},
|
||||
"update-notification-modal": {
|
||||
"title": "Nouvelle mise à jour disponible !",
|
||||
"download": "Télécharger",
|
||||
"help": "Comment mettre à jour",
|
||||
"close": "{{common.close}}"
|
||||
},
|
||||
"library-settings-modal": {
|
||||
"folder-tab": "Dossier",
|
||||
"name-label": "Nom",
|
||||
"include-in-search-tooltip": "Inclure les séries et informations dérivées (genres, personnes, fichiers) dans les résultats de recherche.",
|
||||
"include-in-recommendation-label": "Inclure dans les recommandations",
|
||||
"naming-conventions-part-2": "dossier requis.",
|
||||
"type-label": "Type",
|
||||
"help-us-part-1": "Aidez-nous en suivant ",
|
||||
"reset": "{{common.reset}}",
|
||||
"manage-collection-label": "Gérer les collections",
|
||||
"type-tooltip": "Le type de bibliothèque détermine la manière dont les noms de fichiers sont analysés et si l'interface utilisateur affiche des chapitres (Manga) ou des numéros (Bandes dessinées). Les livres fonctionnent de la même manière que les mangas mais ont un nom différent dans l'interface utilisateur.",
|
||||
"cover-tab": "Couverture",
|
||||
"manage-reading-list-label": "Gérer les listes de lecture",
|
||||
"include-in-search-label": "Inclure dans la recherche",
|
||||
"force-scan-tooltip": "Ceci forcera l'analyse de la bibliothèque, c",
|
||||
"edit-title": "Modifier le {{name}}",
|
||||
"close": "{{common.close}}",
|
||||
"required-field": "{{validation.required-field}}",
|
||||
"general-tab": "Général",
|
||||
"cancel": "{{common.cancel}}",
|
||||
"exclude-patterns-label": "Exclure les modèles",
|
||||
"help-us-part-3": "pour nommer et organiser vos médias.",
|
||||
"naming-conventions-part-1": "Kavita a ",
|
||||
"cover-description": "Les icônes des bibliothèques personnalisées sont optionnelles",
|
||||
"naming-conventions-part-3": "Vérifiez ce lien pour vous assurer que vous suivez, sinon les fichiers ne s'afficheront pas lors de l'analyse.",
|
||||
"folder-watching-label": "Surveillance de dossiers",
|
||||
"library-name-unique": "Le nom de la bibliothèque doit être unique",
|
||||
"browse": "Rechercher des dossiers multimédias",
|
||||
"folder-description": "Ajouter des dossier dans votre bibliothèque",
|
||||
"help-us-part-2": "notre guide",
|
||||
"allow-scrobbling-label": "Autoriser le scrobbling",
|
||||
"advanced-tab": "Avancée",
|
||||
"help": "{{common.help}}",
|
||||
"include-in-recommendation-tooltip": "Inclure les séries de la bibliothèque dans la page des recommandations.",
|
||||
"last-scanned-label": "Dernière analyse :",
|
||||
"add-title": "Ajouter Bibliothèque",
|
||||
"file-type-group-label": "Types de fichier",
|
||||
"save": "{{common.save}}",
|
||||
"force-scan": "Forcer l'analyse",
|
||||
"include-in-dashboard-label": "Inclure dans le tableau de bord",
|
||||
"next": "Suivant"
|
||||
},
|
||||
"series-detail": {
|
||||
"close": "{{common.close}}",
|
||||
"volumes-tab": "Volumes",
|
||||
"layout-mode-option-card": "Carte",
|
||||
"continue": "Continuer",
|
||||
"layout-mode-label": "{{user-preferences.layout-mode-book-label}}",
|
||||
"cover-change": "L'actualisation de l'image par votre navigateur peut prendre jusqu'à une minute. En attendant, l'ancienne image peut être affichée sur certaines pages.",
|
||||
"read": "{{common.read}}",
|
||||
"incognito": "Mode privé",
|
||||
"remove-from-want-to-read": "{{actionable.remove-from-want-to-read}}",
|
||||
"continue-from": "Continuer {{title}}",
|
||||
"read-options-alt": "Options de lecture",
|
||||
"books-tab": "Livres",
|
||||
"downloading-status": "Téléchargement…",
|
||||
"send-to": "Fichier envoyé par e-mail à {{deviceName}}",
|
||||
"storyline-tab": "Scénario",
|
||||
"no-chapters": "Il n'y a aucun chapitre dans ce volume. Il ne peut être lu.",
|
||||
"continue-incognito": "Continuer en mode privé",
|
||||
"page-settings-title": "Paramètres des pages",
|
||||
"user-reviews-alt": "Critiques d'utilisateurs",
|
||||
"related-tab": "Relations",
|
||||
"read-incognito": "Lire en mode privé",
|
||||
"specials-tab": "Spéciales",
|
||||
"recommendations-tab": "Recommandations",
|
||||
"download-series--tooltip": "Télécharger la série",
|
||||
"edit-series-alt": "Modifier les informations sur la série",
|
||||
"layout-mode-option-list": "Liste",
|
||||
"add-to-want-to-read": "{{actionable.add-to-want-to-read}}",
|
||||
"no-pages": "{{toasts.no-pages}}"
|
||||
},
|
||||
"reader-settings": {
|
||||
"theme-paper": "Papier",
|
||||
"immersive-mode-label": "{{user-preferences.immersive-mode-label}}",
|
||||
"left-to-right": "De gauche à droite",
|
||||
"layout-mode-option-2col": "2 Colonnes",
|
||||
"reading-direction-label": "{{user-preferences.reading-direction-book-label}}",
|
||||
"on": "Activé",
|
||||
"writing-style-tooltip": "Change la direction du texte. L'horizontale est de gauche à droite, la verticale de haut en bas.",
|
||||
"off": "Désactivé",
|
||||
"margin-label": "{{user-preferences.margin-book-label}}",
|
||||
"general-settings-title": "Réglages généraux",
|
||||
"reader-settings-title": "Paramètres de lecture",
|
||||
"fullscreen-label": "Plein écran",
|
||||
"layout-mode-option-1col": "1 Colonne",
|
||||
"fullscreen-tooltip": "Mettre le lecteur en plein écran",
|
||||
"theme-dark": "Sombre",
|
||||
"writing-style-label": "{{user-preferences.writing-style-label}}",
|
||||
"vertical": "Vertical",
|
||||
"layout-mode-label": "{{user-preferences.layout-mode-book-label}}",
|
||||
"tap-to-paginate-label": "Appuyez sur Pagination",
|
||||
"exit": "Sortir",
|
||||
"line-spacing-label": "{{user-preferences.line-height-book-label}}",
|
||||
"color-theme-title": "Couleur du thème",
|
||||
"theme-white": "Blanc",
|
||||
"font-size-label": "{{user-preferences.font-size-book-label}}",
|
||||
"theme-black": "Noir",
|
||||
"font-family-label": "{{user-preferences.font-family-label}}",
|
||||
"reset-to-defaults": "Réinitialiser les paramètres par défaut",
|
||||
"tap-to-paginate-tooltip": "Cliquez sur les bords de l'écran pour paginer",
|
||||
"enter": "Entrer",
|
||||
"right-to-left": "De droite à gauche",
|
||||
"layout-mode-option-scroll": "Faire défiler"
|
||||
},
|
||||
"read-more": {
|
||||
"read-less": "Lire moins",
|
||||
"read-more": "Lire plus"
|
||||
},
|
||||
"confirm-email": {
|
||||
"title": "S'enregistrer",
|
||||
"password-validation": "{{validation.password-validation}}",
|
||||
"valid-email": "{{common.valid-email}}",
|
||||
"email-label": "{{common.email}}",
|
||||
"description": "Compléter le formulaire pour terminer la création du compte",
|
||||
"password-label": "{{common.password}}",
|
||||
"register": "S'enregistrer",
|
||||
"username-label": "{{common.username}}",
|
||||
"required-field": "{{common.required-field}}",
|
||||
"error-label": "Erreurs : "
|
||||
},
|
||||
"bookmarks": {
|
||||
"delete-success": "Les favoris ont été supprimés",
|
||||
"confirm-single-delete": "Êtes-vous sûr de vouloir effacer tous les favoris de {{seriesName}}. Ceci ne peut pas être annulé.",
|
||||
"confirm-delete": "Êtes-vous sûr de vouloir effacer tous les favoris de plusieurs séries ? Ceci ne peut pas être annulé.",
|
||||
"no-data-2": "un.",
|
||||
"no-data": "Il n'y a pas de favoris. Essayez de créer",
|
||||
"title": "{{side-nav.bookmarks}}",
|
||||
"series-count": "{{common.series-count}}"
|
||||
},
|
||||
"side-nav-companion-bar": {
|
||||
"open-filter-and-sort": "Ouvrir Filtres et Tris",
|
||||
"close-filter-and-sort": "Fermer Filtres et Tris",
|
||||
"filter-and-sort-alt": "Trier / Filtrer",
|
||||
"page-settings-title": "{{series-detail.page-settings-title}}"
|
||||
},
|
||||
"confirm-email-change": {
|
||||
"non-confirm-description": "Merci d'attendre la validation de la mise à jour de votre mail.",
|
||||
"title": "Valider la modification du mail",
|
||||
"confirm-description": "Votre mail a été validé et est maintenant modifié dans Kavita. Vous allez être redirigé vers la fenêtre de connexion.",
|
||||
"success": "Succès !"
|
||||
},
|
||||
"confirm-reset-password": {
|
||||
"password-validation": "{{validation.password-validation}}",
|
||||
"required-field": "{{validation.required-field}}",
|
||||
"password-label": "{{common.password}}",
|
||||
"title": "Réinitialisation du mot de passe",
|
||||
"description": "Saisir un nouveau mot de passe",
|
||||
"submit": "{{common.submit}}"
|
||||
},
|
||||
"file-type-group-pipe": {
|
||||
"epub": "Epub",
|
||||
"archive": "Archive",
|
||||
"pdf": "Pdf",
|
||||
"image": "Image"
|
||||
},
|
||||
"library-selector": {
|
||||
"deselect-all": "{{common.deselect-all}}",
|
||||
"select-all": "{{common.select-all}}",
|
||||
"title": "Bibliothèques",
|
||||
"no-data": "Aucune bibliothèque configurée."
|
||||
},
|
||||
"all-filters": {
|
||||
"title": "Tous les filtres intelligents",
|
||||
"count": "{{count}} {{customize-dashboard-modal.title-smart-filters}}"
|
||||
},
|
||||
"personal-table-of-contents": {
|
||||
"delete": "Supprimer {{bookmarkName}}",
|
||||
"no-data": "Rien n'a encore été mis en favoris",
|
||||
"page": "Page {{value}}"
|
||||
},
|
||||
"table-of-contents": {
|
||||
"no-data": "Ce livre n'a pas de table des matières définie dans les métadonnées ou dans un fichier toc"
|
||||
},
|
||||
"badge-expander": {
|
||||
"more-items": "et {{count}} plus"
|
||||
},
|
||||
"user-holds": {
|
||||
"description": "C'est une liste de séries gérée par l'utilisateur qui ne sera pas soumis aux fournisseurs de contenu. Vous pouvez enlever une série à n'importe quel moment et le prochain événement de soumission (progression de lecture, notation, statut \"à lire\") déclenchera l'événement."
|
||||
}
|
||||
}
|
||||
|
|
|
@ -556,7 +556,8 @@
|
|||
"incognito-mode-label": "Modalità Incognito",
|
||||
"next": "Prossimo",
|
||||
"previous": "Precedente",
|
||||
"go-to-page-prompt": "Ci sono {{totalPages}} pagine. A quale pagina vuoi andare?"
|
||||
"go-to-page-prompt": "Ci sono {{totalPages}} pagine. A quale pagina vuoi andare?",
|
||||
"close-reader": "Chiudi Lettore"
|
||||
},
|
||||
"personal-table-of-contents": {
|
||||
"no-data": "Nessun preferito ancora",
|
||||
|
@ -726,7 +727,12 @@
|
|||
"cancel": "{{common.cancel}}",
|
||||
"next": "Prossimo",
|
||||
"save": "{{common.save}}",
|
||||
"required-field": "{{validation.required-field}}"
|
||||
"required-field": "{{validation.required-field}}",
|
||||
"exclude-patterns-label": "Escludi modelli",
|
||||
"help": "{{common.help}}",
|
||||
"file-type-group-label": "Tipi di file",
|
||||
"file-type-group-tooltip": "Quali tipi di file dovrebbe scansionare Kavita. Ad esempio, Archivio includerà tutti i file cb*, zip, rar, ecc.",
|
||||
"exclude-patterns-tooltip": "Configura una serie di modelli (sintassi Glob) che Kavita abbinerà durante la scansione delle directory ed escluderà dai risultati dello scanner."
|
||||
},
|
||||
"reader-settings": {
|
||||
"general-settings-title": "Impostazioni Generali",
|
||||
|
@ -1081,7 +1087,10 @@
|
|||
"analyze-files-task-desc": "Esegue un'attività di lunga durata che analizzerà i file per generare estensione e dimensione. Dovrebbe essere eseguito solo una volta per la versione v0.7. Non necessario se hai installato la versione successiva alla v0.7.",
|
||||
"analyze-files-task-success": "L'analisi del file è stata messa in coda",
|
||||
"check-for-updates-task": "Cerca Aggiornamenti",
|
||||
"check-for-updates-task-desc": "Verifica se sono disponibili versioni stabili prima della tua versione."
|
||||
"check-for-updates-task-desc": "Verifica se sono disponibili versioni stabili prima della tua versione.",
|
||||
"bust-locale-task-desc": "Blocca la cache delle impostazioni locali. Ciò può risolvere i problemi relativi alle stringhe che non vengono visualizzate correttamente dopo un aggiornamento",
|
||||
"bust-locale-task-success": "Cache locale cancellata",
|
||||
"bust-locale-task": "Busta cache delle impostazioni locali"
|
||||
},
|
||||
"manage-users": {
|
||||
"title": "Utenti Attivi",
|
||||
|
@ -1234,7 +1243,8 @@
|
|||
"reading-lists": "Liste Lettura",
|
||||
"collections": "Collezioni",
|
||||
"close": "{{common.close}}",
|
||||
"loading": "{{common.loading}}"
|
||||
"loading": "{{common.loading}}",
|
||||
"bookmarks": "{{side-nav.bookmarks}}"
|
||||
},
|
||||
"nav-header": {
|
||||
"skip-alt": "Passa al contenuto principale",
|
||||
|
@ -1337,7 +1347,10 @@
|
|||
"no-prev-chapter": "Nessun capitolo precedente",
|
||||
"user-preferences-updated": "Preferenze utente aggiornate",
|
||||
"emulate-comic-book-label": "{{user-preferences.emulate-comic-book-label}}",
|
||||
"bookmarks-title": "Segnalibri"
|
||||
"bookmarks-title": "Segnalibri",
|
||||
"series-progress": "Avanzamento della serie: {{percentage}}",
|
||||
"unbookmark-page-tooltip": "Annulla pagina segnalibri",
|
||||
"bookmark-page-tooltip": "Pagina dei segnalibri"
|
||||
},
|
||||
"metadata-filter": {
|
||||
"filter-title": "{{common.filter}}",
|
||||
|
@ -1384,7 +1397,8 @@
|
|||
"last-modified": "Ultima modifica",
|
||||
"last-chapter-added": "Elemento aggiunto",
|
||||
"time-to-read": "Tempo di leggere",
|
||||
"release-year": "Anno di pubblicazione"
|
||||
"release-year": "Anno di pubblicazione",
|
||||
"read-progress": "Ultimo Letto"
|
||||
},
|
||||
"edit-series-modal": {
|
||||
"title": "{{seriesName}} Dettagli",
|
||||
|
@ -1449,7 +1463,8 @@
|
|||
"day-breakdown": {
|
||||
"title": "Ripartizione del giorno",
|
||||
"x-axis-label": "Giorno della settimana",
|
||||
"y-axis-label": "Eventi di lettura"
|
||||
"y-axis-label": "Eventi di lettura",
|
||||
"no-data": "Nessun progresso, inizia a leggere"
|
||||
},
|
||||
"file-breakdown-stats": {
|
||||
"format-title": "Formato",
|
||||
|
@ -1712,7 +1727,8 @@
|
|||
"chapter-num": "Capitolo",
|
||||
"volume-num": "Volume",
|
||||
"clear": "Pulisci",
|
||||
"filter": "Filtro"
|
||||
"filter": "Filtro",
|
||||
"remove": "Rimuovere"
|
||||
},
|
||||
"infinite-scroller": {
|
||||
"continuous-reading-prev-chapter-alt": "Scorri verso l'alto per passare al capitolo precedente",
|
||||
|
@ -1758,7 +1774,9 @@
|
|||
"languages": "Lingue",
|
||||
"libraries": "Librerie",
|
||||
"path": "Percorso",
|
||||
"file-path": "Percorso File"
|
||||
"file-path": "Percorso File",
|
||||
"read-date": "Data di lettura",
|
||||
"want-to-read": "Voler leggere"
|
||||
},
|
||||
"filter-comparison-pipe": {
|
||||
"not-equal": "Non uguale",
|
||||
|
@ -1820,7 +1838,9 @@
|
|||
"remove-from-want-to-read": "{{actionable.remove-from-want-to-read}}",
|
||||
"vols-and-chapters": "{{volCount}} Volumi / {{chpCount}} Capitoli",
|
||||
"tags-label": "{{filter-field-pipe.tags}}",
|
||||
"add-to-want-to-read": "{{actionable.add-to-want-to-read}}"
|
||||
"add-to-want-to-read": "{{actionable.add-to-want-to-read}}",
|
||||
"view-series": "Visualizza serie",
|
||||
"staff-label": "Personale"
|
||||
},
|
||||
"customize-dashboard-modal": {
|
||||
"smart-filters": "Filtri Intelligenti",
|
||||
|
@ -1829,7 +1849,10 @@
|
|||
"help": "{{common.help}}",
|
||||
"external-sources": "Fonti esterne",
|
||||
"title-dashboard": "Personalizza la Dashboard",
|
||||
"title-smart-filters": "Filtri Intelligenti"
|
||||
"title-smart-filters": "Filtri Intelligenti",
|
||||
"dashboard": "Dashboard",
|
||||
"sidenav": "Nav laterale",
|
||||
"title-sidenav": "Personalizza la navigazione laterale"
|
||||
},
|
||||
"stream-list-item": {
|
||||
"external-source": "Fonti Esterne",
|
||||
|
@ -1857,7 +1880,8 @@
|
|||
"filter": "{{common.filter}}",
|
||||
"clear": "{{common.clear}}",
|
||||
"no-data": "Non esistono fonti esterne",
|
||||
"help-link": "Maggiori informazioni"
|
||||
"help-link": "Maggiori informazioni",
|
||||
"description": "Migliora la tua esperienza aggiungendo server esterni e includili comodamente nel tuo Side Nav per un accesso rapido sia al tuo server che a quello del tuo amico."
|
||||
},
|
||||
"next-expected-card": {
|
||||
"title": "~{{date}}"
|
||||
|
@ -1869,5 +1893,11 @@
|
|||
"metadata-filter-row": {
|
||||
"unit-reading-progress": "Percentuale",
|
||||
"unit-reading-date": "Data"
|
||||
},
|
||||
"file-type-group-pipe": {
|
||||
"epub": "Epub",
|
||||
"archive": "Archivio",
|
||||
"pdf": "Pdf",
|
||||
"image": "Immagine"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,7 +10,8 @@
|
|||
"dashboard": {
|
||||
"server-settings-link": "サーバー設定",
|
||||
"recently-updated-title": "最近更新されたシリーズ",
|
||||
"recently-added-title": "最近追加されたシリーズ"
|
||||
"recently-added-title": "最近追加されたシリーズ",
|
||||
"on-deck-title": "最近読んだ本"
|
||||
},
|
||||
"edit-user": {
|
||||
"edit": "{{common.edit}}",
|
||||
|
@ -25,7 +26,8 @@
|
|||
},
|
||||
"user-scrobble-history": {
|
||||
"series-header": "シリーズ",
|
||||
"no-data": "データなし"
|
||||
"no-data": "データなし",
|
||||
"filter-label": "{{common.filter}}"
|
||||
},
|
||||
"scrobble-event-type-pipe": {
|
||||
"want-to-read-add": "読みたい:追加",
|
||||
|
@ -160,5 +162,10 @@
|
|||
},
|
||||
"card-detail-layout": {
|
||||
"jumpkey-count": "{{count}} シリーズ"
|
||||
},
|
||||
"user-preferences": {
|
||||
"account-tab": "アカウント",
|
||||
"theme-tab": "テーマ",
|
||||
"devices-tab": "デバイス"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -30,7 +30,7 @@
|
|||
"user-scrobble-history": {
|
||||
"title": "Histórico de scrobble",
|
||||
"description": "Aqui você encontrará todos os eventos scrobble vinculados à sua conta. Para que os eventos existam, você deve ter um provedor scrobble ativo configurado. Todos os eventos processados serão apagados após um mês. Se houver eventos não processados, é provável que eles não possam formar correspondências upstream. Entre em contato com seu administrador para corrigi-los.",
|
||||
"filter-label": "Filtro",
|
||||
"filter-label": "{{common.filter}}",
|
||||
"created-header": "Criado",
|
||||
"last-modified-header": "Última Modificação",
|
||||
"type-header": "Tipo",
|
||||
|
@ -150,7 +150,7 @@
|
|||
},
|
||||
"user-holds": {
|
||||
"title": "Segurar Scrobble",
|
||||
"description": "Esta é uma lista de séries gerenciada pelo usuário que não será scrobbled para provedores upstream. Você pode remover uma série a qualquer momento e o próximo evento compatível com Scrobble (progresso de leitura, classificação, status de desejo de leitura) acionará eventos."
|
||||
"description": "Esta é uma lista de séries gerenciada pelo usuário que não será transferida para provedores upstream. Você pode remover uma série a qualquer momento e o próximo evento passível de scrobble (leitura de progresso, classificação, desejo de ler status) acionará eventos."
|
||||
},
|
||||
"theme-manager": {
|
||||
"title": "Gerenciador de Temas",
|
||||
|
@ -233,7 +233,7 @@
|
|||
"save": "{{common.save}}"
|
||||
},
|
||||
"change-age-restriction": {
|
||||
"age-restriction-label": "Classificação Etária",
|
||||
"age-restriction-label": "Restrição de Etária",
|
||||
"unknowns": "Desconhecidos",
|
||||
"reset": "{{common.reset}}",
|
||||
"edit": "{{common.edit}}",
|
||||
|
@ -340,7 +340,7 @@
|
|||
"all-time": "Todo o Tempo"
|
||||
},
|
||||
"device-platform-pipe": {
|
||||
"custom": "Personalizar"
|
||||
"custom": "Personalizado"
|
||||
},
|
||||
"day-of-week-pipe": {
|
||||
"monday": "Segunda-feira",
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
"name": "GPL-3.0",
|
||||
"url": "https://github.com/Kareadita/Kavita/blob/develop/LICENSE"
|
||||
},
|
||||
"version": "0.7.11.0"
|
||||
"version": "0.7.11.1"
|
||||
},
|
||||
"servers": [
|
||||
{
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue