diff --git a/API.Tests/Services/BookmarkServiceTests.cs b/API.Tests/Services/BookmarkServiceTests.cs index 0026ea678..13acf3684 100644 --- a/API.Tests/Services/BookmarkServiceTests.cs +++ b/API.Tests/Services/BookmarkServiceTests.cs @@ -389,7 +389,7 @@ public class BookmarkServiceTests VolumeId = 1 }, $"{CacheDirectory}1/0001.jpg"); - var files = await bookmarkService.GetBookmarkFilesById(1, new[] {1}); + var files = await bookmarkService.GetBookmarkFilesById(new[] {1}); var actualFiles = ds.GetFiles(BookmarkDirectory, searchOption: SearchOption.AllDirectories); Assert.Equal(files.Select(API.Parser.Parser.NormalizePath).ToList(), actualFiles.Select(API.Parser.Parser.NormalizePath).ToList()); } diff --git a/API.Tests/Services/CacheServiceTests.cs b/API.Tests/Services/CacheServiceTests.cs index 7a2cbada8..d5a8d4bee 100644 --- a/API.Tests/Services/CacheServiceTests.cs +++ b/API.Tests/Services/CacheServiceTests.cs @@ -157,7 +157,8 @@ namespace API.Tests.Services filesystem.AddDirectory($"{CacheDirectory}1/"); var ds = new DirectoryService(Substitute.For>(), filesystem); var cleanupService = new CacheService(_logger, _unitOfWork, ds, - new ReadingItemService(Substitute.For(), Substitute.For(), Substitute.For(), ds)); + new ReadingItemService(Substitute.For(), + Substitute.For(), Substitute.For(), ds), Substitute.For()); await ResetDB(); var s = DbFactory.Series("Test"); @@ -240,7 +241,8 @@ namespace API.Tests.Services filesystem.AddFile($"{CacheDirectory}3/003.jpg", new MockFileData("")); var ds = new DirectoryService(Substitute.For>(), filesystem); var cleanupService = new CacheService(_logger, _unitOfWork, ds, - new ReadingItemService(Substitute.For(), Substitute.For(), Substitute.For(), ds)); + new ReadingItemService(Substitute.For(), + Substitute.For(), Substitute.For(), ds), Substitute.For()); cleanupService.CleanupChapters(new []{1, 3}); Assert.Empty(ds.GetFiles(CacheDirectory, searchOption:SearchOption.AllDirectories)); @@ -260,7 +262,8 @@ namespace API.Tests.Services filesystem.AddFile($"{DataDirectory}2.epub", new MockFileData("")); var ds = new DirectoryService(Substitute.For>(), filesystem); var cs = new CacheService(_logger, _unitOfWork, ds, - new ReadingItemService(Substitute.For(), Substitute.For(), Substitute.For(), ds)); + new ReadingItemService(Substitute.For(), + Substitute.For(), Substitute.For(), ds), Substitute.For()); var c = new Chapter() { @@ -311,7 +314,8 @@ namespace API.Tests.Services var ds = new DirectoryService(Substitute.For>(), filesystem); var cs = new CacheService(_logger, _unitOfWork, ds, - new ReadingItemService(Substitute.For(), Substitute.For(), Substitute.For(), ds)); + new ReadingItemService(Substitute.For(), + Substitute.For(), Substitute.For(), ds), Substitute.For()); // Flatten to prepare for how GetFullPath expects ds.Flatten($"{CacheDirectory}1/"); @@ -362,7 +366,8 @@ namespace API.Tests.Services var ds = new DirectoryService(Substitute.For>(), filesystem); var cs = new CacheService(_logger, _unitOfWork, ds, - new ReadingItemService(Substitute.For(), Substitute.For(), Substitute.For(), ds)); + new ReadingItemService(Substitute.For(), + Substitute.For(), Substitute.For(), ds), Substitute.For()); // Flatten to prepare for how GetFullPath expects ds.Flatten($"{CacheDirectory}1/"); @@ -408,7 +413,8 @@ namespace API.Tests.Services var ds = new DirectoryService(Substitute.For>(), filesystem); var cs = new CacheService(_logger, _unitOfWork, ds, - new ReadingItemService(Substitute.For(), Substitute.For(), Substitute.For(), ds)); + new ReadingItemService(Substitute.For(), + Substitute.For(), Substitute.For(), ds), Substitute.For()); // Flatten to prepare for how GetFullPath expects ds.Flatten($"{CacheDirectory}1/"); @@ -460,7 +466,8 @@ namespace API.Tests.Services var ds = new DirectoryService(Substitute.For>(), filesystem); var cs = new CacheService(_logger, _unitOfWork, ds, - new ReadingItemService(Substitute.For(), Substitute.For(), Substitute.For(), ds)); + new ReadingItemService(Substitute.For(), + Substitute.For(), Substitute.For(), ds), Substitute.For()); // Flatten to prepare for how GetFullPath expects ds.Flatten($"{CacheDirectory}1/"); diff --git a/API/Controllers/DownloadController.cs b/API/Controllers/DownloadController.cs index b60fae6e8..433f16721 100644 --- a/API/Controllers/DownloadController.cs +++ b/API/Controllers/DownloadController.cs @@ -171,7 +171,7 @@ namespace API.Controllers var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername()); var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(downloadBookmarkDto.Bookmarks.First().SeriesId); - var files = await _bookmarkService.GetBookmarkFilesById(user.Id, downloadBookmarkDto.Bookmarks.Select(b => b.Id)); + var files = await _bookmarkService.GetBookmarkFilesById(downloadBookmarkDto.Bookmarks.Select(b => b.Id)); var filename = $"{series.Name} - Bookmarks.zip"; await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, diff --git a/API/Controllers/ReaderController.cs b/API/Controllers/ReaderController.cs index 04d7e59c3..26d48c96d 100644 --- a/API/Controllers/ReaderController.cs +++ b/API/Controllers/ReaderController.cs @@ -73,6 +73,41 @@ namespace API.Controllers } } + /// + /// Returns an image for a given bookmark series. Side effect: This will cache the bookmark images for reading. + /// + /// + /// Api key for the user the bookmarks are on + /// + /// We must use api key as bookmarks could be leaked to other users via the API + /// + [HttpGet("bookmark-image")] + public async Task GetBookmarkImage(int seriesId, string apiKey, int page) + { + if (page < 0) page = 0; + var userId = await _unitOfWork.UserRepository.GetUserIdByApiKeyAsync(apiKey); + var totalPages = await _cacheService.CacheBookmarkForSeries(userId, seriesId); + if (page > totalPages) + { + page = totalPages; + } + + try + { + 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(".", ""); + + Response.AddCacheHeader(path, TimeSpan.FromMinutes(10).Seconds); + return PhysicalFile(path, "image/" + format, Path.GetFileName(path)); + } + catch (Exception) + { + _cacheService.CleanupBookmarks(new []{ seriesId }); + throw; + } + } + /// /// Returns various information about a Chapter. Side effect: This will cache the chapter images for reading. /// @@ -104,6 +139,29 @@ namespace API.Controllers }); } + /// + /// Returns various information about all bookmark files for a Series. Side effect: This will cache the bookmark images for reading. + /// + /// Series Id for all bookmarks + /// + [HttpGet("bookmark-info")] + public async Task> GetBookmarkInfo(int seriesId) + { + var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername()); + var totalPages = await _cacheService.CacheBookmarkForSeries(user.Id, seriesId); + // TODO: Change Includes to None from LinkedSeries branch + var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId); + + return Ok(new BookmarkInfoDto() + { + SeriesName = series.Name, + SeriesFormat = series.Format, + SeriesId = series.Id, + LibraryId = series.LibraryId, + Pages = totalPages, + }); + } + [HttpPost("mark-read")] public async Task MarkRead(MarkReadDto markReadDto) diff --git a/API/DTOs/Reader/BookmarkInfoDto.cs b/API/DTOs/Reader/BookmarkInfoDto.cs new file mode 100644 index 000000000..a34eb81c2 --- /dev/null +++ b/API/DTOs/Reader/BookmarkInfoDto.cs @@ -0,0 +1,13 @@ +using API.Entities.Enums; + +namespace API.DTOs.Reader; + +public class BookmarkInfoDto +{ + public string SeriesName { get; set; } + public MangaFormat SeriesFormat { get; set; } + public int SeriesId { get; set; } + public int LibraryId { get; set; } + public LibraryType LibraryType { get; set; } + public int Pages { get; set; } +} diff --git a/API/Services/BookmarkService.cs b/API/Services/BookmarkService.cs index 2f4cd8cdc..4468a79a1 100644 --- a/API/Services/BookmarkService.cs +++ b/API/Services/BookmarkService.cs @@ -17,7 +17,8 @@ public interface IBookmarkService Task DeleteBookmarkFiles(IEnumerable bookmarks); Task BookmarkPage(AppUser userWithBookmarks, BookmarkDto bookmarkDto, string imageToBookmark); Task RemoveBookmarkPage(AppUser userWithBookmarks, BookmarkDto bookmarkDto); - Task> GetBookmarkFilesById(int userId, IEnumerable bookmarkIds); + Task> GetBookmarkFilesById(IEnumerable bookmarkIds); + } public class BookmarkService : IBookmarkService @@ -141,7 +142,7 @@ public class BookmarkService : IBookmarkService return true; } - public async Task> GetBookmarkFilesById(int userId, IEnumerable bookmarkIds) + public async Task> GetBookmarkFilesById(IEnumerable bookmarkIds) { var bookmarkDirectory = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.BookmarkDirectory)).Value; diff --git a/API/Services/CacheService.cs b/API/Services/CacheService.cs index ef2d3609a..5c37d431e 100644 --- a/API/Services/CacheService.cs +++ b/API/Services/CacheService.cs @@ -25,9 +25,12 @@ namespace API.Services /// /// Volumes that belong to that library. Assume the library might have been deleted before this invocation. void CleanupChapters(IEnumerable chapterIds); + void CleanupBookmarks(IEnumerable seriesIds); string GetCachedPagePath(Chapter chapter, int page); + string GetCachedBookmarkPagePath(int seriesId, int page); string GetCachedEpubFile(int chapterId, Chapter chapter); public void ExtractChapterFiles(string extractPath, IReadOnlyList files); + Task CacheBookmarkForSeries(int userId, int seriesId); } public class CacheService : ICacheService { @@ -35,16 +38,36 @@ namespace API.Services private readonly IUnitOfWork _unitOfWork; private readonly IDirectoryService _directoryService; private readonly IReadingItemService _readingItemService; - private readonly NumericComparer _numericComparer; + private readonly IBookmarkService _bookmarkService; public CacheService(ILogger logger, IUnitOfWork unitOfWork, - IDirectoryService directoryService, IReadingItemService readingItemService) + IDirectoryService directoryService, IReadingItemService readingItemService, + IBookmarkService bookmarkService) { _logger = logger; _unitOfWork = unitOfWork; _directoryService = directoryService; _readingItemService = readingItemService; - _numericComparer = new NumericComparer(); + _bookmarkService = bookmarkService; + } + + public string GetCachedBookmarkPagePath(int seriesId, int page) + { + // Calculate what chapter the page belongs to + var path = GetBookmarkCachePath(seriesId); + var files = _directoryService.GetFilesWithExtension(path, Parser.Parser.ImageFileExtensions); + files = files + .AsEnumerable() + .OrderByNatural(Path.GetFileNameWithoutExtension) + .ToArray(); + + if (files.Length == 0) + { + return string.Empty; + } + + // 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); } /// @@ -146,6 +169,18 @@ namespace API.Services } } + /// + /// Removes the cached files and folders for a set of chapterIds + /// + /// + public void CleanupBookmarks(IEnumerable seriesIds) + { + foreach (var series in seriesIds) + { + _directoryService.ClearAndDeleteDirectory(GetBookmarkCachePath(series)); + } + } + /// /// Returns the cache path for a given Chapter. Should be cacheDirectory/{chapterId}/ @@ -157,6 +192,11 @@ namespace API.Services return _directoryService.FileSystem.Path.GetFullPath(_directoryService.FileSystem.Path.Join(_directoryService.CacheDirectory, $"{chapterId}/")); } + private string GetBookmarkCachePath(int seriesId) + { + return _directoryService.FileSystem.Path.GetFullPath(_directoryService.FileSystem.Path.Join(_directoryService.CacheDirectory, $"{seriesId}_bookmarks/")); + } + /// /// Returns the absolute path of a cached page. /// @@ -181,5 +221,17 @@ namespace API.Services // 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); } + + public async Task CacheBookmarkForSeries(int userId, int seriesId) + { + var destDirectory = _directoryService.FileSystem.Path.Join(_directoryService.CacheDirectory, seriesId + "_bookmarks"); + if (_directoryService.Exists(destDirectory)) return _directoryService.GetFiles(destDirectory).Count(); + + 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); + return files.Count; + } } } diff --git a/UI/Web/src/app/_models/manga-reader/bookmark-info.ts b/UI/Web/src/app/_models/manga-reader/bookmark-info.ts new file mode 100644 index 000000000..e63c31390 --- /dev/null +++ b/UI/Web/src/app/_models/manga-reader/bookmark-info.ts @@ -0,0 +1,11 @@ +import { LibraryType } from "../library"; +import { MangaFormat } from "../manga-format"; + +export interface BookmarkInfo { + seriesName: string; + seriesFormat: MangaFormat; + seriesId: number; + libraryId: number; + libraryType: LibraryType; + pages: number; +} \ No newline at end of file diff --git a/UI/Web/src/app/_services/action-factory.service.ts b/UI/Web/src/app/_services/action-factory.service.ts index 6f47bd178..1294b781c 100644 --- a/UI/Web/src/app/_services/action-factory.service.ts +++ b/UI/Web/src/app/_services/action-factory.service.ts @@ -17,10 +17,17 @@ export enum Action { Info = 5, RefreshMetadata = 6, Download = 7, + /** + * @deprecated This is no longer supported. Use the dedicated page instead + */ Bookmarks = 8, IncognitoRead = 9, AddToReadingList = 10, - AddToCollection = 11 + AddToCollection = 11, + /** + * Essentially a download, but handled differently. Needed so card bubbles it up for handling + */ + DownloadBookmark = 12 } export interface ActionItem { @@ -47,6 +54,8 @@ export class ActionFactoryService { readingListActions: Array> = []; + bookmarkActions: Array> = []; + isAdmin = false; hasDownloadRole = false; @@ -181,6 +190,12 @@ export class ActionFactoryService { return actions; } + getBookmarkActions(callback: (action: Action, series: Series) => void) { + const actions = this.bookmarkActions.map(a => {return {...a}}); + actions.forEach(action => action.callback = callback); + return actions; + } + filterBookmarksForFormat(action: ActionItem, series: Series) { if (action.action === Action.Bookmarks && series?.format === MangaFormat.EPUB) return false; return true; @@ -206,12 +221,6 @@ export class ActionFactoryService { callback: this.dummyCallback, requiresAdmin: false }, - { - action: Action.Bookmarks, - title: 'Bookmarks', - callback: this.dummyCallback, - requiresAdmin: false - }, { action: Action.AddToReadingList, title: 'Add to Reading List', @@ -294,5 +303,20 @@ export class ActionFactoryService { requiresAdmin: false }, ]; + + this.bookmarkActions = [ + { + action: Action.DownloadBookmark, + title: 'Download', + callback: this.dummyCallback, + requiresAdmin: false + }, + { + action: Action.Delete, + title: 'Clear', + callback: this.dummyCallback, + requiresAdmin: false + }, + ] } } diff --git a/UI/Web/src/app/_services/action.service.ts b/UI/Web/src/app/_services/action.service.ts index 6d627ca6a..7f05a02d8 100644 --- a/UI/Web/src/app/_services/action.service.ts +++ b/UI/Web/src/app/_services/action.service.ts @@ -3,7 +3,6 @@ import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; import { ToastrService } from 'ngx-toastr'; import { Subject } from 'rxjs'; import { take } from 'rxjs/operators'; -import { BookmarksModalComponent } from '../cards/_modals/bookmarks-modal/bookmarks-modal.component'; import { BulkAddToCollectionComponent } from '../cards/_modals/bulk-add-to-collection/bulk-add-to-collection.component'; import { AddToListModalComponent, ADD_FLOW } from '../reading-list/_modals/add-to-list-modal/add-to-list-modal.component'; import { EditReadingListModalComponent } from '../reading-list/_modals/edit-reading-list-modal/edit-reading-list-modal.component'; @@ -302,25 +301,6 @@ export class ActionService implements OnDestroy { }); } - - openBookmarkModal(series: Series, callback?: SeriesActionCallback) { - if (this.bookmarkModalRef != null) { return; } - this.bookmarkModalRef = this.modalService.open(BookmarksModalComponent, { scrollable: true, size: 'lg' }); - this.bookmarkModalRef.componentInstance.series = series; - this.bookmarkModalRef.closed.pipe(take(1)).subscribe(() => { - this.bookmarkModalRef = null; - if (callback) { - callback(series); - } - }); - this.bookmarkModalRef.dismissed.pipe(take(1)).subscribe(() => { - this.bookmarkModalRef = null; - if (callback) { - callback(series); - } - }); - } - addMultipleToReadingList(seriesId: number, volumes: Array, chapters?: Array, callback?: VoidActionCallback) { if (this.readingListModalRef != null) { return; } this.readingListModalRef = this.modalService.open(AddToListModalComponent, { scrollable: true, size: 'md' }); diff --git a/UI/Web/src/app/_services/reader.service.ts b/UI/Web/src/app/_services/reader.service.ts index 33a788a88..b41efcb93 100644 --- a/UI/Web/src/app/_services/reader.service.ts +++ b/UI/Web/src/app/_services/reader.service.ts @@ -4,6 +4,7 @@ import { environment } from 'src/environments/environment'; import { ChapterInfo } from '../manga-reader/_models/chapter-info'; import { UtilityService } from '../shared/_services/utility.service'; import { Chapter } from '../_models/chapter'; +import { BookmarkInfo } from '../_models/manga-reader/bookmark-info'; import { PageBookmark } from '../_models/page-bookmark'; import { ProgressBookmark } from '../_models/progress-bookmark'; import { Volume } from '../_models/volume'; @@ -48,6 +49,14 @@ export class ReaderService { return this.httpClient.post(this.baseUrl + 'reader/remove-bookmarks', {seriesId}); } + /** + * Used exclusively for reading multiple bookmarks from a series + * @param seriesId + */ + getBookmarkInfo(seriesId: number) { + return this.httpClient.get(this.baseUrl + 'reader/bookmark-info?seriesId=' + seriesId); + } + getProgress(chapterId: number) { return this.httpClient.get(this.baseUrl + 'reader/get-progress?chapterId=' + chapterId); } @@ -56,6 +65,10 @@ export class ReaderService { return this.baseUrl + 'reader/image?chapterId=' + chapterId + '&page=' + page; } + getBookmarkPageUrl(seriesId: number, apiKey: string, page: number) { + return this.baseUrl + 'reader/bookmark-image?seriesId=' + seriesId + '&page=' + page + '&apiKey=' + encodeURIComponent(apiKey); + } + getChapterInfo(chapterId: number) { return this.httpClient.get(this.baseUrl + 'reader/chapter-info?chapterId=' + chapterId); } diff --git a/UI/Web/src/app/all-series/all-series.component.html b/UI/Web/src/app/all-series/all-series.component.html index edb13798b..198027462 100644 --- a/UI/Web/src/app/all-series/all-series.component.html +++ b/UI/Web/src/app/all-series/all-series.component.html @@ -1,6 +1,5 @@

