Readable Bookmarks (#1228)
* Moved bookmarks to it's own page on side nav and integrated actions. * Implemented the ability to read bookmarks in the manga reader. * Removed old bookmark components that aren't needed any longer. * Removed recently added component as we use all-series instead now * Removed bookmark tab from card detail * Fixed scroll to top not working and being missing * When opening the side nav on mobile with metadata filter already open, collapse the filter. * When on mobile viewports, when clicking an item from side nav, collapse it afterwards * Converted most of series detail to use the card detail layout, except storyline which has custom logic * Fixed unit test
This commit is contained in:
parent
62715a9977
commit
9d6843614d
48 changed files with 648 additions and 634 deletions
22
UI/Web/src/app/bookmark/bookmark-routing.module.ts
Normal file
22
UI/Web/src/app/bookmark/bookmark-routing.module.ts
Normal file
|
|
@ -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 { }
|
||||
26
UI/Web/src/app/bookmark/bookmark.module.ts
Normal file
26
UI/Web/src/app/bookmark/bookmark.module.ts
Normal file
|
|
@ -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 { }
|
||||
23
UI/Web/src/app/bookmark/bookmarks/bookmarks.component.html
Normal file
23
UI/Web/src/app/bookmark/bookmarks/bookmarks.component.html
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
<app-side-nav-companion-bar [hasFilter]="false">
|
||||
<h2 title>
|
||||
<app-card-actionables [actions]="actions"></app-card-actionables>
|
||||
Bookmarks
|
||||
</h2>
|
||||
<h6 subtitle style="margin-left:40px;">{{series?.length}} Series</h6>
|
||||
</app-side-nav-companion-bar>
|
||||
<app-bulk-operations [actionCallback]="bulkActionCallback"></app-bulk-operations>
|
||||
<app-card-detail-layout
|
||||
[isLoading]="loadingBookmarks"
|
||||
[items]="series">
|
||||
<ng-template #cardItem let-item let-position="idx">
|
||||
<app-card-item [entity]="item" (reload)="loadBookmarks()" [title]="item.name" [imageUrl]="imageService.getSeriesCoverImage(item.id)"
|
||||
[supressArchiveWarning]="true" (clicked)="viewBookmarks(item)" [count]="seriesIds[item.id]" [allowSelection]="true"
|
||||
[actions]="actions"
|
||||
[selected]="bulkSelectionService.isCardSelected('bookmark', position)" (selection)="bulkSelectionService.handleCardSelection('bookmark', position, series.length, $event)"
|
||||
></app-card-item>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #noData>
|
||||
There are no bookmarks. Try creating <a href="https://wiki.kavitareader.com/en/guides/get-started-using-your-library/bookmarks" target="_blank">one <i class="fa fa-external-link-alt" aria-hidden="true"></i></a>.
|
||||
</ng-template>
|
||||
</app-card-detail-layout>
|
||||
167
UI/Web/src/app/bookmark/bookmarks/bookmarks.component.ts
Normal file
167
UI/Web/src/app/bookmark/bookmarks/bookmarks.component.ts
Normal file
|
|
@ -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<PageBookmark> = [];
|
||||
series: Array<Series> = [];
|
||||
loadingBookmarks: boolean = false;
|
||||
seriesIds: {[id: number]: number} = {};
|
||||
downloadingSeries: {[id: number]: boolean} = {};
|
||||
clearingSeries: {[id: number]: boolean} = {};
|
||||
actions: ActionItem<Series>[] = [];
|
||||
|
||||
private onDestroy: Subject<void> = new Subject<void>();
|
||||
|
||||
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 */});
|
||||
}
|
||||
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue