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:
Joseph Milazzo 2022-04-23 13:58:14 -05:00 committed by GitHub
parent 62715a9977
commit 9d6843614d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
48 changed files with 648 additions and 634 deletions

View 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 { }

View 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 { }

View 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&nbsp;<i class="fa fa-external-link-alt" aria-hidden="true"></i></a>.
</ng-template>
</app-card-detail-layout>

View 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 */});
}
}