- All Series

{{pagination?.totalItems}} Series
diff --git a/UI/Web/src/app/all-series/all-series.component.ts b/UI/Web/src/app/all-series/all-series.component.ts index 16aa80f92..7298ce71b 100644 --- a/UI/Web/src/app/all-series/all-series.component.ts +++ b/UI/Web/src/app/all-series/all-series.component.ts @@ -26,7 +26,6 @@ export class AllSeriesComponent implements OnInit, OnDestroy { series: Series[] = []; loadingSeries = false; pagination!: Pagination; - actions: ActionItem[] = []; filter: SeriesFilter | undefined = undefined; onDestroy: Subject = new Subject(); filterSettings: FilterSettings = new FilterSettings(); diff --git a/UI/Web/src/app/app-routing.module.ts b/UI/Web/src/app/app-routing.module.ts index 0ea1e0fa0..b0ca5be2e 100644 --- a/UI/Web/src/app/app-routing.module.ts +++ b/UI/Web/src/app/app-routing.module.ts @@ -2,7 +2,6 @@ import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; import { LibraryDetailComponent } from './library-detail/library-detail.component'; import { SeriesDetailComponent } from './series-detail/series-detail.component'; -import { RecentlyAddedComponent } from './recently-added/recently-added.component'; import { UserLoginComponent } from './user-login/user-login.component'; import { AuthGuard } from './_guards/auth.guard'; import { LibraryAccessGuard } from './_guards/library-access.guard'; @@ -10,8 +9,6 @@ import { DashboardComponent } from './dashboard/dashboard.component'; import { AllSeriesComponent } from './all-series/all-series.component'; import { AdminGuard } from './_guards/admin.guard'; import { ThemeTestComponent } from './theme-test/theme-test.component'; -import { ReadingListsComponent } from './reading-list/reading-lists/reading-lists.component'; -import { AllCollectionsComponent } from './collections/all-collections/all-collections.component'; // TODO: Once we modularize the components, use this and measure performance impact: https://angular.io/guide/lazy-loading-ngmodules#preloading-modules @@ -45,6 +42,10 @@ const routes: Routes = [ path: 'announcements', loadChildren: () => import('../app/announcements/announcements.module').then(m => m.AnnouncementsModule) }, + { + path: 'bookmarks', + loadChildren: () => import('../app/bookmark/bookmark.module').then(m => m.BookmarkModule) + }, { path: '', runGuardsAndResolvers: 'always', @@ -68,8 +69,7 @@ const routes: Routes = [ canActivate: [AuthGuard], children: [ {path: 'library', component: DashboardComponent}, - {path: 'recently-added', component: RecentlyAddedComponent}, - {path: 'all-series', component: AllSeriesComponent}, + {path: 'all-series', component: AllSeriesComponent}, // TODO: This might be better as a separate module ] }, diff --git a/UI/Web/src/app/app.module.ts b/UI/Web/src/app/app.module.ts index d790f027c..6d6d67ae4 100644 --- a/UI/Web/src/app/app.module.ts +++ b/UI/Web/src/app/app.module.ts @@ -1,6 +1,5 @@ import { BrowserModule, Title } from '@angular/platform-browser'; import { NgModule } from '@angular/core'; -import { APP_BASE_HREF } from '@angular/common'; import { AppRoutingModule } from './app-routing.module'; import { AppComponent } from './app.component'; @@ -22,7 +21,6 @@ import { ReviewSeriesModalComponent } from './_modals/review-series-modal/review import { CarouselModule } from './carousel/carousel.module'; import { TypeaheadModule } from './typeahead/typeahead.module'; -import { RecentlyAddedComponent } from './recently-added/recently-added.component'; import { DashboardComponent } from './dashboard/dashboard.component'; import { CardsModule } from './cards/cards.module'; import { CollectionsModule } from './collections/collections.module'; @@ -35,7 +33,6 @@ import { RegistrationModule } from './registration/registration.module'; import { GroupedTypeaheadComponent } from './grouped-typeahead/grouped-typeahead.component'; import { ThemeTestComponent } from './theme-test/theme-test.component'; import { PipeModule } from './pipe/pipe.module'; -import { ColorPickerModule } from 'ngx-color-picker'; import { SidenavModule } from './sidenav/sidenav.module'; @@ -48,7 +45,6 @@ import { SidenavModule } from './sidenav/sidenav.module'; LibraryDetailComponent, SeriesDetailComponent, ReviewSeriesModalComponent, - RecentlyAddedComponent, DashboardComponent, EventsWidgetComponent, SeriesMetadataDetailComponent, @@ -66,8 +62,9 @@ import { SidenavModule } from './sidenav/sidenav.module'; NgbDropdownModule, // Nav NgbPopoverModule, // Nav Events toggle - NgbRatingModule, // Series Detail & Filter NgbNavModule, + + NgbRatingModule, // Series Detail & Filter NgbPaginationModule, NgbCollapseModule, // Login @@ -80,8 +77,6 @@ import { SidenavModule } from './sidenav/sidenav.module'; ReadingListModule, RegistrationModule, - ColorPickerModule, // User preferences - NgbAccordionModule, // ThemeTest Component only PipeModule, diff --git a/UI/Web/src/app/bookmark/bookmark-routing.module.ts b/UI/Web/src/app/bookmark/bookmark-routing.module.ts new file mode 100644 index 000000000..66b13c29c --- /dev/null +++ b/UI/Web/src/app/bookmark/bookmark-routing.module.ts @@ -0,0 +1,22 @@ +import { NgModule } from "@angular/core"; +import { Routes, RouterModule } from "@angular/router"; +import { AuthGuard } from "../_guards/auth.guard"; +import { BookmarksComponent } from "./bookmarks/bookmarks.component"; + +const routes: Routes = [ + {path: '**', component: BookmarksComponent, pathMatch: 'full', canActivate: [AuthGuard]}, + { + runGuardsAndResolvers: 'always', + canActivate: [AuthGuard], + children: [ + {path: '/bookmarks', component: BookmarksComponent}, + ] + } +]; + + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule] +}) +export class BookmarkRoutingModule { } \ No newline at end of file diff --git a/UI/Web/src/app/bookmark/bookmark.module.ts b/UI/Web/src/app/bookmark/bookmark.module.ts new file mode 100644 index 000000000..12d5c7f46 --- /dev/null +++ b/UI/Web/src/app/bookmark/bookmark.module.ts @@ -0,0 +1,26 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { CardsModule } from '../cards/cards.module'; +import { SharedModule } from '../shared/shared.module'; +import { NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap'; +import { SidenavModule } from '../sidenav/sidenav.module'; +import { BookmarkRoutingModule } from './bookmark-routing.module'; +import { BookmarksComponent } from './bookmarks/bookmarks.component'; + + + +@NgModule({ + declarations: [ + BookmarksComponent + ], + imports: [ + CommonModule, + CardsModule, + SharedModule, + SidenavModule, + NgbTooltipModule, + + BookmarkRoutingModule + ] +}) +export class BookmarkModule { } diff --git a/UI/Web/src/app/bookmark/bookmarks/bookmarks.component.html b/UI/Web/src/app/bookmark/bookmarks/bookmarks.component.html new file mode 100644 index 000000000..f08659b1d --- /dev/null +++ b/UI/Web/src/app/bookmark/bookmarks/bookmarks.component.html @@ -0,0 +1,23 @@ + +

+ + Bookmarks +

+
{{series?.length}} Series
+
+ + + + + + + + There are no bookmarks. Try creating one . + + \ No newline at end of file diff --git a/UI/Web/src/app/cards/_modals/bookmarks-modal/bookmarks-modal.component.scss b/UI/Web/src/app/bookmark/bookmarks/bookmarks.component.scss similarity index 100% rename from UI/Web/src/app/cards/_modals/bookmarks-modal/bookmarks-modal.component.scss rename to UI/Web/src/app/bookmark/bookmarks/bookmarks.component.scss diff --git a/UI/Web/src/app/bookmark/bookmarks/bookmarks.component.ts b/UI/Web/src/app/bookmark/bookmarks/bookmarks.component.ts new file mode 100644 index 000000000..9524772ee --- /dev/null +++ b/UI/Web/src/app/bookmark/bookmarks/bookmarks.component.ts @@ -0,0 +1,167 @@ +import { Component, HostListener, OnDestroy, OnInit } from '@angular/core'; +import { Router } from '@angular/router'; +import { ToastrService } from 'ngx-toastr'; +import { take, takeWhile, finalize, Subject, forkJoin } from 'rxjs'; +import { BulkSelectionService } from 'src/app/cards/bulk-selection.service'; +import { ConfirmService } from 'src/app/shared/confirm.service'; +import { DownloadService } from 'src/app/shared/_services/download.service'; +import { KEY_CODES } from 'src/app/shared/_services/utility.service'; +import { PageBookmark } from 'src/app/_models/page-bookmark'; +import { Series } from 'src/app/_models/series'; +import { Action, ActionFactoryService, ActionItem } from 'src/app/_services/action-factory.service'; +import { ImageService } from 'src/app/_services/image.service'; +import { ReaderService } from 'src/app/_services/reader.service'; +import { SeriesService } from 'src/app/_services/series.service'; + +@Component({ + selector: 'app-bookmarks', + templateUrl: './bookmarks.component.html', + styleUrls: ['./bookmarks.component.scss'] +}) +export class BookmarksComponent implements OnInit, OnDestroy { + + bookmarks: Array = []; + series: Array = []; + loadingBookmarks: boolean = false; + seriesIds: {[id: number]: number} = {}; + downloadingSeries: {[id: number]: boolean} = {}; + clearingSeries: {[id: number]: boolean} = {}; + actions: ActionItem[] = []; + + private onDestroy: Subject = new Subject(); + + constructor(private readerService: ReaderService, private seriesService: SeriesService, + private downloadService: DownloadService, private toastr: ToastrService, + private confirmService: ConfirmService, public bulkSelectionService: BulkSelectionService, + public imageService: ImageService, private actionFactoryService: ActionFactoryService, + private router: Router) { } + + ngOnInit(): void { + this.loadBookmarks(); + + this.actions = this.actionFactoryService.getBookmarkActions(this.handleAction.bind(this)); + } + + ngOnDestroy() { + this.onDestroy.next(); + this.onDestroy.complete(); + } + + @HostListener('document:keydown.shift', ['$event']) + handleKeypress(event: KeyboardEvent) { + if (event.key === KEY_CODES.SHIFT) { + this.bulkSelectionService.isShiftDown = true; + } + } + + @HostListener('document:keyup.shift', ['$event']) + handleKeyUp(event: KeyboardEvent) { + if (event.key === KEY_CODES.SHIFT) { + this.bulkSelectionService.isShiftDown = false; + } + } + + async handleAction(action: Action, series: Series) { + switch (action) { + case(Action.Delete): + if (!await this.confirmService.confirm('Are you sure you want to clear all bookmarks for ' + series.name + '? This cannot be undone.')) { + break; + } + break; + case(Action.DownloadBookmark): + this.downloadBookmarks(series); + break; + default: + break; + } + } + + bulkActionCallback = async (action: Action, data: any) => { + const selectedSeriesIndexies = this.bulkSelectionService.getSelectedCardsForSource('bookmark'); + const selectedSeries = this.series.filter((series, index: number) => selectedSeriesIndexies.includes(index + '')); + const seriesIds = selectedSeries.map(item => item.id); + + switch (action) { + case Action.DownloadBookmark: + this.downloadService.downloadBookmarks(this.bookmarks.filter(bmk => seriesIds.includes(bmk.seriesId))).pipe( + takeWhile(val => { + return val.state != 'DONE'; + })).subscribe(() => { + this.bulkSelectionService.deselectAll(); + }); + break; + case Action.Delete: + if (!await this.confirmService.confirm('Are you sure you want to clear all bookmarks for multiple series? This cannot be undone.')) { + break; + } + + forkJoin(seriesIds.map(id => this.readerService.clearBookmarks(id))).subscribe(() => { + this.toastr.success('Bookmarks have been removed'); + this.bulkSelectionService.deselectAll(); + this.loadBookmarks(); + }) + break; + default: + break; + } + } + + loadBookmarks() { + this.loadingBookmarks = true; + this.readerService.getAllBookmarks().pipe(take(1)).subscribe(bookmarks => { + this.bookmarks = bookmarks; + this.seriesIds = {}; + this.bookmarks.forEach(bmk => { + if (!this.seriesIds.hasOwnProperty(bmk.seriesId)) { + this.seriesIds[bmk.seriesId] = 1; + } else { + this.seriesIds[bmk.seriesId] += 1; + } + this.downloadingSeries[bmk.seriesId] = false; + this.clearingSeries[bmk.seriesId] = false; + }); + + const ids = Object.keys(this.seriesIds).map(k => parseInt(k, 10)); + this.seriesService.getAllSeriesByIds(ids).subscribe(series => { + this.series = series; + this.loadingBookmarks = false; + }); + }); + } + + viewBookmarks(series: Series) { + this.router.navigate(['library', series.libraryId, 'series', series.id, 'manga', 0], {queryParams: {incognitoMode: false, bookmarkMode: true}}); + } + + async clearBookmarks(series: Series) { + if (!await this.confirmService.confirm('Are you sure you want to clear all bookmarks for ' + series.name + '? This cannot be undone.')) { + return; + } + + this.clearingSeries[series.id] = true; + this.readerService.clearBookmarks(series.id).subscribe(() => { + const index = this.series.indexOf(series); + if (index > -1) { + this.series.splice(index, 1); + } + this.clearingSeries[series.id] = false; + this.toastr.success(series.name + '\'s bookmarks have been removed'); + }); + } + + getBookmarkPages(seriesId: number) { + return this.seriesIds[seriesId]; + } + + downloadBookmarks(series: Series) { + this.downloadingSeries[series.id] = true; + this.downloadService.downloadBookmarks(this.bookmarks.filter(bmk => bmk.seriesId === series.id)).pipe( + takeWhile(val => { + return val.state != 'DONE'; + }), + finalize(() => { + this.downloadingSeries[series.id] = false; + })).subscribe(() => {/* No Operation */}); + } + +} diff --git a/UI/Web/src/app/cards/_modals/bookmarks-modal/bookmarks-modal.component.html b/UI/Web/src/app/cards/_modals/bookmarks-modal/bookmarks-modal.component.html deleted file mode 100644 index 606ed1384..000000000 --- a/UI/Web/src/app/cards/_modals/bookmarks-modal/bookmarks-modal.component.html +++ /dev/null @@ -1,29 +0,0 @@ - - - diff --git a/UI/Web/src/app/cards/_modals/bookmarks-modal/bookmarks-modal.component.ts b/UI/Web/src/app/cards/_modals/bookmarks-modal/bookmarks-modal.component.ts deleted file mode 100644 index 38dd16ff7..000000000 --- a/UI/Web/src/app/cards/_modals/bookmarks-modal/bookmarks-modal.component.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { Component, Input, OnInit } from '@angular/core'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { ToastrService } from 'ngx-toastr'; -import { finalize, take, takeWhile } from 'rxjs/operators'; -import { DownloadService } from 'src/app/shared/_services/download.service'; -import { PageBookmark } from 'src/app/_models/page-bookmark'; -import { Series } from 'src/app/_models/series'; -import { ImageService } from 'src/app/_services/image.service'; -import { ReaderService } from 'src/app/_services/reader.service'; - -@Component({ - selector: 'app-bookmarks-modal', - templateUrl: './bookmarks-modal.component.html', - styleUrls: ['./bookmarks-modal.component.scss'] -}) -export class BookmarksModalComponent implements OnInit { - - @Input() series!: Series; - - bookmarks: Array = []; - title: string = ''; - subtitle: string = ''; - isDownloading: boolean = false; - isClearing: boolean = false; - - uniqueChapters: number = 0; - - constructor(public imageService: ImageService, private readerService: ReaderService, - public modal: NgbActiveModal, private downloadService: DownloadService, - private toastr: ToastrService) { } - - ngOnInit(): void { - this.init(); - } - - init() { - this.readerService.getBookmarksForSeries(this.series.id).pipe(take(1)).subscribe(bookmarks => { - this.bookmarks = bookmarks; - const chapters: {[id: number]: string} = {}; - this.bookmarks.forEach(bmk => { - if (!chapters.hasOwnProperty(bmk.chapterId)) { - chapters[bmk.chapterId] = ''; - } - }); - this.uniqueChapters = Object.keys(chapters).length; - }); - } - - close() { - this.modal.close(); - } - - removeBookmark(bookmark: PageBookmark, index: number) { - this.bookmarks.splice(index, 1); - } - - downloadBookmarks() { - this.isDownloading = true; - this.downloadService.downloadBookmarks(this.bookmarks).pipe( - takeWhile(val => { - return val.state != 'DONE'; - }), - finalize(() => { - this.isDownloading = false; - })).subscribe(() => {/* No Operation */}); - } - - clearBookmarks() { - this.isClearing = true; - this.readerService.clearBookmarks(this.series.id).subscribe(() => { - this.isClearing = false; - this.init(); - this.toastr.success(this.series.name + '\'s bookmarks have been removed'); - }); - } - -} diff --git a/UI/Web/src/app/cards/_modals/card-details-modal/card-details-modal.component.html b/UI/Web/src/app/cards/_modals/card-details-modal/card-details-modal.component.html index 3bab2708c..7675106be 100644 --- a/UI/Web/src/app/cards/_modals/card-details-modal/card-details-modal.component.html +++ b/UI/Web/src/app/cards/_modals/card-details-modal/card-details-modal.component.html @@ -89,20 +89,6 @@ -
  • - {{tabs[3].title}} - -
    - - - - - No bookmarks yet - -
    -
    -
  • -
  • {{tabs[4].title}} diff --git a/UI/Web/src/app/cards/_modals/card-details-modal/card-details-modal.component.ts b/UI/Web/src/app/cards/_modals/card-details-modal/card-details-modal.component.ts index 82a0a3f9d..6c9b86133 100644 --- a/UI/Web/src/app/cards/_modals/card-details-modal/card-details-modal.component.ts +++ b/UI/Web/src/app/cards/_modals/card-details-modal/card-details-modal.component.ts @@ -66,9 +66,8 @@ export class CardDetailsModalComponent implements OnInit { chapterActions: ActionItem[] = []; libraryType: LibraryType = LibraryType.Manga; - bookmarks: PageBookmark[] = []; - tabs = [{title: 'General', disabled: false}, {title: 'Metadata', disabled: false}, {title: 'Cover', disabled: false}, {title: 'Bookmarks', disabled: false}, {title: 'Info', disabled: false}]; + tabs = [{title: 'General', disabled: false}, {title: 'Metadata', disabled: false}, {title: 'Cover', disabled: false}, {title: 'Info', disabled: false}]; active = this.tabs[0]; chapterMetadata!: ChapterMetadata; @@ -100,17 +99,6 @@ export class CardDetailsModalComponent implements OnInit { this.imageUrls.push(this.imageService.getChapterCoverImage(this.chapter.id)); - let bookmarkApi; - if (this.isChapter) { - bookmarkApi = this.readerService.getBookmarks(this.chapter.id); - } else { - bookmarkApi = this.readerService.getBookmarksForVolume(this.data.id); - } - - bookmarkApi.pipe(take(1)).subscribe(bookmarks => { - this.bookmarks = bookmarks; - }); - this.seriesService.getChapterMetadata(this.chapter.id).subscribe(metadata => { this.chapterMetadata = metadata; @@ -240,10 +228,4 @@ export class CardDetailsModalComponent implements OnInit { this.router.navigate(['library', this.libraryId, 'series', this.seriesId, 'manga', chapter.id]); } } - - removeBookmark(bookmark: PageBookmark, index: number) { - this.readerService.unbookmark(bookmark.seriesId, bookmark.volumeId, bookmark.chapterId, bookmark.page).subscribe(() => { - this.bookmarks.splice(index, 1); - }); - } } diff --git a/UI/Web/src/app/cards/bulk-selection.service.ts b/UI/Web/src/app/cards/bulk-selection.service.ts index a7f6eba70..81f972481 100644 --- a/UI/Web/src/app/cards/bulk-selection.service.ts +++ b/UI/Web/src/app/cards/bulk-selection.service.ts @@ -3,7 +3,7 @@ import { NavigationStart, Router } from '@angular/router'; import { filter } from 'rxjs/operators'; import { Action, ActionFactoryService } from '../_services/action-factory.service'; -type DataSource = 'volume' | 'chapter' | 'special' | 'series'; +type DataSource = 'volume' | 'chapter' | 'special' | 'series' | 'bookmark'; /** * Responsible for handling selections on cards. Can handle multiple card sources next to each other in different loops. @@ -132,6 +132,10 @@ export class BulkSelectionService { return this.actionFactory.getSeriesActions(callback).filter(item => allowedActions.includes(item.action)); } + if (Object.keys(this.selectedCards).filter(item => item === 'bookmark').length > 0) { + return this.actionFactory.getBookmarkActions(callback); + } + return this.actionFactory.getVolumeActions(callback).filter(item => allowedActions.includes(item.action)); } diff --git a/UI/Web/src/app/cards/card-detail-layout/card-detail-layout.component.html b/UI/Web/src/app/cards/card-detail-layout/card-detail-layout.component.html index 7487d71fa..540c9091b 100644 --- a/UI/Web/src/app/cards/card-detail-layout/card-detail-layout.component.html +++ b/UI/Web/src/app/cards/card-detail-layout/card-detail-layout.component.html @@ -24,7 +24,7 @@

    - There is no data +

    @@ -73,3 +73,8 @@
    +
    +
    + +
    +
    \ No newline at end of file diff --git a/UI/Web/src/app/cards/card-detail-layout/card-detail-layout.component.ts b/UI/Web/src/app/cards/card-detail-layout/card-detail-layout.component.ts index 933f0fa04..1dd9e8000 100644 --- a/UI/Web/src/app/cards/card-detail-layout/card-detail-layout.component.ts +++ b/UI/Web/src/app/cards/card-detail-layout/card-detail-layout.component.ts @@ -38,6 +38,7 @@ export class CardDetailLayoutComponent implements OnInit, OnDestroy { @Output() applyFilter: EventEmitter = new EventEmitter(); @ContentChild('cardItem') itemTemplate!: TemplateRef; + @ContentChild('noData') noDataTemplate!: TemplateRef; // Filter Code diff --git a/UI/Web/src/app/cards/cards.module.ts b/UI/Web/src/app/cards/cards.module.ts index cf9027fb0..1f8d2d23a 100644 --- a/UI/Web/src/app/cards/cards.module.ts +++ b/UI/Web/src/app/cards/cards.module.ts @@ -5,7 +5,6 @@ import { LibraryCardComponent } from './library-card/library-card.component'; import { CoverImageChooserComponent } from './cover-image-chooser/cover-image-chooser.component'; import { EditSeriesModalComponent } from './_modals/edit-series-modal/edit-series-modal.component'; import { EditCollectionTagsComponent } from './_modals/edit-collection-tags/edit-collection-tags.component'; -import { BookmarksModalComponent } from './_modals/bookmarks-modal/bookmarks-modal.component'; import { NgbTooltipModule, NgbCollapseModule, NgbPaginationModule, NgbDropdownModule, NgbProgressbarModule, NgbNavModule, NgbRatingModule } from '@ng-bootstrap/ng-bootstrap'; import { CardActionablesComponent } from './card-item/card-actionables/card-actionables.component'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; @@ -21,7 +20,6 @@ import { BulkAddToCollectionComponent } from './_modals/bulk-add-to-collection/b import { PipeModule } from '../pipe/pipe.module'; import { ChapterMetadataDetailComponent } from './chapter-metadata-detail/chapter-metadata-detail.component'; import { FileInfoComponent } from './file-info/file-info.component'; -import { BookmarkComponent } from './bookmark/bookmark.component'; import { MetadataFilterModule } from '../metadata-filter/metadata-filter.module'; @@ -34,7 +32,6 @@ import { MetadataFilterModule } from '../metadata-filter/metadata-filter.module' CoverImageChooserComponent, EditSeriesModalComponent, EditCollectionTagsComponent, - BookmarksModalComponent, CardActionablesComponent, CardDetailLayoutComponent, CardDetailsModalComponent, @@ -42,7 +39,6 @@ import { MetadataFilterModule } from '../metadata-filter/metadata-filter.module' BulkAddToCollectionComponent, ChapterMetadataDetailComponent, FileInfoComponent, - BookmarkComponent, ], imports: [ CommonModule, @@ -79,7 +75,6 @@ import { MetadataFilterModule } from '../metadata-filter/metadata-filter.module' CoverImageChooserComponent, EditSeriesModalComponent, EditCollectionTagsComponent, - BookmarksModalComponent, CardActionablesComponent, CardDetailLayoutComponent, CardDetailsModalComponent, diff --git a/UI/Web/src/app/cards/series-card/series-card.component.ts b/UI/Web/src/app/cards/series-card/series-card.component.ts index a460a16f5..adf0560d1 100644 --- a/UI/Web/src/app/cards/series-card/series-card.component.ts +++ b/UI/Web/src/app/cards/series-card/series-card.component.ts @@ -96,9 +96,6 @@ export class SeriesCardComponent implements OnInit, OnChanges, OnDestroy { case(Action.Edit): this.openEditModal(series); break; - case(Action.Bookmarks): - this.actionService.openBookmarkModal(series, (series) => {/* No Operation */ }); - break; case(Action.AddToReadingList): this.actionService.addSeriesToReadingList(series, (series) => {/* No Operation */ }); break; 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 6434fc681..30a364d6c 100644 --- a/UI/Web/src/app/manga-reader/manga-reader.component.html +++ b/UI/Web/src/app/manga-reader/manga-reader.component.html @@ -21,7 +21,7 @@ - + @@ -47,7 +47,7 @@ class="{{getFittingOptionClass()}} {{readerMode === ReaderMode.LeftRight || readerMode === ReaderMode.UpDown ? '' : 'd-none'}} {{showClickOverlay ? 'blur' : ''}}"> - + diff --git a/UI/Web/src/app/manga-reader/manga-reader.component.ts b/UI/Web/src/app/manga-reader/manga-reader.component.ts index bf5a8a7d8..e9bc9adfe 100644 --- a/UI/Web/src/app/manga-reader/manga-reader.component.ts +++ b/UI/Web/src/app/manga-reader/manga-reader.component.ts @@ -27,6 +27,7 @@ import { LibraryType } from '../_models/library'; import { ShorcutsModalComponent } from '../reader-shared/_modals/shorcuts-modal/shorcuts-modal.component'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; import { LayoutMode } from './_models/layout-mode'; +import { SeriesService } from '../_services/series.service'; const PREFETCH_PAGES = 8; @@ -79,6 +80,10 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy { * If this is true, no progress will be saved. */ incognitoMode: boolean = false; + /** + * If this is true, we are reading a bookmark. ChapterId will be 0. There is no continuous reading. Progress is not saved. Bookmark control is removed. + */ + bookmarkMode: boolean = false; /** * If this is true, chapters will be fetched in the order of a reading list, rather than natural series order. @@ -257,7 +262,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy { private readonly onDestroy = new Subject(); - getPageUrl = (pageNum: number) => this.readerService.getPageUrl(this.chapterId, pageNum); + //getPageUrl = (pageNum: number) => this.readerService.getPageUrl(this.chapterId, pageNum); get PageNumber() { return Math.max(Math.min(this.pageNum, this.maxPages - 1), 0); @@ -326,7 +331,8 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy { private formBuilder: FormBuilder, private navService: NavService, private toastr: ToastrService, private memberService: MemberService, public utilityService: UtilityService, private renderer: Renderer2, - @Inject(DOCUMENT) private document: Document, private modalService: NgbModal) { + @Inject(DOCUMENT) private document: Document, private modalService: NgbModal, + private seriesService: SeriesService) { this.navService.hideNavBar(); this.navService.hideSideNav(); } @@ -347,6 +353,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy { this.seriesId = parseInt(seriesId, 10); this.chapterId = parseInt(chapterId, 10); this.incognitoMode = this.route.snapshot.queryParamMap.get('incognitoMode') === 'true'; + this.bookmarkMode = this.route.snapshot.queryParamMap.get('bookmarkMode') === 'true'; const readingListId = this.route.snapshot.queryParamMap.get('readingListId'); if (readingListId != null) { @@ -506,12 +513,42 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy { this.goToPageEvent.complete(); } + if (this.bookmarkMode) { + this.readerService.getBookmarkInfo(this.seriesId).subscribe(bookmarkInfo => { + this.setPageNum(0); + this.title = bookmarkInfo.seriesName + ' Bookmarks'; + this.libraryType = bookmarkInfo.libraryType; + this.maxPages = bookmarkInfo.pages; + + // Due to change detection rules in Angular, we need to re-create the options object to apply the change + const newOptions: Options = Object.assign({}, this.pageOptions); + newOptions.ceil = this.maxPages - 1; // We -1 so that the slider UI shows us hitting the end, since visually we +1 everything. + this.pageOptions = newOptions; + this.inSetup = false; + this.prevChapterDisabled = true; + this.nextChapterDisabled = true; + + const images = []; + for (let i = 0; i < PREFETCH_PAGES + 2; i++) { + images.push(new Image()); + } + + this.cachedImages = new CircularArray(images, 0); + + this.render(); + }); + + return; + + } + forkJoin({ progress: this.readerService.getProgress(this.chapterId), chapterInfo: this.readerService.getChapterInfo(this.chapterId), bookmarks: this.readerService.getBookmarks(this.chapterId), }).pipe(take(1)).subscribe(results => { + if (this.readingListMode && results.chapterInfo.seriesFormat === MangaFormat.EPUB) { // Redirect to the book reader. const params = this.readerService.getQueryParamsObject(this.incognitoMode, this.readingListMode, this.readingListId); @@ -1049,7 +1086,9 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy { return; } if (offsetIndex < this.maxPages - 1) { - item.src = this.readerService.getPageUrl(this.chapterId, offsetIndex); + // if (this.bookmarkMode) item.src = this.readerService.getBookmarkPageUrl(this.seriesId, offsetIndex); + // else item.src = this.readerService.getPageUrl(this.chapterId, offsetIndex); + item.src = this.getPageUrl(offsetIndex); index += 1; } }, this.cachedImages.size() - 3); @@ -1057,6 +1096,11 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy { //console.log('cachedImages: ', this.cachedImages.arr.map(img => this.readerService.imageUrlToPageNum(img.src) + ': ' + img.complete)); } + getPageUrl(pageNum: number) { + if (this.bookmarkMode) return this.readerService.getBookmarkPageUrl(this.seriesId, this.user.apiKey, pageNum); + return this.readerService.getPageUrl(this.chapterId, pageNum); + } + loadPage() { if (!this.canvas || !this.ctx) { return; } @@ -1067,10 +1111,13 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy { if (this.readerService.imageUrlToPageNum(this.canvasImage.src) !== this.pageNum || this.canvasImage.src === '' || !this.canvasImage.complete) { if (this.layoutMode === LayoutMode.Single) { - this.canvasImage.src = this.readerService.getPageUrl(this.chapterId, this.pageNum); + //this.canvasImage.src = this.readerService.getPageUrl(this.chapterId, this.pageNum); + this.canvasImage.src = this.getPageUrl(this.pageNum); } else { - this.canvasImage.src = this.readerService.getPageUrl(this.chapterId, this.pageNum); - this.canvasImage2.src = this.readerService.getPageUrl(this.chapterId, this.pageNum + 1); // TODO: I need to handle last page correctly + // this.canvasImage.src = this.readerService.getPageUrl(this.chapterId, this.pageNum); + // this.canvasImage2.src = this.readerService.getPageUrl(this.chapterId, this.pageNum + 1); // TODO: I need to handle last page correctly + this.canvasImage.src = this.getPageUrl(this.pageNum); + this.canvasImage2.src = this.getPageUrl(this.pageNum + 1); // TODO: I need to handle last page correctly } this.canvasImage.onload = () => this.renderPage(); @@ -1143,7 +1190,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy { tempPageNum = this.pageNum + 1; } - if (!this.incognitoMode) { + if (!this.incognitoMode || !this.bookmarkMode) { this.readerService.saveProgress(this.seriesId, this.volumeId, this.chapterId, tempPageNum).pipe(take(1)).subscribe(() => {/* No operation */}); } } @@ -1297,7 +1344,9 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy { const newRoute = this.readerService.getNextChapterUrl(this.router.url, this.chapterId, this.incognitoMode, this.readingListMode, this.readingListId); window.history.replaceState({}, '', newRoute); this.toastr.info('Incognito mode is off. Progress will now start being tracked.'); - this.readerService.saveProgress(this.seriesId, this.volumeId, this.chapterId, this.pageNum).pipe(take(1)).subscribe(() => {/* No operation */}); + if (!this.bookmarkMode) { + this.readerService.saveProgress(this.seriesId, this.volumeId, this.chapterId, this.pageNum).pipe(take(1)).subscribe(() => {/* No operation */}); + } } getWindowDimensions() { diff --git a/UI/Web/src/app/metadata-filter/metadata-filter.component.html b/UI/Web/src/app/metadata-filter/metadata-filter.component.html index 04fecaa25..943858007 100644 --- a/UI/Web/src/app/metadata-filter/metadata-filter.component.html +++ b/UI/Web/src/app/metadata-filter/metadata-filter.component.html @@ -8,7 +8,7 @@

    Book Settings - +

    diff --git a/UI/Web/src/app/metadata-filter/metadata-filter.component.ts b/UI/Web/src/app/metadata-filter/metadata-filter.component.ts index 268d14944..212966835 100644 --- a/UI/Web/src/app/metadata-filter/metadata-filter.component.ts +++ b/UI/Web/src/app/metadata-filter/metadata-filter.component.ts @@ -17,6 +17,7 @@ import { Tag } from '../_models/tag'; import { CollectionTagService } from '../_services/collection-tag.service'; import { LibraryService } from '../_services/library.service'; import { MetadataService } from '../_services/metadata.service'; +import { NavService } from '../_services/nav.service'; import { SeriesService } from '../_services/series.service'; import { FilterSettings } from './filter-settings'; @@ -42,6 +43,7 @@ export class MetadataFilterComponent implements OnInit, OnDestroy { @Output() applyFilter: EventEmitter = new EventEmitter(); @ContentChild('[ngbCollapse]') collapse!: NgbCollapse; + //@ContentChild('commentDrawer') commentDrawer: formatSettings: TypeaheadSettings> = new TypeaheadSettings(); @@ -74,7 +76,7 @@ export class MetadataFilterComponent implements OnInit, OnDestroy { fullyLoaded: boolean = false; - private onDestory: Subject = new Subject(); + private onDestroy: Subject = new Subject(); get PersonRole(): typeof PersonRole { return PersonRole; @@ -94,7 +96,7 @@ export class MetadataFilterComponent implements OnInit, OnDestroy { } if (this.filterOpen) { - this.filterOpen.pipe(takeUntil(this.onDestory)).subscribe(openState => { + this.filterOpen.pipe(takeUntil(this.onDestroy)).subscribe(openState => { this.filteringCollapsed = !openState; }); } @@ -114,7 +116,7 @@ export class MetadataFilterComponent implements OnInit, OnDestroy { seriesNameQuery: new FormControl({value: this.filter.seriesNameQuery || '', disabled: this.filterSettings.searchNameDisabled}, []) }); - this.readProgressGroup.valueChanges.pipe(takeUntil(this.onDestory)).subscribe(changes => { + this.readProgressGroup.valueChanges.pipe(takeUntil(this.onDestroy)).subscribe(changes => { this.filter.readStatus.read = this.readProgressGroup.get('read')?.value; this.filter.readStatus.inProgress = this.readProgressGroup.get('inProgress')?.value; this.filter.readStatus.notRead = this.readProgressGroup.get('notRead')?.value; @@ -135,7 +137,7 @@ export class MetadataFilterComponent implements OnInit, OnDestroy { } }); - this.sortGroup.valueChanges.pipe(takeUntil(this.onDestory)).subscribe(changes => { + this.sortGroup.valueChanges.pipe(takeUntil(this.onDestroy)).subscribe(changes => { if (this.filter.sortOptions == null) { this.filter.sortOptions = { isAscending: this.isAscendingSort, @@ -148,7 +150,7 @@ export class MetadataFilterComponent implements OnInit, OnDestroy { this.seriesNameGroup.get('seriesNameQuery')?.valueChanges.pipe( map(val => (val || '').trim()), distinctUntilChanged(), - takeUntil(this.onDestory)) + takeUntil(this.onDestroy)) .subscribe(changes => { this.filter.seriesNameQuery = changes; }); @@ -156,9 +158,14 @@ export class MetadataFilterComponent implements OnInit, OnDestroy { this.loadFromPresetsAndSetup(); } + close() { + this.filterOpen.emit(false); + this.filteringCollapsed = true; + } + ngOnDestroy() { - this.onDestory.next(); - this.onDestory.complete(); + this.onDestroy.next(); + this.onDestroy.complete(); } getPersonsSettings(role: PersonRole) { diff --git a/UI/Web/src/app/nav-header/nav-header.component.ts b/UI/Web/src/app/nav-header/nav-header.component.ts index 983b393e0..ae73a2c77 100644 --- a/UI/Web/src/app/nav-header/nav-header.component.ts +++ b/UI/Web/src/app/nav-header/nav-header.component.ts @@ -1,9 +1,10 @@ import { DOCUMENT } from '@angular/common'; import { Component, HostListener, Inject, OnDestroy, OnInit, ViewChild } from '@angular/core'; import { Router } from '@angular/router'; -import { Subject } from 'rxjs'; +import { from, fromEvent, Subject } from 'rxjs'; import { takeUntil } from 'rxjs/operators'; import { ScrollService } from '../scroll.service'; +import { FilterQueryParam, FilterUtilitiesService } from '../shared/_services/filter-utilities.service'; import { CollectionTag } from '../_models/collection-tag'; import { Library } from '../_models/library'; import { PersonRole } from '../_models/person'; @@ -47,28 +48,17 @@ export class NavHeaderComponent implements OnInit, OnDestroy { constructor(public accountService: AccountService, private router: Router, public navService: NavService, private libraryService: LibraryService, public imageService: ImageService, @Inject(DOCUMENT) private document: Document, - private scrollService: ScrollService) { } + private scrollService: ScrollService, private filterUtilityService: FilterUtilitiesService) { } ngOnInit(): void { - // this.navService.darkMode$.pipe(takeUntil(this.onDestroy)).subscribe(res => { - // if (res) { - // this.document.body.classList.remove('bg-light'); - // this.document.body.classList.add('bg-dark'); - // } else { - // this.document.body.classList.remove('bg-dark'); - // this.document.body.classList.add('bg-light'); - // } - // }); - } - - @HostListener("window:scroll", []) - checkBackToTopNeeded() { - const offset = this.scrollService.scrollPosition; - if (offset > 100) { - this.backToTopNeeded = true; - } else if (offset < 40) { - this.backToTopNeeded = false; - } + fromEvent(this.document.body, 'scroll').pipe(takeUntil(this.onDestroy)).subscribe(() => { + const offset = this.scrollService.scrollPosition; + if (offset > 100) { + this.backToTopNeeded = true; + } else if (offset < 40) { + this.backToTopNeeded = false; + } + }) } ngOnDestroy() { @@ -106,50 +96,46 @@ export class NavHeaderComponent implements OnInit, OnDestroy { goTo(queryParamName: string, filter: any) { let params: any = {}; params[queryParamName] = filter; - params['page'] = 1; + params[FilterQueryParam.Page] = 1; this.clearSearch(); this.router.navigate(['all-series'], {queryParams: params}); } goToPerson(role: PersonRole, filter: any) { - // TODO: Move this to utility service this.clearSearch(); switch(role) { case PersonRole.Writer: - this.goTo('writers', filter); + this.goTo(FilterQueryParam.Writers, filter); break; case PersonRole.Artist: - this.goTo('artists', filter); + this.goTo(FilterQueryParam.Artists, filter); break; case PersonRole.Character: - this.goTo('character', filter); + this.goTo(FilterQueryParam.Character, filter); break; case PersonRole.Colorist: - this.goTo('colorist', filter); + this.goTo(FilterQueryParam.Colorist, filter); break; case PersonRole.Editor: - this.goTo('editor', filter); + this.goTo(FilterQueryParam.Editor, filter); break; case PersonRole.Inker: - this.goTo('inker', filter); + this.goTo(FilterQueryParam.Inker, filter); break; case PersonRole.CoverArtist: - this.goTo('coverArtists', filter); - break; - case PersonRole.Inker: - this.goTo('inker', filter); + this.goTo(FilterQueryParam.CoverArtists, filter); break; case PersonRole.Letterer: - this.goTo('letterer', filter); + this.goTo(FilterQueryParam.Letterer, filter); break; case PersonRole.Penciller: - this.goTo('penciller', filter); + this.goTo(FilterQueryParam.Penciller, filter); break; case PersonRole.Publisher: - this.goTo('publisher', filter); + this.goTo(FilterQueryParam.Publisher, filter); break; case PersonRole.Translator: - this.goTo('translator', filter); + this.goTo(FilterQueryParam.Translator, filter); break; } } @@ -183,10 +169,7 @@ export class NavHeaderComponent implements OnInit, OnDestroy { scrollToTop() { - window.scroll({ - top: 0, - behavior: 'smooth' - }); + this.scrollService.scrollTo(0, this.document.body); } focusUpdate(searchFocused: boolean) { diff --git a/UI/Web/src/app/recently-added/recently-added.component.html b/UI/Web/src/app/recently-added/recently-added.component.html deleted file mode 100644 index ab88a2cc0..000000000 --- a/UI/Web/src/app/recently-added/recently-added.component.html +++ /dev/null @@ -1,19 +0,0 @@ - -

    - Recently Added -

    -
    - - - - - - \ No newline at end of file diff --git a/UI/Web/src/app/recently-added/recently-added.component.scss b/UI/Web/src/app/recently-added/recently-added.component.scss deleted file mode 100644 index e69de29bb..000000000 diff --git a/UI/Web/src/app/recently-added/recently-added.component.ts b/UI/Web/src/app/recently-added/recently-added.component.ts deleted file mode 100644 index 0cc6561d3..000000000 --- a/UI/Web/src/app/recently-added/recently-added.component.ts +++ /dev/null @@ -1,159 +0,0 @@ -import { Component, EventEmitter, HostListener, OnDestroy, OnInit } from '@angular/core'; -import { Title } from '@angular/platform-browser'; -import { ActivatedRoute, Router } from '@angular/router'; -import { Subject } from 'rxjs'; -import { debounceTime, take, takeUntil } from 'rxjs/operators'; -import { BulkSelectionService } from '../cards/bulk-selection.service'; -import { FilterSettings } from '../metadata-filter/filter-settings'; -import { KEY_CODES, UtilityService } from '../shared/_services/utility.service'; -import { SeriesAddedEvent } from '../_models/events/series-added-event'; -import { Pagination } from '../_models/pagination'; -import { Series } from '../_models/series'; -import { FilterEvent, SeriesFilter } from '../_models/series-filter'; -import { Action } from '../_services/action-factory.service'; -import { ActionService } from '../_services/action.service'; -import { EVENTS, Message, MessageHubService } from '../_services/message-hub.service'; -import { SeriesService } from '../_services/series.service'; - -// TODO: Do I still need this or can All Series handle it with a custom sort - -/** - * This component is used as a standard layout for any card detail. ie) series, on-deck, collections, etc. - */ -@Component({ - selector: 'app-recently-added', - templateUrl: './recently-added.component.html', - styleUrls: ['./recently-added.component.scss'] -}) -export class RecentlyAddedComponent implements OnInit, OnDestroy { - - isLoading: boolean = true; - series: Series[] = []; - pagination!: Pagination; - libraryId!: number; - - filter: SeriesFilter | undefined = undefined; - filterSettings: FilterSettings = new FilterSettings(); - filterOpen: EventEmitter = new EventEmitter(); - filterActive: boolean = false; - - onDestroy: Subject = new Subject(); - - constructor(private router: Router, private route: ActivatedRoute, private seriesService: SeriesService, private titleService: Title, - private actionService: ActionService, public bulkSelectionService: BulkSelectionService, private hubService: MessageHubService, - private utilityService: UtilityService) { - this.router.routeReuseStrategy.shouldReuseRoute = () => false; - this.titleService.setTitle('Kavita - Recently Added'); - if (this.pagination === undefined || this.pagination === null) { - this.pagination = {currentPage: 1, itemsPerPage: 30, totalItems: 0, totalPages: 1}; - } - this.filterSettings.sortDisabled = true; - - this.loadPage(); - } - - @HostListener('document:keydown.shift', ['$event']) - handleKeypress(event: KeyboardEvent) { - if (event.key === KEY_CODES.SHIFT) { - this.bulkSelectionService.isShiftDown = true; - } - } - - @HostListener('document:keyup.shift', ['$event']) - handleKeyUp(event: KeyboardEvent) { - if (event.key === KEY_CODES.SHIFT) { - this.bulkSelectionService.isShiftDown = false; - } - } - - ngOnInit() { - this.hubService.messages$.pipe(debounceTime(6000), takeUntil(this.onDestroy)).subscribe((event) => { - if (event.event !== EVENTS.SeriesAdded) return; - const seriesAdded = event.payload as SeriesAddedEvent; - if (seriesAdded.libraryId !== this.libraryId) return; - this.loadPage(); - }); - } - - ngOnDestroy() { - this.onDestroy.next(); - this.onDestroy.complete(); - } - - seriesClicked(series: Series) { - this.router.navigate(['library', this.libraryId, 'series', series.id]); - } - - onPageChange(pagination: Pagination) { - window.history.replaceState(window.location.href, '', window.location.href.split('?')[0] + '?page=' + this.pagination.currentPage); - this.loadPage(); - } - - applyFilter(event: FilterEvent) { - this.filter = event.filter; - const page = this.getPage(); - if (page === undefined || page === null || !event.isFirst) { - this.pagination.currentPage = 1; - this.onPageChange(this.pagination); - } else { - this.loadPage(); - } - } - - loadPage() { - const page = this.getPage(); - if (page != null) { - this.pagination.currentPage = parseInt(page, 10); - } - this.filterActive = !this.utilityService.deepEqual(this.filter, this.filterSettings.presets); - this.isLoading = true; - this.seriesService.getRecentlyAdded(this.libraryId, this.pagination?.currentPage, this.pagination?.itemsPerPage, this.filter).pipe(take(1)).subscribe(series => { - this.series = series.result; - this.pagination = series.pagination; - this.isLoading = false; - window.scrollTo(0, 0); - }); - } - - getPage() { - const urlParams = new URLSearchParams(window.location.search); - return urlParams.get('page'); - } - - bulkActionCallback = (action: Action, data: any) => { - const selectedSeriesIndexies = this.bulkSelectionService.getSelectedCardsForSource('series'); - const selectedSeries = this.series.filter((series, index: number) => selectedSeriesIndexies.includes(index + '')); - - switch (action) { - case Action.AddToReadingList: - this.actionService.addMultipleSeriesToReadingList(selectedSeries, () => { - this.bulkSelectionService.deselectAll(); - }); - break; - case Action.AddToCollection: - this.actionService.addMultipleSeriesToCollectionTag(selectedSeries, () => { - this.bulkSelectionService.deselectAll(); - }); - break; - case Action.MarkAsRead: - this.actionService.markMultipleSeriesAsRead(selectedSeries, () => { - this.loadPage(); - this.bulkSelectionService.deselectAll(); - }); - - break; - case Action.MarkAsUnread: - this.actionService.markMultipleSeriesAsUnread(selectedSeries, () => { - this.loadPage(); - this.bulkSelectionService.deselectAll(); - }); - break; - case Action.Delete: - this.actionService.deleteMultipleSeries(selectedSeries, () => { - this.loadPage(); - this.bulkSelectionService.deselectAll(); - }); - break; - } - } -} diff --git a/UI/Web/src/app/series-detail/series-detail.component.html b/UI/Web/src/app/series-detail/series-detail.component.html index c77eaaa9c..2c18e1598 100644 --- a/UI/Web/src/app/series-detail/series-detail.component.html +++ b/UI/Web/src/app/series-detail/series-detail.component.html @@ -72,13 +72,18 @@
  • Specials -
    - - - -
    + + + + + +
  • @@ -101,25 +106,35 @@
  • {{libraryType === LibraryType.Book ? 'Books': 'Volumes'}} -
    - - - -
    + + + + + + +
  • {{utilityService.formatChapterName(libraryType) + 's'}} -
    - - - -
    + + + + +
  • diff --git a/UI/Web/src/app/series-detail/series-detail.component.ts b/UI/Web/src/app/series-detail/series-detail.component.ts index 584fc55dc..2dfeb3258 100644 --- a/UI/Web/src/app/series-detail/series-detail.component.ts +++ b/UI/Web/src/app/series-detail/series-detail.component.ts @@ -263,9 +263,6 @@ export class SeriesDetailComponent implements OnInit, OnDestroy { case(Action.Delete): this.deleteSeries(series); break; - case(Action.Bookmarks): - this.actionService.openBookmarkModal(series, () => this.actionInProgress = false); - break; case(Action.AddToReadingList): this.actionService.addSeriesToReadingList(series, () => this.actionInProgress = false); break; diff --git a/UI/Web/src/app/sidenav/side-nav-companion-bar/side-nav-companion-bar.component.ts b/UI/Web/src/app/sidenav/side-nav-companion-bar/side-nav-companion-bar.component.ts index d49830a52..f05a7336d 100644 --- a/UI/Web/src/app/sidenav/side-nav-companion-bar/side-nav-companion-bar.component.ts +++ b/UI/Web/src/app/sidenav/side-nav-companion-bar/side-nav-companion-bar.component.ts @@ -1,4 +1,7 @@ -import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; +import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core'; +import { Subject, takeUntil } from 'rxjs'; +import { Breakpoint, UtilityService } from 'src/app/shared/_services/utility.service'; +import { NavService } from 'src/app/_services/nav.service'; /** * This should go on all pages which have the side nav present and is not Settings related. @@ -9,7 +12,7 @@ import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; templateUrl: './side-nav-companion-bar.component.html', styleUrls: ['./side-nav-companion-bar.component.scss'] }) -export class SideNavCompanionBarComponent implements OnInit { +export class SideNavCompanionBarComponent implements OnInit, OnDestroy { /** * If the page should show a filter */ @@ -34,10 +37,26 @@ export class SideNavCompanionBarComponent implements OnInit { isFilterOpen = false; - constructor() { } + private onDestroy: Subject = new Subject(); + + constructor(private navService: NavService, private utilityService: UtilityService) { + } ngOnInit(): void { this.isFilterOpen = this.filterOpenByDefault; + + // If user opens side nav while filter is open on mobile, then collapse filter (as it doesn't render well) TODO: Change this when we have new drawer + this.navService.sideNavCollapsed$.pipe(takeUntil(this.onDestroy)).subscribe(sideNavCollapsed => { + if (this.isFilterOpen && sideNavCollapsed && this.utilityService.getActiveBreakpoint() < Breakpoint.Desktop) { + this.isFilterOpen = false; + this.filterOpen.emit(this.isFilterOpen); + } + }); + } + + ngOnDestroy(): void { + this.onDestroy.next(); + this.onDestroy.complete(); } toggleFilter() { diff --git a/UI/Web/src/app/sidenav/side-nav/side-nav.component.html b/UI/Web/src/app/sidenav/side-nav/side-nav.component.html index 91f4b99b6..be5f58b02 100644 --- a/UI/Web/src/app/sidenav/side-nav/side-nav.component.html +++ b/UI/Web/src/app/sidenav/side-nav/side-nav.component.html @@ -8,21 +8,22 @@ --> - - - -
    - -
    - - -
    + + + + +
    + +
    + +
    - - - - +
    + + + +
    diff --git a/UI/Web/src/app/sidenav/side-nav/side-nav.component.ts b/UI/Web/src/app/sidenav/side-nav/side-nav.component.ts index 1d1e43e0b..110fe4acd 100644 --- a/UI/Web/src/app/sidenav/side-nav/side-nav.component.ts +++ b/UI/Web/src/app/sidenav/side-nav/side-nav.component.ts @@ -1,9 +1,9 @@ import { Component, OnDestroy, OnInit } from '@angular/core'; -import { Router } from '@angular/router'; +import { NavigationEnd, Router } from '@angular/router'; import { Observable, Subject } from 'rxjs'; -import { filter, take, takeUntil, takeWhile } from 'rxjs/operators'; +import { filter, map, take, takeUntil, takeWhile } from 'rxjs/operators'; import { EVENTS, MessageHubService } from 'src/app/_services/message-hub.service'; -import { UtilityService } from '../../shared/_services/utility.service'; +import { Breakpoint, UtilityService } from '../../shared/_services/utility.service'; import { Library } from '../../_models/library'; import { User } from '../../_models/user'; import { AccountService } from '../../_services/account.service'; @@ -29,12 +29,12 @@ export class SideNavComponent implements OnInit, OnDestroy { return library.name.toLowerCase().indexOf((this.filterQuery || '').toLowerCase()) >= 0; } - private onDestory: Subject = new Subject(); + private onDestroy: Subject = new Subject(); constructor(public accountService: AccountService, private libraryService: LibraryService, public utilityService: UtilityService, private messageHub: MessageHubService, - private actionFactoryService: ActionFactoryService, private actionService: ActionService, public navService: NavService) { } + private actionFactoryService: ActionFactoryService, private actionService: ActionService, public navService: NavService, private router: Router) { } ngOnInit(): void { this.accountService.currentUser$.pipe(take(1)).subscribe(user => { @@ -49,16 +49,27 @@ export class SideNavComponent implements OnInit, OnDestroy { this.actions = this.actionFactoryService.getLibraryActions(this.handleAction.bind(this)); }); - this.messageHub.messages$.pipe(takeUntil(this.onDestory), filter(event => event.event === EVENTS.LibraryModified)).subscribe(event => { + this.messageHub.messages$.pipe(takeUntil(this.onDestroy), filter(event => event.event === EVENTS.LibraryModified)).subscribe(event => { this.libraryService.getLibrariesForMember().pipe(take(1)).subscribe((libraries: Library[]) => { this.libraries = libraries; }); }); + + this.router.events + .pipe(filter(event => event instanceof NavigationEnd), + takeUntil(this.onDestroy), + map(evt => evt as NavigationEnd)) + .subscribe((evt: NavigationEnd) => { + if (this.utilityService.getActiveBreakpoint() < Breakpoint.Desktop) { + // collapse side nav + this.navService.toggleSideNav(); + } + }); } ngOnDestroy(): void { - this.onDestory.next(); - this.onDestory.complete(); + this.onDestroy.next(); + this.onDestroy.complete(); } handleAction(action: Action, library: Library) { diff --git a/UI/Web/src/app/user-settings/series-bookmarks/series-bookmarks.component.html b/UI/Web/src/app/user-settings/series-bookmarks/series-bookmarks.component.html deleted file mode 100644 index 53bc85356..000000000 --- a/UI/Web/src/app/user-settings/series-bookmarks/series-bookmarks.component.html +++ /dev/null @@ -1,42 +0,0 @@ -

    - There are no bookmarks. Try creating one . -

    -
      -
    • -
      -

      - {{series.name | titlecase}} -  {{getBookmarkPages(series.id)}} -
      - - - -
      -

      -
      -
    • -
    • -
      - -
      -
    • -
    - diff --git a/UI/Web/src/app/user-settings/series-bookmarks/series-bookmarks.component.scss b/UI/Web/src/app/user-settings/series-bookmarks/series-bookmarks.component.scss deleted file mode 100644 index e69de29bb..000000000 diff --git a/UI/Web/src/app/user-settings/series-bookmarks/series-bookmarks.component.ts b/UI/Web/src/app/user-settings/series-bookmarks/series-bookmarks.component.ts deleted file mode 100644 index 4f67fdabe..000000000 --- a/UI/Web/src/app/user-settings/series-bookmarks/series-bookmarks.component.ts +++ /dev/null @@ -1,96 +0,0 @@ -import { Component, OnInit } from '@angular/core'; -import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; -import { ToastrService } from 'ngx-toastr'; -import { take, takeWhile, finalize } from 'rxjs/operators'; -import { BookmarksModalComponent } from 'src/app/cards/_modals/bookmarks-modal/bookmarks-modal.component'; -import { ConfirmService } from 'src/app/shared/confirm.service'; -import { DownloadService } from 'src/app/shared/_services/download.service'; -import { PageBookmark } from 'src/app/_models/page-bookmark'; -import { Series } from 'src/app/_models/series'; -import { ReaderService } from 'src/app/_services/reader.service'; -import { SeriesService } from 'src/app/_services/series.service'; - -@Component({ - selector: 'app-series-bookmarks', - templateUrl: './series-bookmarks.component.html', - styleUrls: ['./series-bookmarks.component.scss'] -}) -export class SeriesBookmarksComponent implements OnInit { - - bookmarks: Array = []; - series: Array = []; - loadingBookmarks: boolean = false; - seriesIds: {[id: number]: number} = {}; - downloadingSeries: {[id: number]: boolean} = {}; - clearingSeries: {[id: number]: boolean} = {}; - - constructor(private readerService: ReaderService, private seriesService: SeriesService, - private modalService: NgbModal, private downloadService: DownloadService, private toastr: ToastrService, - private confirmService: ConfirmService) { } - - ngOnInit(): void { - this.loadBookmarks(); - } - - loadBookmarks() { - this.loadingBookmarks = true; - this.readerService.getAllBookmarks().pipe(take(1)).subscribe(bookmarks => { - this.bookmarks = bookmarks; - this.seriesIds = {}; - this.bookmarks.forEach(bmk => { - if (!this.seriesIds.hasOwnProperty(bmk.seriesId)) { - this.seriesIds[bmk.seriesId] = 1; - } else { - this.seriesIds[bmk.seriesId] += 1; - } - this.downloadingSeries[bmk.seriesId] = false; - this.clearingSeries[bmk.seriesId] = false; - }); - - const ids = Object.keys(this.seriesIds).map(k => parseInt(k, 10)); - this.seriesService.getAllSeriesByIds(ids).subscribe(series => { - this.series = series; - this.loadingBookmarks = false; - }); - }); - } - - viewBookmarks(series: Series) { - const bookmarkModalRef = this.modalService.open(BookmarksModalComponent, { scrollable: true, size: 'lg' }); - bookmarkModalRef.componentInstance.series = series; - bookmarkModalRef.closed.pipe(take(1)).subscribe(() => { - this.loadBookmarks(); - }); - } - - async clearBookmarks(series: Series) { - if (!await this.confirmService.confirm('Are you sure you want to clear all bookmarks for ' + series.name + '? This cannot be undone.')) { - return; - } - - this.clearingSeries[series.id] = true; - this.readerService.clearBookmarks(series.id).subscribe(() => { - const index = this.series.indexOf(series); - if (index > -1) { - this.series.splice(index, 1); - } - this.clearingSeries[series.id] = false; - this.toastr.success(series.name + '\'s bookmarks have been removed'); - }); - } - - getBookmarkPages(seriesId: number) { - return this.seriesIds[seriesId]; - } - - downloadBookmarks(series: Series) { - this.downloadingSeries[series.id] = true; - this.downloadService.downloadBookmarks(this.bookmarks.filter(bmk => bmk.seriesId === series.id)).pipe( - takeWhile(val => { - return val.state != 'DONE'; - }), - finalize(() => { - this.downloadingSeries[series.id] = false; - })).subscribe(() => {/* No Operation */}); - } -} diff --git a/UI/Web/src/app/user-settings/user-preferences/user-preferences.component.html b/UI/Web/src/app/user-settings/user-preferences/user-preferences.component.html index 8c8e9f8fa..cf99950c1 100644 --- a/UI/Web/src/app/user-settings/user-preferences/user-preferences.component.html +++ b/UI/Web/src/app/user-settings/user-preferences/user-preferences.component.html @@ -192,9 +192,6 @@ - - -

    Change your Password

    diff --git a/UI/Web/src/app/user-settings/user-preferences/user-preferences.component.ts b/UI/Web/src/app/user-settings/user-preferences/user-preferences.component.ts index d5a0cbfd9..07ff48b7b 100644 --- a/UI/Web/src/app/user-settings/user-preferences/user-preferences.component.ts +++ b/UI/Web/src/app/user-settings/user-preferences/user-preferences.component.ts @@ -56,7 +56,6 @@ export class UserPreferencesComponent implements OnInit, OnDestroy { tabs: Array<{title: string, fragment: string}> = [ {title: 'Preferences', fragment: ''}, - {title: 'Bookmarks', fragment: 'bookmarks'}, {title: 'Password', fragment: 'password'}, {title: '3rd Party Clients', fragment: 'clients'}, {title: 'Theme', fragment: 'theme'}, diff --git a/UI/Web/src/app/user-settings/user-settings.module.ts b/UI/Web/src/app/user-settings/user-settings.module.ts index c2e9c940b..7638080fa 100644 --- a/UI/Web/src/app/user-settings/user-settings.module.ts +++ b/UI/Web/src/app/user-settings/user-settings.module.ts @@ -1,6 +1,5 @@ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; -import { SeriesBookmarksComponent } from './series-bookmarks/series-bookmarks.component'; import { UserPreferencesComponent } from './user-preferences/user-preferences.component'; import { NgbAccordionModule, NgbNavModule, NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap'; import { ReactiveFormsModule } from '@angular/forms'; @@ -17,7 +16,6 @@ import { ColorPickerModule } from 'ngx-color-picker'; @NgModule({ declarations: [ - SeriesBookmarksComponent, UserPreferencesComponent, ApiKeyComponent, ThemeManagerComponent,