diff --git a/API.Tests/Services/DirectoryServiceTests.cs b/API.Tests/Services/DirectoryServiceTests.cs index 254d851fa..193114865 100644 --- a/API.Tests/Services/DirectoryServiceTests.cs +++ b/API.Tests/Services/DirectoryServiceTests.cs @@ -593,6 +593,23 @@ public class DirectoryServiceTests || outputFiles.Contains(API.Services.Tasks.Scanner.Parser.Parser.NormalizePath("C:/manga/output/file (3).zip"))); } + [Fact] + public void CopyFilesToDirectory_ShouldRenameFilesToPassedNames() + { + const string testDirectory = "/manga/"; + var fileSystem = new MockFileSystem(); + fileSystem.AddFile(MockUnixSupport.Path($"{testDirectory}file.zip"), new MockFileData("")); + + var ds = new DirectoryService(Substitute.For>(), fileSystem); + ds.CopyFilesToDirectory(new []{MockUnixSupport.Path($"{testDirectory}file.zip")}, "/manga/output/", new [] {"01"}); + var outputFiles = ds.GetFiles("/manga/output/").Select(API.Services.Tasks.Scanner.Parser.Parser.NormalizePath).ToList(); + Assert.Equal(1, outputFiles.Count()); // we have 2 already there and 2 copies + // For some reason, this has C:/ on directory even though everything is emulated (System.IO.Abstractions issue, not changing) + // https://github.com/TestableIO/System.IO.Abstractions/issues/831 + Assert.True(outputFiles.Contains(API.Services.Tasks.Scanner.Parser.Parser.NormalizePath("/manga/output/01.zip")) + || outputFiles.Contains(API.Services.Tasks.Scanner.Parser.Parser.NormalizePath("C:/manga/output/01.zip"))); + } + #endregion #region ListDirectory diff --git a/API/Controllers/ReaderController.cs b/API/Controllers/ReaderController.cs index db5db71bb..73631e67c 100644 --- a/API/Controllers/ReaderController.cs +++ b/API/Controllers/ReaderController.cs @@ -83,8 +83,9 @@ public class ReaderController : BaseApiController } /// - /// Returns an image for a given chapter. Side effect: This will cache the chapter images for reading. + /// Returns an image for a given chapter. Will perform bounding checks /// + /// This will cache the chapter images for reading /// /// /// @@ -99,6 +100,7 @@ public class ReaderController : BaseApiController try { + // TODO: This code is very generic and repeated, see if we can refactor into a common method var path = _cacheService.GetCachedPagePath(chapter, page); if (string.IsNullOrEmpty(path) || !System.IO.File.Exists(path)) return BadRequest($"No such image for page {page}. Try refreshing to allow re-cache."); var format = Path.GetExtension(path).Replace(".", ""); @@ -128,7 +130,6 @@ public class ReaderController : BaseApiController if (page < 0) page = 0; var userId = await _unitOfWork.UserRepository.GetUserIdByApiKeyAsync(apiKey); - // NOTE: I'm not sure why I need this flow here var totalPages = await _cacheService.CacheBookmarkForSeries(userId, seriesId); if (page > totalPages) { @@ -139,7 +140,7 @@ public class ReaderController : BaseApiController { var path = _cacheService.GetCachedBookmarkPagePath(seriesId, page); if (string.IsNullOrEmpty(path) || !System.IO.File.Exists(path)) return BadRequest($"No such image for page {page}"); - var format = Path.GetExtension(path).Replace(".", ""); + var format = Path.GetExtension(path).Replace(".", string.Empty); return PhysicalFile(path, "image/" + format, Path.GetFileName(path)); } diff --git a/API/Controllers/ReadingListController.cs b/API/Controllers/ReadingListController.cs index 1428e81f9..b6ee2724d 100644 --- a/API/Controllers/ReadingListController.cs +++ b/API/Controllers/ReadingListController.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using API.Comparators; diff --git a/API/DTOs/ReadingLists/UpdateReadingListDto.cs b/API/DTOs/ReadingLists/UpdateReadingListDto.cs index b61ab2a72..6be7b8f69 100644 --- a/API/DTOs/ReadingLists/UpdateReadingListDto.cs +++ b/API/DTOs/ReadingLists/UpdateReadingListDto.cs @@ -1,10 +1,14 @@ -namespace API.DTOs.ReadingLists; +using System; +using System.ComponentModel.DataAnnotations; + +namespace API.DTOs.ReadingLists; public class UpdateReadingListDto { + [Required] public int ReadingListId { get; set; } - public string Title { get; set; } - public string Summary { get; set; } + public string Title { get; set; } = string.Empty; + public string Summary { get; set; } = string.Empty; public bool Promoted { get; set; } public bool CoverImageLocked { get; set; } } diff --git a/API/Data/Repositories/UserRepository.cs b/API/Data/Repositories/UserRepository.cs index c7115081b..904cc64b1 100644 --- a/API/Data/Repositories/UserRepository.cs +++ b/API/Data/Repositories/UserRepository.cs @@ -305,7 +305,7 @@ public class UserRepository : IUserRepository { return await _context.AppUserBookmark .Where(x => x.AppUserId == userId && x.SeriesId == seriesId) - .OrderBy(x => x.Page) + .OrderBy(x => x.Created) .AsNoTracking() .ProjectTo(_mapper.ConfigurationProvider) .ToListAsync(); @@ -315,7 +315,7 @@ public class UserRepository : IUserRepository { return await _context.AppUserBookmark .Where(x => x.AppUserId == userId && x.VolumeId == volumeId) - .OrderBy(x => x.Page) + .OrderBy(x => x.Created) .AsNoTracking() .ProjectTo(_mapper.ConfigurationProvider) .ToListAsync(); @@ -325,7 +325,7 @@ public class UserRepository : IUserRepository { return await _context.AppUserBookmark .Where(x => x.AppUserId == userId && x.ChapterId == chapterId) - .OrderBy(x => x.Page) + .OrderBy(x => x.Created) .AsNoTracking() .ProjectTo(_mapper.ConfigurationProvider) .ToListAsync(); @@ -341,25 +341,27 @@ public class UserRepository : IUserRepository { var query = _context.AppUserBookmark .Where(x => x.AppUserId == userId) - .OrderBy(x => x.Page) + .OrderBy(x => x.Created) .AsNoTracking(); - if (!string.IsNullOrEmpty(filter.SeriesNameQuery)) - { - var seriesNameQueryNormalized = Services.Tasks.Scanner.Parser.Parser.Normalize(filter.SeriesNameQuery); - var filterSeriesQuery = query.Join(_context.Series, b => b.SeriesId, s => s.Id, (bookmark, series) => new - { - bookmark, - series - }) - .Where(o => EF.Functions.Like(o.series.Name, $"%{filter.SeriesNameQuery}%") - || EF.Functions.Like(o.series.OriginalName, $"%{filter.SeriesNameQuery}%") - || EF.Functions.Like(o.series.LocalizedName, $"%{filter.SeriesNameQuery}%") - || EF.Functions.Like(o.series.NormalizedName, $"%{seriesNameQueryNormalized}%") - ); + if (string.IsNullOrEmpty(filter.SeriesNameQuery)) + return await query + .ProjectTo(_mapper.ConfigurationProvider) + .ToListAsync(); - query = filterSeriesQuery.Select(o => o.bookmark); - } + var seriesNameQueryNormalized = Services.Tasks.Scanner.Parser.Parser.Normalize(filter.SeriesNameQuery); + var filterSeriesQuery = query.Join(_context.Series, b => b.SeriesId, s => s.Id, (bookmark, series) => new + { + bookmark, + series + }) + .Where(o => EF.Functions.Like(o.series.Name, $"%{filter.SeriesNameQuery}%") + || EF.Functions.Like(o.series.OriginalName, $"%{filter.SeriesNameQuery}%") + || EF.Functions.Like(o.series.LocalizedName, $"%{filter.SeriesNameQuery}%") + || EF.Functions.Like(o.series.NormalizedName, $"%{seriesNameQueryNormalized}%") + ); + + query = filterSeriesQuery.Select(o => o.bookmark); return await query diff --git a/API/Data/Seed.cs b/API/Data/Seed.cs index 15e68abeb..61f3b086d 100644 --- a/API/Data/Seed.cs +++ b/API/Data/Seed.cs @@ -79,10 +79,7 @@ public static class Seed { new() {Key = ServerSettingKey.CacheDirectory, Value = directoryService.CacheDirectory}, new() {Key = ServerSettingKey.TaskScan, Value = "daily"}, - new() - { - Key = ServerSettingKey.LoggingLevel, Value = "Information" - }, // Not used from DB, but DB is sync with appSettings.json + new() {Key = ServerSettingKey.LoggingLevel, Value = "Debug"}, new() {Key = ServerSettingKey.TaskBackup, Value = "daily"}, new() { diff --git a/API/Services/CacheService.cs b/API/Services/CacheService.cs index a150bde22..7cf00bf57 100644 --- a/API/Services/CacheService.cs +++ b/API/Services/CacheService.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading.Tasks; @@ -101,7 +102,7 @@ public class CacheService : ICacheService var extractPath = GetCachePath(chapterId); if (_directoryService.Exists(extractPath)) return chapter; - var files = chapter.Files.ToList(); + var files = chapter?.Files.ToList(); ExtractChapterFiles(extractPath, files); return chapter; @@ -223,6 +224,8 @@ public class CacheService : ICacheService return string.Empty; } + if (page > files.Length) page = files.Length; + // Since array is 0 based, we need to keep that in account (only affects last image) return page == files.Length ? files.ElementAt(page - 1) : files.ElementAt(page); } @@ -234,8 +237,8 @@ public class CacheService : ICacheService var bookmarkDtos = await _unitOfWork.UserRepository.GetBookmarkDtosForSeries(userId, seriesId); var files = (await _bookmarkService.GetBookmarkFilesById(bookmarkDtos.Select(b => b.Id))).ToList(); - _directoryService.CopyFilesToDirectory(files, destDirectory); - _directoryService.Flatten(destDirectory); + _directoryService.CopyFilesToDirectory(files, destDirectory, + Enumerable.Range(1, files.Count).Select(i => i + string.Empty).ToList()); return files.Count; } diff --git a/API/Services/DirectoryService.cs b/API/Services/DirectoryService.cs index dbf7214cb..25119dbe0 100644 --- a/API/Services/DirectoryService.cs +++ b/API/Services/DirectoryService.cs @@ -37,6 +37,7 @@ public interface IDirectoryService IEnumerable ListDirectory(string rootPath); Task ReadFileAsync(string path); bool CopyFilesToDirectory(IEnumerable filePaths, string directoryPath, string prepend = ""); + bool CopyFilesToDirectory(IEnumerable filePaths, string directoryPath, IList newFilenames); bool Exists(string directory); void CopyFileToDirectory(string fullFilePath, string targetDirectory); int TraverseTreeParallelForEach(string root, Action action, string searchPattern, ILogger logger); @@ -424,6 +425,46 @@ public class DirectoryService : IDirectoryService return true; } + /// + /// Copies files to a destination directory. If the destination directory doesn't exist, this will create it. + /// + /// If a file already exists in dest, this will rename as (2). It does not support multiple iterations of this. Overwriting is not supported. + /// + /// + /// A list that matches one to one with filePaths. Each filepath will be renamed to newFilenames + /// + public bool CopyFilesToDirectory(IEnumerable filePaths, string directoryPath, IList newFilenames) + { + ExistOrCreate(directoryPath); + string currentFile = null; + var index = 0; + try + { + foreach (var file in filePaths) + { + currentFile = file; + + if (!FileSystem.File.Exists(file)) + { + _logger.LogError("Unable to copy {File} to {DirectoryPath} as it doesn't exist", file, directoryPath); + continue; + } + var fileInfo = FileSystem.FileInfo.FromFileName(file); + var targetFile = FileSystem.FileInfo.FromFileName(RenameFileForCopy(newFilenames[index] + fileInfo.Extension, directoryPath)); + + fileInfo.CopyTo(FileSystem.Path.Join(directoryPath, targetFile.Name)); + index++; + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Unable to copy {File} to {DirectoryPath}", currentFile, directoryPath); + return false; + } + + return true; + } + /// /// Generates the combined filepath given a prepend (optional), output directory path, and a full input file path. /// If the output file already exists, will append (1), (2), etc until it can be written out @@ -434,30 +475,32 @@ public class DirectoryService : IDirectoryService /// private string RenameFileForCopy(string fileToCopy, string directoryPath, string prepend = "") { - var fileInfo = FileSystem.FileInfo.FromFileName(fileToCopy); - var filename = prepend + fileInfo.Name; - - var targetFile = FileSystem.FileInfo.FromFileName(FileSystem.Path.Join(directoryPath, filename)); - if (!targetFile.Exists) + while (true) { - return targetFile.FullName; - } + var fileInfo = FileSystem.FileInfo.FromFileName(fileToCopy); + var filename = prepend + fileInfo.Name; - var noExtension = FileSystem.Path.GetFileNameWithoutExtension(fileInfo.Name); - if (FileCopyAppend.IsMatch(noExtension)) - { - var match = FileCopyAppend.Match(noExtension).Value; - var matchNumber = match.Replace("(", string.Empty).Replace(")", string.Empty); - noExtension = noExtension.Replace(match, $"({int.Parse(matchNumber) + 1})"); - } - else - { - noExtension += " (1)"; - } + var targetFile = FileSystem.FileInfo.FromFileName(FileSystem.Path.Join(directoryPath, filename)); + if (!targetFile.Exists) + { + return targetFile.FullName; + } - var newFilename = prepend + noExtension + - FileSystem.Path.GetExtension(fileInfo.Name); - return RenameFileForCopy(FileSystem.Path.Join(directoryPath, newFilename), directoryPath, prepend); + var noExtension = FileSystem.Path.GetFileNameWithoutExtension(fileInfo.Name); + if (FileCopyAppend.IsMatch(noExtension)) + { + var match = FileCopyAppend.Match(noExtension).Value; + var matchNumber = match.Replace("(", string.Empty).Replace(")", string.Empty); + noExtension = noExtension.Replace(match, $"({int.Parse(matchNumber) + 1})"); + } + else + { + noExtension += " (1)"; + } + + var newFilename = prepend + noExtension + FileSystem.Path.GetExtension(fileInfo.Name); + fileToCopy = FileSystem.Path.Join(directoryPath, newFilename); + } } /// diff --git a/UI/Web/package-lock.json b/UI/Web/package-lock.json index 1e6b9b82b..aba7c9f7b 100644 --- a/UI/Web/package-lock.json +++ b/UI/Web/package-lock.json @@ -12469,9 +12469,9 @@ "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==" }, "minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "requires": { "brace-expansion": "^1.1.7" } @@ -15417,9 +15417,9 @@ "dev": true }, "swiper": { - "version": "8.0.6", - "resolved": "https://registry.npmjs.org/swiper/-/swiper-8.0.6.tgz", - "integrity": "sha512-Ssyu1+FeNATF/G8e84QG+ZUNtUOAZ5vngdgxzczh0oWZPhGUVgkdv+BoePUuaCXLAFXnwVpNjgLIcGnxMdmWPA==", + "version": "8.4.4", + "resolved": "https://registry.npmjs.org/swiper/-/swiper-8.4.4.tgz", + "integrity": "sha512-jA/8BfOZwT8PqPSnMX0TENZYitXEhNa7ZSNj1Diqh5LZyUJoBQaZcqAiPQ/PIg1+IPaRn/V8ZYVb0nxHMh51yw==", "requires": { "dom7": "^4.0.4", "ssr-window": "^4.0.2" diff --git a/UI/Web/package.json b/UI/Web/package.json index c78cdc29e..7df5cdf86 100644 --- a/UI/Web/package.json +++ b/UI/Web/package.json @@ -44,7 +44,7 @@ "ngx-toastr": "^14.2.1", "requires": "^1.0.2", "rxjs": "~7.5.4", - "swiper": "^8.0.6", + "swiper": "^8.4.4", "tslib": "^2.3.1", "webpack-bundle-analyzer": "^4.5.0", "zone.js": "~0.11.4" diff --git a/UI/Web/src/app/_services/jumpbar.service.ts b/UI/Web/src/app/_services/jumpbar.service.ts index 43dc5ed3d..7c9bf8478 100644 --- a/UI/Web/src/app/_services/jumpbar.service.ts +++ b/UI/Web/src/app/_services/jumpbar.service.ts @@ -36,15 +36,15 @@ export class JumpbarService { const removalTimes = Math.ceil(removeCount / 2); const midPoint = Math.floor(jumpBarKeys.length / 2); jumpBarKeysToRender.push(jumpBarKeys[0]); - this.removeFirstPartOfJumpBar(midPoint, removalTimes, jumpBarKeys, jumpBarKeysToRender); + this._removeFirstPartOfJumpBar(midPoint, removalTimes, jumpBarKeys, jumpBarKeysToRender); jumpBarKeysToRender.push(jumpBarKeys[midPoint]); - this.removeSecondPartOfJumpBar(midPoint, removalTimes, jumpBarKeys, jumpBarKeysToRender); + this._removeSecondPartOfJumpBar(midPoint, removalTimes, jumpBarKeys, jumpBarKeysToRender); jumpBarKeysToRender.push(jumpBarKeys[jumpBarKeys.length - 1]); return jumpBarKeysToRender; } - removeSecondPartOfJumpBar(midPoint: number, numberOfRemovals: number = 1, jumpBarKeys: Array, jumpBarKeysToRender: Array) { + _removeSecondPartOfJumpBar(midPoint: number, numberOfRemovals: number = 1, jumpBarKeys: Array, jumpBarKeysToRender: Array) { const removedIndexes: Array = []; for(let removal = 0; removal < numberOfRemovals; removal++) { let min = 100000000; @@ -62,7 +62,7 @@ export class JumpbarService { } } - removeFirstPartOfJumpBar(midPoint: number, numberOfRemovals: number = 1, jumpBarKeys: Array, jumpBarKeysToRender: Array) { + _removeFirstPartOfJumpBar(midPoint: number, numberOfRemovals: number = 1, jumpBarKeys: Array, jumpBarKeysToRender: Array) { const removedIndexes: Array = []; for(let removal = 0; removal < numberOfRemovals; removal++) { let min = 100000000; @@ -80,4 +80,35 @@ export class JumpbarService { if (!removedIndexes.includes(i)) jumpBarKeysToRender.push(jumpBarKeys[i]); } } + + /** + * + * @param data An array of objects + * @param keySelector A method to fetch a string from the object, which is used to classify the JumpKey + * @returns + */ + getJumpKeys(data :Array, keySelector: (data: any) => string) { + const keys: {[key: string]: number} = {}; + data.forEach(obj => { + let ch = keySelector(obj).charAt(0); + if (/\d|\#|!|%|@|\(|\)|\^|\.|_|\*/g.test(ch)) { + ch = '#'; + } + if (!keys.hasOwnProperty(ch)) { + keys[ch] = 0; + } + keys[ch] += 1; + }); + return Object.keys(keys).map(k => { + return { + key: k, + size: keys[k], + title: k.toUpperCase() + } + }).sort((a, b) => { + if (a.key < b.key) return -1; + if (a.key > b.key) return 1; + return 0; + }); + } } diff --git a/UI/Web/src/app/bookmark/bookmarks/bookmarks.component.html b/UI/Web/src/app/bookmark/bookmarks/bookmarks.component.html index a43e90936..78dbf6dc2 100644 --- a/UI/Web/src/app/bookmark/bookmarks/bookmarks.component.html +++ b/UI/Web/src/app/bookmark/bookmarks/bookmarks.component.html @@ -11,6 +11,7 @@ [filterSettings]="filterSettings" [trackByIdentity]="trackByIdentity" [refresh]="refresh" + [jumpBarKeys]="jumpbarKeys" (applyFilter)="updateFilter($event)" > diff --git a/UI/Web/src/app/bookmark/bookmarks/bookmarks.component.ts b/UI/Web/src/app/bookmark/bookmarks/bookmarks.component.ts index 42c4cfaee..e37c96aea 100644 --- a/UI/Web/src/app/bookmark/bookmarks/bookmarks.component.ts +++ b/UI/Web/src/app/bookmark/bookmarks/bookmarks.component.ts @@ -8,12 +8,14 @@ import { ConfirmService } from 'src/app/shared/confirm.service'; import { DownloadService } from 'src/app/shared/_services/download.service'; import { FilterUtilitiesService } from 'src/app/shared/_services/filter-utilities.service'; import { KEY_CODES } from 'src/app/shared/_services/utility.service'; +import { JumpKey } from 'src/app/_models/jumpbar/jump-key'; import { PageBookmark } from 'src/app/_models/page-bookmark'; import { Pagination } from 'src/app/_models/pagination'; import { Series } from 'src/app/_models/series'; import { FilterEvent, SeriesFilter } from 'src/app/_models/series-filter'; import { Action, ActionFactoryService, ActionItem } from 'src/app/_services/action-factory.service'; import { ImageService } from 'src/app/_services/image.service'; +import { JumpbarService } from 'src/app/_services/jumpbar.service'; import { ReaderService } from 'src/app/_services/reader.service'; import { SeriesService } from 'src/app/_services/series.service'; @@ -32,6 +34,7 @@ export class BookmarksComponent implements OnInit, OnDestroy { downloadingSeries: {[id: number]: boolean} = {}; clearingSeries: {[id: number]: boolean} = {}; actions: ActionItem[] = []; + jumpbarKeys: Array = []; pagination!: Pagination; filter: SeriesFilter | undefined = undefined; @@ -50,7 +53,8 @@ export class BookmarksComponent implements OnInit, OnDestroy { private confirmService: ConfirmService, public bulkSelectionService: BulkSelectionService, public imageService: ImageService, private actionFactoryService: ActionFactoryService, private router: Router, private readonly cdRef: ChangeDetectorRef, - private filterUtilityService: FilterUtilitiesService, private route: ActivatedRoute) { + private filterUtilityService: FilterUtilitiesService, private route: ActivatedRoute, + private jumpbarService: JumpbarService) { this.filterSettings.ageRatingDisabled = true; this.filterSettings.collectionDisabled = true; this.filterSettings.formatDisabled = true; @@ -158,6 +162,7 @@ export class BookmarksComponent implements OnInit, OnDestroy { const ids = Object.keys(this.seriesIds).map(k => parseInt(k, 10)); this.seriesService.getAllSeriesByIds(ids).subscribe(series => { + this.jumpbarKeys = this.jumpbarService.getJumpKeys(series, (t: Series) => t.name); this.series = series; this.loadingBookmarks = false; this.cdRef.markForCheck(); diff --git a/UI/Web/src/app/collections/all-collections/all-collections.component.ts b/UI/Web/src/app/collections/all-collections/all-collections.component.ts index 02588d01a..7c5dad327 100644 --- a/UI/Web/src/app/collections/all-collections/all-collections.component.ts +++ b/UI/Web/src/app/collections/all-collections/all-collections.component.ts @@ -3,13 +3,13 @@ import { Title } from '@angular/platform-browser'; import { Router } from '@angular/router'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; import { EditCollectionTagsComponent } from 'src/app/cards/_modals/edit-collection-tags/edit-collection-tags.component'; -import { UtilityService } from 'src/app/shared/_services/utility.service'; import { CollectionTag } from 'src/app/_models/collection-tag'; import { JumpKey } from 'src/app/_models/jumpbar/jump-key'; import { Tag } from 'src/app/_models/tag'; import { ActionItem, ActionFactoryService, Action } from 'src/app/_services/action-factory.service'; import { CollectionTagService } from 'src/app/_services/collection-tag.service'; import { ImageService } from 'src/app/_services/image.service'; +import { JumpbarService } from 'src/app/_services/jumpbar.service'; @Component({ @@ -30,7 +30,7 @@ export class AllCollectionsComponent implements OnInit { constructor(private collectionService: CollectionTagService, private router: Router, private actionFactoryService: ActionFactoryService, private modalService: NgbModal, - private titleService: Title, private utilityService: UtilityService, + private titleService: Title, private jumpbarService: JumpbarService, private readonly cdRef: ChangeDetectorRef, public imageSerivce: ImageService) { this.router.routeReuseStrategy.shouldReuseRoute = () => false; this.titleService.setTitle('Kavita - Collections'); @@ -54,7 +54,7 @@ export class AllCollectionsComponent implements OnInit { this.collectionService.allTags().subscribe(tags => { this.collections = tags; this.isLoading = false; - this.jumpbarKeys = this.utilityService.getJumpKeys(tags, (t: Tag) => t.title); + this.jumpbarKeys = this.jumpbarService.getJumpKeys(tags, (t: Tag) => t.title); this.cdRef.markForCheck(); }); } diff --git a/UI/Web/src/app/collections/collection-detail/collection-detail.component.ts b/UI/Web/src/app/collections/collection-detail/collection-detail.component.ts index b105f7007..6627c6d50 100644 --- a/UI/Web/src/app/collections/collection-detail/collection-detail.component.ts +++ b/UI/Web/src/app/collections/collection-detail/collection-detail.component.ts @@ -21,6 +21,7 @@ import { Action, ActionFactoryService, ActionItem } from 'src/app/_services/acti import { ActionService } from 'src/app/_services/action.service'; import { CollectionTagService } from 'src/app/_services/collection-tag.service'; import { ImageService } from 'src/app/_services/image.service'; +import { JumpbarService } from 'src/app/_services/jumpbar.service'; import { EVENTS, MessageHubService } from 'src/app/_services/message-hub.service'; import { ScrollService } from 'src/app/_services/scroll.service'; import { SeriesService } from 'src/app/_services/series.service'; @@ -124,7 +125,7 @@ export class CollectionDetailComponent implements OnInit, OnDestroy, AfterConten constructor(public imageService: ImageService, private collectionService: CollectionTagService, private router: Router, private route: ActivatedRoute, private seriesService: SeriesService, private toastr: ToastrService, private actionFactoryService: ActionFactoryService, - private modalService: NgbModal, private titleService: Title, + private modalService: NgbModal, private titleService: Title, private jumpbarService: JumpbarService, public bulkSelectionService: BulkSelectionService, private actionService: ActionService, private messageHub: MessageHubService, private filterUtilityService: FilterUtilitiesService, private utilityService: UtilityService, @Inject(DOCUMENT) private document: Document, private readonly cdRef: ChangeDetectorRef, private scrollService: ScrollService) { @@ -210,7 +211,7 @@ export class CollectionDetailComponent implements OnInit, OnDestroy, AfterConten this.seriesService.getAllSeries(undefined, undefined, this.filter).pipe(take(1)).subscribe(series => { this.series = series.result; this.seriesPagination = series.pagination; - this.jumpbarKeys = this.utilityService.getJumpKeys(this.series, (series: Series) => series.name); + this.jumpbarKeys = this.jumpbarService.getJumpKeys(this.series, (series: Series) => series.name); this.isLoading = false; window.scrollTo(0, 0); this.cdRef.markForCheck(); diff --git a/UI/Web/src/app/manga-reader/fullscreen-icon.pipe.ts b/UI/Web/src/app/manga-reader/_pipes/fullscreen-icon.pipe.ts similarity index 100% rename from UI/Web/src/app/manga-reader/fullscreen-icon.pipe.ts rename to UI/Web/src/app/manga-reader/_pipes/fullscreen-icon.pipe.ts diff --git a/UI/Web/src/app/manga-reader/_pipes/layout-mode-icon.pipe.ts b/UI/Web/src/app/manga-reader/_pipes/layout-mode-icon.pipe.ts new file mode 100644 index 000000000..26f43dfee --- /dev/null +++ b/UI/Web/src/app/manga-reader/_pipes/layout-mode-icon.pipe.ts @@ -0,0 +1,20 @@ +import { Pipe, PipeTransform } from '@angular/core'; +import { LayoutMode } from '../_models/layout-mode'; + +@Pipe({ + name: 'layoutModeIcon' +}) +export class LayoutModeIconPipe implements PipeTransform { + + transform(layoutMode: LayoutMode): string { + switch (layoutMode) { + case LayoutMode.Single: + return 'none'; + case LayoutMode.Double: + return 'double'; + case LayoutMode.DoubleReversed: + return 'double-reversed'; + } + } + +} diff --git a/UI/Web/src/app/manga-reader/_pipes/reader-mode-icon.pipe.ts b/UI/Web/src/app/manga-reader/_pipes/reader-mode-icon.pipe.ts new file mode 100644 index 000000000..5017ad755 --- /dev/null +++ b/UI/Web/src/app/manga-reader/_pipes/reader-mode-icon.pipe.ts @@ -0,0 +1,22 @@ +import { Pipe, PipeTransform } from '@angular/core'; +import { ReaderMode } from 'src/app/_models/preferences/reader-mode'; + +@Pipe({ + name: 'readerModeIcon' +}) +export class ReaderModeIconPipe implements PipeTransform { + + transform(readerMode: ReaderMode): string { + switch(readerMode) { + case ReaderMode.LeftRight: + return 'fa-exchange-alt'; + case ReaderMode.UpDown: + return 'fa-exchange-alt fa-rotate-90'; + case ReaderMode.Webtoon: + return 'fa-arrows-alt-v'; + default: + return ''; + } + } + +} diff --git a/UI/Web/src/app/manga-reader/manga-reader.component.html b/UI/Web/src/app/manga-reader/manga-reader.component.html index 21c28f7fd..aca92651b 100644 --- a/UI/Web/src/app/manga-reader/manga-reader.component.html +++ b/UI/Web/src/app/manga-reader/manga-reader.component.html @@ -121,7 +121,7 @@
@@ -197,7 +197,6 @@ -