mirror of
https://github.com/Kareadita/Kavita.git
synced 2025-12-27 23:30:22 -05:00
Polish Pass 1 (#4084)
Co-authored-by: Amelia <77553571+Fesaa@users.noreply.github.com>
This commit is contained in:
parent
a3c8c9be33
commit
5f744fa2fe
1
.gitignore
vendored
1
.gitignore
vendored
@ -537,6 +537,7 @@ UI/Web/.vscode/settings.json
|
||||
/API.Tests/Services/Test Data/ArchiveService/CoverImages/output/*
|
||||
UI/Web/.angular/
|
||||
BenchmarkDotNet.Artifacts
|
||||
.claude/
|
||||
|
||||
|
||||
API.Tests/Services/Test Data/ImageService/**/*_output*
|
||||
|
||||
@ -581,8 +581,11 @@ public class OidcServiceTests(ITestOutputHelper outputHelper): AbstractDbTest(ou
|
||||
await userManager.CreateAsync(user);
|
||||
await userManager.CreateAsync(defaultAdmin);
|
||||
|
||||
var accountService = new AccountService(userManager, Substitute.For<ILogger<AccountService>>(), unitOfWork, mapper, Substitute.For<ILocalizationService>());
|
||||
var oidcService = new OidcService(Substitute.For<ILogger<OidcService>>(), userManager, unitOfWork, accountService, Substitute.For<IEmailService>());
|
||||
var accountService = new AccountService(userManager, Substitute.For<ILogger<AccountService>>(),
|
||||
unitOfWork, mapper, Substitute.For<ILocalizationService>());
|
||||
var oidcService = new OidcService(Substitute.For<ILogger<OidcService>>(), userManager, unitOfWork,
|
||||
accountService, Substitute.For<IEmailService>());
|
||||
|
||||
return (oidcService, user, accountService, userManager);
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -274,14 +274,14 @@ public class ReaderController : BaseApiController
|
||||
if (info.IsSpecial)
|
||||
{
|
||||
info.Subtitle = Path.GetFileNameWithoutExtension(info.FileName);
|
||||
} else if (!info.IsSpecial && info.VolumeNumber.Equals(Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume))
|
||||
} else if (!info.IsSpecial && info.VolumeNumber.Equals(Parser.LooseLeafVolume))
|
||||
{
|
||||
info.Subtitle = ReaderService.FormatChapterName(info.LibraryType, true, true) + info.ChapterNumber;
|
||||
}
|
||||
else
|
||||
{
|
||||
info.Subtitle = await _localizationService.Translate(User.GetUserId(), "volume-num", info.VolumeNumber);
|
||||
if (!info.ChapterNumber.Equals(Services.Tasks.Scanner.Parser.Parser.DefaultChapter))
|
||||
if (!info.ChapterNumber.Equals(Parser.DefaultChapter))
|
||||
{
|
||||
info.Subtitle += " " + ReaderService.FormatChapterName(info.LibraryType, true, true) +
|
||||
info.ChapterNumber;
|
||||
|
||||
@ -32,4 +32,8 @@ public sealed record LicenseInfoDto
|
||||
/// A license is stored within Kavita
|
||||
/// </summary>
|
||||
public bool HasLicense { get; set; }
|
||||
/// <summary>
|
||||
/// InstallId which can be given to support
|
||||
/// </summary>
|
||||
public string InstallId { get; set; }
|
||||
}
|
||||
|
||||
@ -58,6 +58,8 @@ public interface IReadingListRepository
|
||||
Task<IEnumerable<ReadingList>> GetReadingListsByIds(IList<int> ids, ReadingListIncludes includes = ReadingListIncludes.Items);
|
||||
Task<IEnumerable<ReadingList>> GetReadingListsBySeriesId(int seriesId, ReadingListIncludes includes = ReadingListIncludes.Items);
|
||||
Task<ReadingListInfoDto?> GetReadingListInfoAsync(int readingListId);
|
||||
Task<bool> AnyUserReadingProgressAsync(int readingListId, int userId);
|
||||
Task<ReadingListItemDto?> GetContinueReadingPoint(int readingListId, int userId);
|
||||
}
|
||||
|
||||
public class ReadingListRepository : IReadingListRepository
|
||||
@ -357,13 +359,128 @@ public class ReadingListRepository : IReadingListRepository
|
||||
.SingleOrDefaultAsync();
|
||||
}
|
||||
|
||||
public async Task<bool> AnyUserReadingProgressAsync(int readingListId, int userId)
|
||||
{
|
||||
// Since the list is already created, we can assume RBS doesn't need to apply
|
||||
var chapterIdsQuery = _context.ReadingListItem
|
||||
.Where(s => s.ReadingListId == readingListId)
|
||||
.Select(s => s.ChapterId)
|
||||
.AsQueryable();
|
||||
|
||||
return await _context.AppUserProgresses
|
||||
.Where(p => chapterIdsQuery.Contains(p.ChapterId) && p.AppUserId == userId)
|
||||
.AsNoTracking()
|
||||
.AnyAsync();
|
||||
}
|
||||
|
||||
public async Task<ReadingListItemDto?> GetContinueReadingPoint(int readingListId, int userId)
|
||||
{
|
||||
var userLibraries = _context.Library.GetUserLibraries(userId);
|
||||
|
||||
var query = _context.ReadingListItem
|
||||
.Where(rli => rli.ReadingListId == readingListId)
|
||||
.Join(_context.Chapter, rli => rli.ChapterId, chapter => chapter.Id, (rli, chapter) => new
|
||||
{
|
||||
ReadingListItem = rli,
|
||||
Chapter = chapter,
|
||||
})
|
||||
.Join(_context.Volume, x => x.ReadingListItem.VolumeId, volume => volume.Id, (x, volume) => new
|
||||
{
|
||||
x.ReadingListItem,
|
||||
x.Chapter,
|
||||
Volume = volume
|
||||
})
|
||||
.Join(_context.Series, x => x.ReadingListItem.SeriesId, series => series.Id, (x, series) => new
|
||||
{
|
||||
x.ReadingListItem,
|
||||
x.Chapter,
|
||||
x.Volume,
|
||||
Series = series
|
||||
})
|
||||
.Where(x => userLibraries.Contains(x.Series.LibraryId))
|
||||
.GroupJoin(_context.AppUserProgresses.Where(p => p.AppUserId == userId),
|
||||
x => x.ReadingListItem.ChapterId,
|
||||
progress => progress.ChapterId,
|
||||
(x, progressGroup) => new
|
||||
{
|
||||
x.ReadingListItem,
|
||||
x.Chapter,
|
||||
x.Volume,
|
||||
x.Series,
|
||||
ProgressGroup = progressGroup
|
||||
})
|
||||
.SelectMany(
|
||||
x => x.ProgressGroup.DefaultIfEmpty(),
|
||||
(x, progress) => new
|
||||
{
|
||||
x.ReadingListItem,
|
||||
x.Chapter,
|
||||
x.Volume,
|
||||
x.Series,
|
||||
Progress = progress,
|
||||
PagesRead = progress != null ? progress.PagesRead : 0,
|
||||
HasProgress = progress != null,
|
||||
IsPartiallyRead = progress != null && progress.PagesRead > 0 && progress.PagesRead < x.Chapter.Pages,
|
||||
IsUnread = progress == null || progress.PagesRead == 0
|
||||
})
|
||||
.OrderBy(x => x.ReadingListItem.Order);
|
||||
|
||||
// First try to find a partially read item then the first unread item
|
||||
var item = await query
|
||||
.OrderBy(x => x.IsPartiallyRead ? 0 : x.IsUnread ? 1 : 2)
|
||||
.ThenBy(x => x.ReadingListItem.Order)
|
||||
.FirstOrDefaultAsync();
|
||||
|
||||
|
||||
if (item == null) return null;
|
||||
|
||||
// Map to DTO
|
||||
var library = await _context.Library
|
||||
.Where(l => l.Id == item.Series.LibraryId)
|
||||
.Select(l => new { l.Name, l.Type })
|
||||
.FirstAsync();
|
||||
|
||||
var dto = new ReadingListItemDto
|
||||
{
|
||||
Id = item.ReadingListItem.Id,
|
||||
ChapterId = item.ReadingListItem.ChapterId,
|
||||
Order = item.ReadingListItem.Order,
|
||||
SeriesId = item.ReadingListItem.SeriesId,
|
||||
SeriesName = item.Series.Name,
|
||||
SeriesFormat = item.Series.Format,
|
||||
PagesTotal = item.Chapter.Pages,
|
||||
PagesRead = item.PagesRead,
|
||||
ChapterNumber = item.Chapter.Range,
|
||||
VolumeNumber = item.Volume.Name,
|
||||
LibraryId = item.Series.LibraryId,
|
||||
VolumeId = item.Volume.Id,
|
||||
ReadingListId = item.ReadingListItem.ReadingListId,
|
||||
ReleaseDate = item.Chapter.ReleaseDate,
|
||||
LibraryType = library.Type,
|
||||
ChapterTitleName = item.Chapter.TitleName,
|
||||
LibraryName = library.Name,
|
||||
FileSize = item.Chapter.Files.Sum(f => f.Bytes), // TODO: See if we can put FileSize on the chapter in future
|
||||
Summary = item.Chapter.Summary,
|
||||
IsSpecial = item.Chapter.IsSpecial,
|
||||
LastReadingProgressUtc = item.Progress?.LastModifiedUtc
|
||||
};
|
||||
|
||||
dto.Title = ReadingListService.FormatTitle(dto);
|
||||
|
||||
return dto;
|
||||
}
|
||||
|
||||
|
||||
public async Task<IEnumerable<ReadingListItemDto>> GetReadingListItemDtosByIdAsync(int readingListId, int userId, UserParams? userParams = null)
|
||||
{
|
||||
var userLibraries = _context.Library.GetUserLibraries(userId);
|
||||
|
||||
var query = _context.ReadingListItem
|
||||
.Where(s => s.ReadingListId == readingListId)
|
||||
.Join(_context.Chapter, s => s.ChapterId, chapter => chapter.Id, (data, chapter) => new
|
||||
.Where(s => s.ReadingListId == readingListId)
|
||||
.Join(_context.Chapter,
|
||||
s => s.ChapterId,
|
||||
chapter => chapter.Id,
|
||||
(data, chapter) => new
|
||||
{
|
||||
TotalPages = chapter.Pages,
|
||||
ChapterNumber = chapter.Range,
|
||||
@ -373,9 +490,11 @@ public class ReadingListRepository : IReadingListRepository
|
||||
FileSize = chapter.Files.Sum(f => f.Bytes),
|
||||
chapter.Summary,
|
||||
chapter.IsSpecial
|
||||
|
||||
})
|
||||
.Join(_context.Volume, s => s.ReadingListItem.VolumeId, volume => volume.Id, (data, volume) => new
|
||||
.Join(_context.Volume,
|
||||
s => s.ReadingListItem.VolumeId,
|
||||
volume => volume.Id,
|
||||
(data, volume) => new
|
||||
{
|
||||
data.ReadingListItem,
|
||||
data.TotalPages,
|
||||
@ -388,59 +507,68 @@ public class ReadingListRepository : IReadingListRepository
|
||||
VolumeId = volume.Id,
|
||||
VolumeNumber = volume.Name,
|
||||
})
|
||||
.Join(_context.Series, s => s.ReadingListItem.SeriesId, series => series.Id,
|
||||
(data, s) => new
|
||||
{
|
||||
SeriesName = s.Name,
|
||||
SeriesFormat = s.Format,
|
||||
s.LibraryId,
|
||||
data.ReadingListItem,
|
||||
data.TotalPages,
|
||||
data.ChapterNumber,
|
||||
data.VolumeNumber,
|
||||
data.VolumeId,
|
||||
data.ReleaseDate,
|
||||
data.ChapterTitleName,
|
||||
data.FileSize,
|
||||
data.Summary,
|
||||
data.IsSpecial,
|
||||
LibraryName = _context.Library.Where(l => l.Id == s.LibraryId).Select(l => l.Name).Single(),
|
||||
LibraryType = _context.Library.Where(l => l.Id == s.LibraryId).Select(l => l.Type).Single()
|
||||
})
|
||||
.Select(data => new ReadingListItemDto()
|
||||
.Join(_context.Series,
|
||||
s => s.ReadingListItem.SeriesId,
|
||||
series => series.Id,
|
||||
(data, s) => new
|
||||
{
|
||||
Id = data.ReadingListItem.Id,
|
||||
ChapterId = data.ReadingListItem.ChapterId,
|
||||
Order = data.ReadingListItem.Order,
|
||||
SeriesId = data.ReadingListItem.SeriesId,
|
||||
SeriesName = data.SeriesName,
|
||||
SeriesFormat = data.SeriesFormat,
|
||||
PagesTotal = data.TotalPages,
|
||||
ChapterNumber = data.ChapterNumber,
|
||||
VolumeNumber = data.VolumeNumber,
|
||||
LibraryId = data.LibraryId,
|
||||
VolumeId = data.VolumeId,
|
||||
ReadingListId = data.ReadingListItem.ReadingListId,
|
||||
ReleaseDate = data.ReleaseDate,
|
||||
LibraryType = data.LibraryType,
|
||||
ChapterTitleName = data.ChapterTitleName,
|
||||
LibraryName = data.LibraryName,
|
||||
FileSize = data.FileSize,
|
||||
Summary = data.Summary,
|
||||
IsSpecial = data.IsSpecial
|
||||
SeriesName = s.Name,
|
||||
SeriesFormat = s.Format,
|
||||
s.LibraryId,
|
||||
data.ReadingListItem,
|
||||
data.TotalPages,
|
||||
data.ChapterNumber,
|
||||
data.VolumeNumber,
|
||||
data.VolumeId,
|
||||
data.ReleaseDate,
|
||||
data.ChapterTitleName,
|
||||
data.FileSize,
|
||||
data.Summary,
|
||||
data.IsSpecial,
|
||||
LibraryName = _context.Library.Where(l => l.Id == s.LibraryId).Select(l => l.Name).Single(),
|
||||
LibraryType = _context.Library.Where(l => l.Id == s.LibraryId).Select(l => l.Type).Single()
|
||||
})
|
||||
.Where(o => userLibraries.Contains(o.LibraryId))
|
||||
.OrderBy(rli => rli.Order)
|
||||
.AsSplitQuery();
|
||||
.GroupJoin(_context.AppUserProgresses.Where(p => p.AppUserId == userId),
|
||||
data => data.ReadingListItem.ChapterId,
|
||||
progress => progress.ChapterId,
|
||||
(data, progressGroup) => new { Data = data, ProgressGroup = progressGroup })
|
||||
.SelectMany(
|
||||
x => x.ProgressGroup.DefaultIfEmpty(),
|
||||
(x, progress) => new ReadingListItemDto()
|
||||
{
|
||||
Id = x.Data.ReadingListItem.Id,
|
||||
ChapterId = x.Data.ReadingListItem.ChapterId,
|
||||
Order = x.Data.ReadingListItem.Order,
|
||||
SeriesId = x.Data.ReadingListItem.SeriesId,
|
||||
SeriesName = x.Data.SeriesName,
|
||||
SeriesFormat = x.Data.SeriesFormat,
|
||||
PagesTotal = x.Data.TotalPages,
|
||||
ChapterNumber = x.Data.ChapterNumber,
|
||||
VolumeNumber = x.Data.VolumeNumber,
|
||||
LibraryId = x.Data.LibraryId,
|
||||
VolumeId = x.Data.VolumeId,
|
||||
ReadingListId = x.Data.ReadingListItem.ReadingListId,
|
||||
ReleaseDate = x.Data.ReleaseDate,
|
||||
LibraryType = x.Data.LibraryType,
|
||||
ChapterTitleName = x.Data.ChapterTitleName,
|
||||
LibraryName = x.Data.LibraryName,
|
||||
FileSize = x.Data.FileSize,
|
||||
Summary = x.Data.Summary,
|
||||
IsSpecial = x.Data.IsSpecial,
|
||||
PagesRead = progress != null ? progress.PagesRead : 0,
|
||||
LastReadingProgressUtc = progress != null ? progress.LastModifiedUtc : null
|
||||
})
|
||||
.Where(o => userLibraries.Contains(o.LibraryId))
|
||||
.OrderBy(rli => rli.Order)
|
||||
.AsSplitQuery();
|
||||
|
||||
if (userParams != null)
|
||||
{
|
||||
query = query
|
||||
.Skip((userParams.PageNumber - 1) * userParams.PageSize) // NOTE: PageNumber starts at 1 with PagedList, so copy logic here
|
||||
.Skip((userParams.PageNumber - 1) * userParams.PageSize)
|
||||
.Take(userParams.PageSize);
|
||||
}
|
||||
|
||||
|
||||
var items = await query.ToListAsync();
|
||||
|
||||
foreach (var item in items)
|
||||
@ -448,22 +576,6 @@ public class ReadingListRepository : IReadingListRepository
|
||||
item.Title = ReadingListService.FormatTitle(item);
|
||||
}
|
||||
|
||||
// Attach progress information
|
||||
var fetchedChapterIds = items.Select(i => i.ChapterId);
|
||||
var progresses = await _context.AppUserProgresses
|
||||
.Where(p => fetchedChapterIds.Contains(p.ChapterId))
|
||||
.AsNoTracking()
|
||||
.ToListAsync();
|
||||
|
||||
foreach (var progress in progresses)
|
||||
{
|
||||
var progressItem = items.SingleOrDefault(i => i.ChapterId == progress.ChapterId && i.ReadingListId == readingListId);
|
||||
if (progressItem == null) continue;
|
||||
|
||||
progressItem.PagesRead = progress.PagesRead;
|
||||
progressItem.LastReadingProgressUtc = progress.LastModifiedUtc;
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
|
||||
@ -596,13 +596,18 @@ public class OpdsService : IOpdsService
|
||||
|
||||
|
||||
// Check if there is reading progress or not, if so, inject a "continue-reading" item
|
||||
var firstReadReadingListItem = items.FirstOrDefault(i => i.PagesRead > 0 && i.PagesRead != i.PagesTotal) ??
|
||||
items.FirstOrDefault(i => i.PagesRead == 0 && i.PagesRead != i.PagesTotal);
|
||||
if (firstReadReadingListItem != null && request.PageNumber == FirstPageNumber)
|
||||
var anyProgress = await _unitOfWork.ReadingListRepository.AnyUserReadingProgressAsync(readingListId, userId);
|
||||
if (anyProgress)
|
||||
{
|
||||
await AddContinueReadingPoint(firstReadReadingListItem, feed, request);
|
||||
var firstReadReadingListItem = await _unitOfWork.ReadingListRepository.GetContinueReadingPoint(readingListId, userId);
|
||||
if (firstReadReadingListItem != null && request.PageNumber == FirstPageNumber)
|
||||
{
|
||||
await AddContinueReadingPoint(firstReadReadingListItem, feed, request);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
foreach (var item in items)
|
||||
{
|
||||
var chapterDto = await _unitOfWork.ChapterRepository.GetChapterDtoAsync(item.ChapterId, userId);
|
||||
@ -1003,7 +1008,7 @@ public class OpdsService : IOpdsService
|
||||
|
||||
var pageNumber = Math.Max(list.CurrentPage, 1);
|
||||
|
||||
if (pageNumber > 1)
|
||||
if (pageNumber > FirstPageNumber)
|
||||
{
|
||||
feed.Links.Add(CreateLink(FeedLinkRelation.Prev, FeedLinkType.AtomNavigation, url + "pageNumber=" + (pageNumber - 1)));
|
||||
}
|
||||
|
||||
@ -265,8 +265,6 @@ public class LicenseService(
|
||||
if (cacheValue.HasValue) return cacheValue.Value;
|
||||
}
|
||||
|
||||
// TODO: If info.IsCancelled && notActive, let's remove the license so we aren't constantly checking
|
||||
|
||||
try
|
||||
{
|
||||
var encryptedLicense = await unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.LicenseKey);
|
||||
@ -286,17 +284,25 @@ public class LicenseService(
|
||||
.All(r => new Version(r.UpdateVersion) <= BuildInfo.Version);
|
||||
|
||||
response.HasLicense = hasLicense;
|
||||
response.InstallId = HashUtil.ServerToken();
|
||||
|
||||
// Cache if the license is valid here as well
|
||||
var licenseProvider = cachingProviderFactory.GetCachingProvider(EasyCacheProfiles.License);
|
||||
await licenseProvider.SetAsync(CacheKey, response.IsActive, _licenseCacheTimeout);
|
||||
|
||||
// TODO: If info.IsCancelled && notActive, let's remove the license so we aren't constantly checking
|
||||
if (response is {IsCancelled: true, IsActive: false})
|
||||
{
|
||||
//logger.LogWarning("Kavita+ License is no longer active, removing Server registration");
|
||||
}
|
||||
|
||||
// Cache the license info if IsActive and ExpirationDate > DateTime.UtcNow + 2
|
||||
if (response.IsActive && response.ExpirationDate > DateTime.UtcNow.AddDays(2))
|
||||
{
|
||||
await licenseInfoProvider.SetAsync(LicenseInfoCacheKey, response, _licenseCacheTimeout);
|
||||
}
|
||||
|
||||
|
||||
return response;
|
||||
}
|
||||
catch (FlurlHttpException e)
|
||||
|
||||
@ -6,4 +6,5 @@ export interface LicenseInfo {
|
||||
registeredEmail: string;
|
||||
totalMonthsSubbed: number;
|
||||
hasLicense: boolean;
|
||||
installId: string;
|
||||
}
|
||||
|
||||
@ -37,7 +37,6 @@ export class FontService {
|
||||
}
|
||||
|
||||
getFontFace(font: EpubFont): FontFace {
|
||||
// TODO: We need to refactor this so that we loadFonts with an array, fonts have an id to remove them, and we don't keep populating the document
|
||||
if (font.provider === FontProvider.System) {
|
||||
return new FontFace(font.name, `url('/assets/fonts/${font.name}/${font.fileName}')`);
|
||||
}
|
||||
|
||||
@ -179,6 +179,14 @@
|
||||
</app-setting-item>
|
||||
</div>
|
||||
|
||||
<div class="mb-2 col-md-6 col-sm-12">
|
||||
<app-setting-item [canEdit]="false" [showEdit]="false" [title]="t('supportId-label')">
|
||||
<ng-template #view>
|
||||
{{licInfo.installId}}
|
||||
</ng-template>
|
||||
</app-setting-item>
|
||||
</div>
|
||||
|
||||
<div class="setting-section-break"></div>
|
||||
|
||||
<!-- Actions around license -->
|
||||
|
||||
@ -7,7 +7,7 @@
|
||||
<div ngbAccordion class="mb-2">
|
||||
<div ngbAccordionItem class="p-2">
|
||||
<h2 ngbAccordionHeader>
|
||||
<button ngbAccordionButton>
|
||||
<button ngbAccordionButton class="accordion-button">
|
||||
<h4 class="changelog-header">{{update.updateTitle}}
|
||||
@if (update.isOnNightlyInRelease) {
|
||||
<span class="badge bg-secondary">{{t('nightly', {version: update.currentVersion})}}</span>
|
||||
@ -17,6 +17,7 @@
|
||||
<span class="badge bg-secondary">{{t('available')}}</span>
|
||||
}
|
||||
</h4>
|
||||
<i class="fas fa-chevron-up" aria-hidden="true"></i>
|
||||
</button>
|
||||
</h2>
|
||||
<div ngbAccordionCollapse>
|
||||
|
||||
@ -6,3 +6,7 @@
|
||||
.changelog-header {
|
||||
color: var(--body-text-color);
|
||||
}
|
||||
|
||||
.accordion-button {
|
||||
color: var(--body-text-color);
|
||||
}
|
||||
|
||||
@ -37,7 +37,8 @@
|
||||
|
||||
|
||||
@if(showPageLink()) {
|
||||
<span class="badge bg-secondary clickable" (click)="loadAnnotation()">{{t('page-num', {page: annotation().pageNumber})}}</span>
|
||||
<!-- Page numbers are incremented by 1 in the UI -->
|
||||
<span class="badge bg-secondary clickable" (click)="loadAnnotation()">{{t('page-num', {page: annotation().pageNumber + 1})}}</span>
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -89,6 +89,10 @@ export class AnnotationCardComponent {
|
||||
* If enabled, listens to annotation updates
|
||||
*/
|
||||
listedToUpdates = input<boolean>(false);
|
||||
/**
|
||||
* If the card is rendered inside the book reader. Used for styling the confirm button
|
||||
*/
|
||||
inBookReader = input<boolean>(false);
|
||||
|
||||
selected = input<boolean>(false);
|
||||
@Output() delete = new EventEmitter();
|
||||
@ -157,7 +161,10 @@ export class AnnotationCardComponent {
|
||||
}
|
||||
|
||||
async deleteAnnotation() {
|
||||
if (!await this.confirmService.confirm(translate('toasts.confirm-delete-annotation'))) return;
|
||||
if (!await this.confirmService.confirm(translate('toasts.confirm-delete-annotation'), {
|
||||
...this.confirmService.defaultConfirm,
|
||||
bookReader: this.inBookReader(),
|
||||
})) return;
|
||||
const annotation = this.annotation();
|
||||
if (!annotation) return;
|
||||
|
||||
|
||||
@ -36,6 +36,7 @@
|
||||
(delete)="handleDelete(annotation)"
|
||||
(navigate)="handleNavigateTo($event)"
|
||||
[forceSize]="false"
|
||||
[inBookReader]="true"
|
||||
/>
|
||||
}
|
||||
@empty {
|
||||
|
||||
@ -2259,11 +2259,13 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
|
||||
async viewAnnotations() {
|
||||
await this.epubMenuService.openViewAnnotationsDrawer((annotation: Annotation) => {
|
||||
const currentPageNum = this.pageNum();
|
||||
if (this.pageNum() != annotation.pageNumber) {
|
||||
this.setPageNum(annotation.pageNumber);
|
||||
}
|
||||
|
||||
if (annotation.xPath != null) {
|
||||
this.adhocPageHistory.push({page: currentPageNum, scrollPart: this.readerService.scopeBookReaderXpath(this.lastSeenScrollPartPath)});
|
||||
this.loadPage(annotation.xPath);
|
||||
}
|
||||
});
|
||||
|
||||
@ -6,6 +6,7 @@
|
||||
<h2 class="accordion-header" ngbAccordionHeader>
|
||||
<button ngbAccordionButton class="accordion-button" type="button" [attr.aria-expanded]="acc.isExpanded('general-panel')" aria-controls="collapseOne">
|
||||
{{t('general-settings-title')}}
|
||||
<i class="fas fa-chevron-up" aria-hidden="true"></i>
|
||||
</button>
|
||||
</h2>
|
||||
<div ngbAccordionCollapse>
|
||||
@ -68,6 +69,7 @@
|
||||
<h2 class="accordion-header" ngbAccordionHeader>
|
||||
<button class="accordion-button" ngbAccordionButton type="button" [attr.aria-expanded]="acc.isExpanded('reader-panel')" aria-controls="collapseOne">
|
||||
{{t('reader-settings-title')}}
|
||||
<i class="fas fa-chevron-up" aria-hidden="true"></i>
|
||||
</button>
|
||||
</h2>
|
||||
<div ngbAccordionCollapse>
|
||||
@ -157,6 +159,7 @@
|
||||
<h2 class="accordion-header" ngbAccordionHeader>
|
||||
<button class="accordion-button" ngbAccordionButton type="button" [attr.aria-expanded]="acc.isExpanded('color-panel')" aria-controls="collapseOne">
|
||||
{{t('color-theme-title')}}
|
||||
<i class="fas fa-chevron-up" aria-hidden="true"></i>
|
||||
</button>
|
||||
</h2>
|
||||
<div ngbAccordionCollapse>
|
||||
|
||||
@ -310,7 +310,6 @@ export class EditSeriesModalComponent implements OnInit {
|
||||
this.volumeCollapsed[v.name] = true;
|
||||
});
|
||||
this.seriesVolumes.forEach(vol => {
|
||||
//.sort(this.utilityService.sortChapters) (no longer needed, all data is sorted on the backend)
|
||||
vol.volumeFiles = vol.chapters?.map((c: Chapter) => c.files.map((f: any) => {
|
||||
// TODO: Identify how to fix this hack
|
||||
f.chapter = c.range;
|
||||
|
||||
@ -241,7 +241,7 @@ export class CardItemComponent implements OnInit {
|
||||
}
|
||||
} else if (this.utilityService.isSeries(this.entity)) {
|
||||
this.tooltipTitle = this.title || (this.utilityService.asSeries(this.entity).name);
|
||||
} else if (this.entity.hasOwnProperty('expectedDate')) {
|
||||
} else if (this.entity.hasOwnProperty('expectedDate')) { // Upcoming Chapter Entity
|
||||
this.suppressArchiveWarning = true;
|
||||
this.imageUrl = '';
|
||||
const nextDate = (this.entity as NextExpectedChapter);
|
||||
@ -250,7 +250,6 @@ export class CardItemComponent implements OnInit {
|
||||
// this.overlayInformation = `
|
||||
// <i class="fa-regular fa-clock mb-2" style="font-size: 26px" aria-hidden="true"></i>
|
||||
// <div>${tokens[0]}</div><div>${tokens[1]}</div>`;
|
||||
// // todo: figure out where this caller is
|
||||
this.centerOverlay = true;
|
||||
|
||||
if (nextDate.expectedDate) {
|
||||
|
||||
@ -19,7 +19,7 @@ $image-height: 160px;
|
||||
position: relative;
|
||||
display: flex;
|
||||
background-color: hsl(0deg 0% 0% / 12%);
|
||||
/* TODO: Robbie fix this hack */
|
||||
|
||||
.missing-img {
|
||||
align-self: center;
|
||||
display: flex;
|
||||
|
||||
@ -86,7 +86,7 @@
|
||||
</div>
|
||||
|
||||
<div class="mt-2 mb-3">
|
||||
<app-read-more [text]="chapter.summary || ''" [maxLength]="utilityService.getActiveBreakpoint() >= Breakpoint.Desktop ? 170 : 200" />
|
||||
<app-read-more [text]="chapter.summary || ''" [maxLength]="utilityService.activeUserBreakpoint() < UserBreakpoint.Desktop ? 170 : 200" />
|
||||
</div>
|
||||
|
||||
<div class="mt-2">
|
||||
|
||||
@ -61,7 +61,7 @@ import {
|
||||
} from "../series-detail/_components/metadata-detail-row/metadata-detail-row.component";
|
||||
import {DownloadButtonComponent} from "../series-detail/_components/download-button/download-button.component";
|
||||
import {hasAnyCast} from "../_models/common/i-has-cast";
|
||||
import {Breakpoint, UtilityService} from "../shared/_services/utility.service";
|
||||
import {Breakpoint, UserBreakpoint, UtilityService} from "../shared/_services/utility.service";
|
||||
import {EVENTS, MessageHubService} from "../_services/message-hub.service";
|
||||
import {CoverUpdateEvent} from "../_models/events/cover-update-event";
|
||||
import {ChapterRemovedEvent} from "../_models/events/chapter-removed-event";
|
||||
@ -156,7 +156,7 @@ export class ChapterDetailComponent implements OnInit {
|
||||
protected readonly AgeRating = AgeRating;
|
||||
protected readonly TabID = TabID;
|
||||
protected readonly FilterField = FilterField;
|
||||
protected readonly Breakpoint = Breakpoint;
|
||||
protected readonly UserBreakpoint = UserBreakpoint;
|
||||
protected readonly LibraryType = LibraryType;
|
||||
protected readonly encodeURIComponent = encodeURIComponent;
|
||||
|
||||
|
||||
@ -39,7 +39,7 @@
|
||||
<div class="col-md-10 col-xs-8 col-sm-6 mt-2">
|
||||
@if (summary.length > 0) {
|
||||
<div class="mb-2">
|
||||
<app-read-more [text]="summary" [maxLength]="(utilityService.activeBreakpoint$ | async)! >= Breakpoint.Desktop ? 170 : 200" />
|
||||
<app-read-more [text]="summary" [maxLength]="utilityService.activeUserBreakpoint() < UserBreakpoint.Desktop ? 170 : 200" />
|
||||
</div>
|
||||
|
||||
@if (collectionTag.source !== ScrobbleProvider.Kavita) {
|
||||
|
||||
@ -8,7 +8,7 @@ import {debounceTime, take} from 'rxjs/operators';
|
||||
import {BulkSelectionService} from 'src/app/cards/bulk-selection.service';
|
||||
import {EditCollectionTagsComponent} from 'src/app/cards/_modals/edit-collection-tags/edit-collection-tags.component';
|
||||
import {FilterUtilitiesService} from 'src/app/shared/_services/filter-utilities.service';
|
||||
import {Breakpoint, UtilityService} from 'src/app/shared/_services/utility.service';
|
||||
import {Breakpoint, UserBreakpoint, UtilityService} from 'src/app/shared/_services/utility.service';
|
||||
import {UserCollection} from 'src/app/_models/collection-tag';
|
||||
import {SeriesAddedToCollectionEvent} from 'src/app/_models/events/series-added-to-collection-event';
|
||||
import {JumpKey} from 'src/app/_models/jumpbar/jump-key';
|
||||
@ -90,7 +90,7 @@ export class CollectionDetailComponent implements OnInit, AfterContentChecked {
|
||||
private readonly metadataService = inject(MetadataService);
|
||||
|
||||
protected readonly ScrobbleProvider = ScrobbleProvider;
|
||||
protected readonly Breakpoint = Breakpoint;
|
||||
protected readonly UserBreakpoint = UserBreakpoint;
|
||||
|
||||
@ViewChild('scrollingBlock') scrollingBlock: ElementRef<HTMLDivElement> | undefined;
|
||||
@ViewChild('companionBar') companionBar: ElementRef<HTMLDivElement> | undefined;
|
||||
|
||||
@ -26,7 +26,7 @@
|
||||
<div class="subtitle">
|
||||
{{chapterTitleLabel()}}
|
||||
@if (totalSeriesPages > 0) {
|
||||
<span>{{t('series-progress', {percentage: (Math.min(1, ((totalSeriesPagesRead + pageNum) / totalSeriesPages)) | percent)}) }}</span>
|
||||
<span class="ms-2">{{t('series-progress', {percentage: (Math.min(1, ((totalSeriesPagesRead + pageNum) / totalSeriesPages)) | percent)}) }}</span>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -501,7 +501,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
return this.bookmarks[this.pageNum];
|
||||
}
|
||||
|
||||
return chapterInfo?.chapterTitle ?? '';
|
||||
return chapterInfo?.chapterTitle || chapterInfo?.subtitle || '';
|
||||
});
|
||||
|
||||
|
||||
@ -835,7 +835,8 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
|
||||
//send the current width override value to the label
|
||||
this.widthOverrideLabel$ = this.readerSettings$?.pipe(
|
||||
map(values => (parseInt(values.widthSlider) <= 0) ? '' : values.widthSlider + '%'),
|
||||
map(values => parseInt(values.widthSlider)),
|
||||
map(widthOverride => (isNaN(widthOverride)) ? '' : widthOverride + '%'),
|
||||
takeUntilDestroyed(this.destroyRef)
|
||||
);
|
||||
}
|
||||
|
||||
@ -24,8 +24,9 @@
|
||||
@if (fileToProcess.validateSummary; as summary) {
|
||||
<div ngbAccordionItem>
|
||||
<h5 ngbAccordionHeader>
|
||||
<button ngbAccordionButton>
|
||||
<button ngbAccordionButton class="accordion-button">
|
||||
<ng-container [ngTemplateOutlet]="heading" [ngTemplateOutletContext]="{ summary: summary, filename: fileToProcess.fileName }" />
|
||||
<i class="fas fa-chevron-up" aria-hidden="true"></i>
|
||||
</button>
|
||||
</h5>
|
||||
<div ngbAccordionCollapse>
|
||||
@ -48,8 +49,9 @@
|
||||
@if (fileToProcess.dryRunSummary; as summary) {
|
||||
<div ngbAccordionItem>
|
||||
<h5 ngbAccordionHeader>
|
||||
<button ngbAccordionButton>
|
||||
<button ngbAccordionButton class="accordion-button">
|
||||
<ng-container [ngTemplateOutlet]="heading" [ngTemplateOutletContext]="{ summary: summary, filename: fileToProcess.fileName }" />
|
||||
<i class="fas fa-chevron-up" aria-hidden="true"></i>
|
||||
</button>
|
||||
</h5>
|
||||
<div ngbAccordionCollapse>
|
||||
@ -70,8 +72,9 @@
|
||||
@if (fileToProcess.finalizeSummary; as summary) {
|
||||
<div ngbAccordionItem>
|
||||
<h5 ngbAccordionHeader>
|
||||
<button ngbAccordionButton>
|
||||
<button ngbAccordionButton class="accordion-button">
|
||||
<ng-container [ngTemplateOutlet]="heading" [ngTemplateOutletContext]="{ summary: summary, filename: fileToProcess.fileName }" />
|
||||
<i class="fas fa-chevron-up" aria-hidden="true"></i>
|
||||
</button>
|
||||
</h5>
|
||||
<div ngbAccordionCollapse>
|
||||
|
||||
@ -100,7 +100,7 @@
|
||||
</div>
|
||||
|
||||
<div class="mt-2 mb-3">
|
||||
<app-read-more [text]="readingList.summary || ''" [maxLength]="(utilityService.activeBreakpoint$ | async)! >= Breakpoint.Desktop ? 170 : 200" />
|
||||
<app-read-more [text]="readingList.summary || ''" [maxLength]="utilityService.activeUserBreakpoint() < UserBreakpoint.Desktop ? 170 : 200" />
|
||||
</div>
|
||||
|
||||
<div class="mt-2 upper-details">
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, DestroyRef, ElementRef, inject, OnInit, ViewChild } from '@angular/core';
|
||||
import {ActivatedRoute, Router, RouterLink} from '@angular/router';
|
||||
import {AsyncPipe, DatePipe, DecimalPipe, DOCUMENT, Location, NgClass, NgStyle} from '@angular/common';
|
||||
import {DatePipe, DecimalPipe, DOCUMENT, Location, NgClass, NgStyle} from '@angular/common';
|
||||
import {ToastrService} from 'ngx-toastr';
|
||||
import {take} from 'rxjs/operators';
|
||||
import {ConfirmService} from 'src/app/shared/confirm.service';
|
||||
import {Breakpoint, UtilityService} from 'src/app/shared/_services/utility.service';
|
||||
import {Breakpoint, UserBreakpoint, UtilityService} from 'src/app/shared/_services/utility.service';
|
||||
import {LibraryType} from 'src/app/_models/library/library';
|
||||
import {MangaFormat} from 'src/app/_models/manga-format';
|
||||
import {ReadingList, ReadingListInfo, ReadingListItem} from 'src/app/_models/reading-list';
|
||||
@ -65,7 +65,7 @@ enum TabID {
|
||||
imports: [CardActionablesComponent, ImageComponent, NgbDropdown,
|
||||
NgbDropdownToggle, NgbDropdownMenu, NgbDropdownItem, ReadMoreComponent, BadgeExpanderComponent,
|
||||
LoadingComponent, DraggableOrderedListComponent,
|
||||
ReadingListItemComponent, NgClass, AsyncPipe, DecimalPipe, DatePipe, TranslocoDirective, ReactiveFormsModule,
|
||||
ReadingListItemComponent, NgClass, DecimalPipe, DatePipe, TranslocoDirective, ReactiveFormsModule,
|
||||
NgbNav, NgbNavContent, NgbNavLink, NgbTooltip,
|
||||
RouterLink, VirtualScrollerModule, NgStyle, NgbNavOutlet, NgbNavItem, PromotedIconComponent, DefaultValuePipe, DetailsTabComponent]
|
||||
})
|
||||
@ -75,6 +75,7 @@ export class ReadingListDetailComponent implements OnInit {
|
||||
|
||||
protected readonly MangaFormat = MangaFormat;
|
||||
protected readonly Breakpoint = Breakpoint;
|
||||
protected readonly UserBreakpoint = UserBreakpoint;
|
||||
protected readonly TabID = TabID;
|
||||
protected readonly encodeURIComponent = encodeURIComponent;
|
||||
|
||||
|
||||
@ -32,7 +32,7 @@
|
||||
|
||||
@if (showOidcButton()) {
|
||||
<a
|
||||
class="btn btn-outline-primary mt-2 d-flex align-items-center gap-2"
|
||||
class="btn btn-outline-primary mt-2 d-flex justify-content-center align-items-center gap-2"
|
||||
href="oidc/login"
|
||||
>
|
||||
<app-image height="36px" width="36px" [imageUrl]="'assets/icons/open-id-connect-logo.svg'" [styles]="{'object-fit': 'contains'}" />
|
||||
|
||||
@ -109,7 +109,7 @@
|
||||
</div>
|
||||
|
||||
<div class="mt-2 mb-3">
|
||||
<app-read-more [text]="seriesMetadata.summary || ''" [maxLength]="(utilityService.activeBreakpoint$ | async)! >= Breakpoint.Desktop ? 170 : 200" />
|
||||
<app-read-more [text]="seriesMetadata.summary || ''" [maxLength]="utilityService.activeUserBreakpoint() < UserBreakpoint.Desktop ? 170 : 200" />
|
||||
</div>
|
||||
|
||||
<div class="mt-2 upper-details">
|
||||
|
||||
@ -38,7 +38,7 @@ import {
|
||||
EditSeriesModalComponent
|
||||
} from 'src/app/cards/_modals/edit-series-modal/edit-series-modal.component';
|
||||
import {DownloadEvent, DownloadService} from 'src/app/shared/_services/download.service';
|
||||
import {Breakpoint, UtilityService} from 'src/app/shared/_services/utility.service';
|
||||
import {Breakpoint, UserBreakpoint, UtilityService} from 'src/app/shared/_services/utility.service';
|
||||
import {Chapter, LooseLeafOrDefaultNumber, SpecialVolumeNumber} from 'src/app/_models/chapter';
|
||||
import {Device} from 'src/app/_models/device/device';
|
||||
import {ScanSeriesEvent} from 'src/app/_models/events/scan-series-event';
|
||||
@ -158,7 +158,7 @@ export class SeriesDetailComponent implements OnInit, AfterContentChecked {
|
||||
protected readonly SettingsTabId = SettingsTabId;
|
||||
protected readonly FilterField = FilterField;
|
||||
protected readonly AgeRating = AgeRating;
|
||||
protected readonly Breakpoint = Breakpoint;
|
||||
protected readonly UserBreakpoint = UserBreakpoint;
|
||||
protected readonly encodeURIComponent = encodeURIComponent;
|
||||
|
||||
private readonly destroyRef = inject(DestroyRef);
|
||||
@ -866,9 +866,14 @@ export class SeriesDetailComponent implements OnInit, AfterContentChecked {
|
||||
this.volumes = detail.volumes;
|
||||
this.storyChapters = detail.storylineChapters;
|
||||
|
||||
this.totalSize.set(detail.volumes.reduce((sum, v) => sum + v.chapters.reduce((volumeSum, c) => {
|
||||
return volumeSum + c.files.reduce((chapterSum, f) => chapterSum + f.bytes , 0)
|
||||
}, 0), 0));
|
||||
const uniqueChapters = Array.from(
|
||||
new Map([...detail.chapters, ...detail.volumes.flatMap(v => v.chapters)]
|
||||
.map(c => [c.id, c])).values()
|
||||
);
|
||||
|
||||
this.totalSize.set(uniqueChapters
|
||||
.flatMap(c => c.files)
|
||||
.reduce((sum, f) => sum + f.bytes, 0));
|
||||
|
||||
this.storylineItems = [];
|
||||
const v = this.volumes.map(v => {
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
<ng-container *transloco="let t; prefix: 'multi-check-box-form'">
|
||||
<div class="d-flex justify-content-between">
|
||||
<div class="w-75">
|
||||
<div class="w-70">
|
||||
<h4>{{title()}}</h4>
|
||||
<span class="text-muted d-block">{{tooltip()}}</span>
|
||||
</div>
|
||||
|
||||
@ -1,17 +1,17 @@
|
||||
<ng-container *transloco="let t; prefix: 'badge-expander'">
|
||||
<div class="badge-expander">
|
||||
<div class="content">
|
||||
@for(item of visibleItems; track item; let i = $index; let last = $last) {
|
||||
@for(item of visibleItems(); track item; let i = $index; let last = $last) {
|
||||
<ng-container [ngTemplateOutlet]="itemTemplate" [ngTemplateOutletContext]="{ $implicit: item, idx: i, last: last }" />
|
||||
@if (!last && includeComma) {
|
||||
@if (!last && includeComma()) {
|
||||
<span>, </span>
|
||||
}
|
||||
} @empty {
|
||||
{{null | defaultValue}}
|
||||
}
|
||||
@if (!isCollapsed && itemsLeft !== 0) {
|
||||
<a href="javascript:void(0);" type="button" class="dark-exempt btn-icon ms-1" (click)="toggleVisible()" [attr.aria-expanded]="!isCollapsed">
|
||||
{{t('more-items', {count: itemsLeft})}}
|
||||
@if (isCollapsed() && itemsLeft() !== 0) {
|
||||
<a href="javascript:void(0);" type="button" class="dark-exempt btn-icon ms-1" (click)="toggleVisible()" [attr.aria-expanded]="!isCollapsed()">
|
||||
{{t('more-items', {count: itemsLeft()})}}
|
||||
</a>
|
||||
}
|
||||
</div>
|
||||
|
||||
@ -1,11 +1,9 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
Component, computed,
|
||||
ContentChild, EventEmitter,
|
||||
inject,
|
||||
Input, OnChanges,
|
||||
OnInit, Output, SimpleChanges,
|
||||
input,
|
||||
OnInit, Output, signal,
|
||||
TemplateRef
|
||||
} from '@angular/core';
|
||||
import {NgTemplateOutlet} from "@angular/common";
|
||||
@ -19,59 +17,49 @@ import {DefaultValuePipe} from "../../_pipes/default-value.pipe";
|
||||
styleUrls: ['./badge-expander.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class BadgeExpanderComponent implements OnInit, OnChanges {
|
||||
export class BadgeExpanderComponent implements OnInit {
|
||||
|
||||
private readonly cdRef = inject(ChangeDetectorRef);
|
||||
|
||||
@Input() items: Array<any> = [];
|
||||
@Input() itemsTillExpander: number = 4;
|
||||
@Input() allowToggle: boolean = true;
|
||||
@Input() includeComma: boolean = true;
|
||||
items = input.required<any[]>();
|
||||
itemsTillExpander = input(4);
|
||||
allowToggle = input(true);
|
||||
includeComma = input(true);
|
||||
/**
|
||||
* If should be expanded by default. Defaults to false.
|
||||
* If the list should be expanded by default. Defaults to false.
|
||||
*/
|
||||
@Input() defaultExpanded: boolean = false;
|
||||
defaultExpanded = input(false);
|
||||
|
||||
/**
|
||||
* Invoked when the "and more" is clicked
|
||||
*/
|
||||
@Output() toggle = new EventEmitter<void>();
|
||||
@ContentChild('badgeExpanderItem') itemTemplate!: TemplateRef<any>;
|
||||
|
||||
isCollapsed = signal<boolean | undefined>(undefined);
|
||||
visibleItems = computed(() => {
|
||||
const allItems = this.items();
|
||||
const isCollapsed = this.isCollapsed();
|
||||
const cutOff = this.itemsTillExpander();
|
||||
|
||||
visibleItems: Array<any> = [];
|
||||
isCollapsed: boolean = false;
|
||||
if (!isCollapsed) return allItems;
|
||||
|
||||
get itemsLeft() {
|
||||
if (this.defaultExpanded) return 0;
|
||||
return allItems.slice(0, cutOff);
|
||||
});
|
||||
itemsLeft = computed(() => {
|
||||
const allItems = this.items();
|
||||
const visibleItems = this.visibleItems();
|
||||
|
||||
return Math.max(this.items.length - this.itemsTillExpander, 0);
|
||||
}
|
||||
return allItems.length - visibleItems.length;
|
||||
});
|
||||
|
||||
ngOnInit(): void {
|
||||
|
||||
if (this.defaultExpanded) {
|
||||
this.isCollapsed = false;
|
||||
this.visibleItems = this.items;
|
||||
this.cdRef.markForCheck();
|
||||
return;
|
||||
}
|
||||
|
||||
this.visibleItems = this.items.slice(0, this.itemsTillExpander);
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
ngOnChanges(changes: SimpleChanges) {
|
||||
this.visibleItems = this.items.slice(0, this.itemsTillExpander);
|
||||
this.cdRef.markForCheck();
|
||||
this.isCollapsed.set(!this.defaultExpanded());
|
||||
}
|
||||
|
||||
toggleVisible() {
|
||||
this.toggle.emit();
|
||||
if (!this.allowToggle) return;
|
||||
if (!this.allowToggle()) return;
|
||||
|
||||
this.isCollapsed = !this.isCollapsed;
|
||||
this.visibleItems = this.items;
|
||||
this.cdRef.markForCheck();
|
||||
this.isCollapsed.update(x => !x);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -9,7 +9,7 @@
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
background-color: transparent; /*make scrollbar space invisible */
|
||||
background-color: transparent;
|
||||
width: inherit;
|
||||
display: none;
|
||||
visibility: hidden;
|
||||
@ -17,7 +17,7 @@
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: transparent; /*makes it invisible when not hovering*/
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
|
||||
@ -90,7 +90,7 @@
|
||||
</div>
|
||||
|
||||
<div class="mt-2 mb-3">
|
||||
<app-read-more [text]="volume.chapters[0].summary || ''" [maxLength]="utilityService.getActiveBreakpoint() >= Breakpoint.Desktop ? 170 : 200" />
|
||||
<app-read-more [text]="volume.chapters[0].summary || ''" [maxLength]="utilityService.activeUserBreakpoint() < UserBreakpoint.Desktop ? 170 : 200" />
|
||||
</div>
|
||||
|
||||
<div class="mt-2">
|
||||
|
||||
@ -53,7 +53,7 @@ import {IHasCast} from "../_models/common/i-has-cast";
|
||||
import {EntityTitleComponent} from "../cards/entity-title/entity-title.component";
|
||||
import {VirtualScrollerModule} from "@iharbeck/ngx-virtual-scroller";
|
||||
import {Action, ActionFactoryService, ActionItem} from "../_services/action-factory.service";
|
||||
import {Breakpoint, UtilityService} from "../shared/_services/utility.service";
|
||||
import {Breakpoint, UserBreakpoint, UtilityService} from "../shared/_services/utility.service";
|
||||
import {ChapterCardComponent} from "../cards/chapter-card/chapter-card.component";
|
||||
import {EditVolumeModalComponent} from "../_single-module/edit-volume-modal/edit-volume-modal.component";
|
||||
import {Genre} from "../_models/metadata/genre";
|
||||
@ -194,7 +194,7 @@ export class VolumeDetailComponent implements OnInit {
|
||||
protected readonly AgeRating = AgeRating;
|
||||
protected readonly TabID = TabID;
|
||||
protected readonly FilterField = FilterField;
|
||||
protected readonly Breakpoint = Breakpoint;
|
||||
protected readonly UserBreakpoint = UserBreakpoint;
|
||||
protected readonly encodeURIComponent = encodeURIComponent;
|
||||
|
||||
@ViewChild('scrollingBlock') scrollingBlock: ElementRef<HTMLDivElement> | undefined;
|
||||
|
||||
@ -48,7 +48,7 @@
|
||||
"provider-name-tooltip": "Name shown on the login screen",
|
||||
|
||||
"defaults-title": "Defaults",
|
||||
"defaults-requirement": "The following settings are used when a user is registered via OIDC while SyncUserSettings is turned off",
|
||||
"defaults-requirement": "The following settings are used when a user is registered via OIDC while Sync User Settings is turned off",
|
||||
"default-include-unknowns-label": "Include unknowns",
|
||||
"default-include-unknowns-tooltip": "Include unknown age ratings",
|
||||
"default-age-restriction-label": "Age rating",
|
||||
@ -815,6 +815,7 @@
|
||||
"faq-title": "FAQ",
|
||||
"total-subbed-months-label": "Total Months Subscribed",
|
||||
"email-label": "Registered Email",
|
||||
"supportId-label": "Support Id",
|
||||
|
||||
"license-mismatch": "License may be registered to another Kavita instance. Re-register to fix.",
|
||||
"k+-license-overwrite": "License is registered to another Kavita instance. This can happen on re-installation and rarely due to system updates. Select overwrite to force this instance to register with Kavita+.",
|
||||
@ -2180,7 +2181,7 @@
|
||||
"next": "{{import-cbl-modal.next}}",
|
||||
"save": "{{common.save}}",
|
||||
|
||||
"import-description": "Upload a file you, or someone else has exported to replace or merge with your current settings.",
|
||||
"import-description": "Upload a file you, or someone else, has exported to replace or merge with your current settings.",
|
||||
"select-files-warning": "You must upload a json file to continue",
|
||||
"invalid-file": "Failed to parse your file, check your input",
|
||||
"file-no-valid-content": "Your import did not contain any meaningful data to continue with",
|
||||
|
||||
@ -1,36 +1,53 @@
|
||||
.accordion-header {
|
||||
font-weight: bold;
|
||||
color: var(--accordion-header-text-color);
|
||||
color: var(--accordion-header-text-color);
|
||||
}
|
||||
|
||||
.accordion-item {
|
||||
.accordion-item {
|
||||
background-color: var(--accordion-body-bg-color);
|
||||
color: var(--accordion-body-text-color);
|
||||
border-color: var(--accordion-body-border-color);
|
||||
|
||||
div[role="tabpanel"] {
|
||||
background-color: var(--accordion-header-bg-color);
|
||||
background-color: var(--accordion-header-bg-color);
|
||||
|
||||
.accordion-body {
|
||||
background-color: var(--accordion-active-body-bg-color);
|
||||
background-color: var(--accordion-active-body-bg-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.accordion-button {
|
||||
&:not(.collapsed) {
|
||||
color: var(--accordion-header-text-color);
|
||||
background-color: var(--accordion-body-bg-color);
|
||||
box-shadow: var(--accordion-body-box-shadow);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
&::after {
|
||||
content: none;
|
||||
}
|
||||
|
||||
&:not(.collapsed) {
|
||||
color: var(--accordion-header-text-color);
|
||||
background-color: var(--accordion-body-bg-color);
|
||||
box-shadow: var(--accordion-body-box-shadow);
|
||||
|
||||
i {
|
||||
transform: rotate(180deg);
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
}
|
||||
|
||||
&.collapsed {
|
||||
color: var(--accordion-header-collapsed-text-color);
|
||||
background-color: var(--accordion-header-collapsed-bg-color);
|
||||
|
||||
i {
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
&.collapsed {
|
||||
color: var(--accordion-header-collapsed-text-color);
|
||||
background-color: var(--accordion-header-collapsed-bg-color);
|
||||
}
|
||||
}
|
||||
|
||||
&:focus {
|
||||
border-color: var(--accordion-button-focus-border-color);
|
||||
box-shadow: var(--accordion-button-focus-box-shadow);
|
||||
}
|
||||
&:focus {
|
||||
border-color: var(--accordion-button-focus-border-color);
|
||||
box-shadow: var(--accordion-button-focus-box-shadow);
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user