Bulk actions and nicer behaviour with implicit profiles

This commit is contained in:
Amelia 2025-05-29 22:29:18 +02:00
parent 9b4a4b8a50
commit 483c90904d
No known key found for this signature in database
GPG key ID: D6D0ECE365407EAA
18 changed files with 481 additions and 113 deletions

View file

@ -122,6 +122,10 @@ export enum Action {
* Merge two (or more?) entities
*/
Merge = 29,
/**
* Add to a reading profile
*/
AddToReadingProfile = 30,
}
/**
@ -529,6 +533,16 @@ export class ActionFactoryService {
requiredRoles: [],
children: [],
},
{
action: Action.AddToReadingProfile,
title: 'add-to-reading-profile',
description: 'add-to-reading-profile-tooltip',
callback: this.dummyCallback,
shouldRender: this.dummyShouldRender,
requiresAdmin: false,
requiredRoles: [],
children: [],
},
],
},
{

View file

@ -31,6 +31,9 @@ import {ChapterService} from "./chapter.service";
import {VolumeService} from "./volume.service";
import {DefaultModalOptions} from "../_models/default-modal-options";
import {MatchSeriesModalComponent} from "../_single-module/match-series-modal/match-series-modal.component";
import {
BulkAddToReadingProfileComponent
} from "../cards/_modals/bulk-add-to-reading-profile/bulk-add-to-reading-profile.component";
export type LibraryActionCallback = (library: Partial<Library>) => void;
@ -813,4 +816,30 @@ export class ActionService {
});
}
/**
* Adds series to a reading list
* @param series
* @param callback
*/
addMultipleToReadingProfile(series: Array<Series>, callback?: BooleanActionCallback) {
if (this.readingListModalRef != null) { return; }
this.readingListModalRef = this.modalService.open(BulkAddToReadingProfileComponent, { scrollable: true, size: 'md', fullscreen: 'md' });
this.readingListModalRef.componentInstance.seriesIds = series.map(s => s.id)
this.readingListModalRef.componentInstance.title = "hi"
this.readingListModalRef.closed.pipe(take(1)).subscribe(() => {
this.readingListModalRef = null;
if (callback) {
callback(true);
}
});
this.readingListModalRef.dismissed.pipe(take(1)).subscribe(() => {
this.readingListModalRef = null;
if (callback) {
callback(false);
}
});
}
}

View file

@ -16,10 +16,7 @@ export class ReadingProfileService {
return this.httpClient.get<ReadingProfile>(this.baseUrl + "ReadingProfile/"+seriesId);
}
updateProfile(profile: ReadingProfile, seriesId?: number) {
if (seriesId) {
return this.httpClient.post(this.baseUrl + "ReadingProfile?seriesCtx="+seriesId, profile);
}
updateProfile(profile: ReadingProfile) {
return this.httpClient.post(this.baseUrl + "ReadingProfile", profile);
}
@ -59,4 +56,8 @@ export class ReadingProfileService {
return this.httpClient.delete(this.baseUrl + `ReadingProfile/library/${libraryId}?profileId=${id}`, {});
}
batchAddToSeries(id: number, seriesIds: number[]) {
return this.httpClient.post(this.baseUrl + `ReadingProfile/batch?profileId=${id}`, seriesIds);
}
}

View file

@ -385,7 +385,7 @@ export class ReaderSettingsComponent implements OnInit {
}
savePref() {
this.readingProfileService.updateProfile(this.packReadingProfile(), this.seriesId).subscribe()
this.readingProfileService.updateProfile(this.packReadingProfile()).subscribe()
}
private packReadingProfile(): ReadingProfile {

View file

@ -0,0 +1,43 @@
<ng-container *transloco="let t; prefix: 'bulk-add-to-reading-profile'">
<div class="modal-header">
<h4 class="modal-title" id="modal-basic-title">{{t('title')}}</h4>
<button type="button" class="btn-close" [attr.aria-label]="t('close')" (click)="close()"></button>
</div>
<form style="width: 100%" [formGroup]="profileForm">
<div class="modal-body">
@if (profiles.length >= MaxItems) {
<div class="mb-3">
<label for="filter" class="form-label">{{t('filter-label')}}</label>
<div class="input-group">
<input id="filter" autocomplete="off" class="form-control" formControlName="filterQuery" type="text" aria-describedby="reset-input">
<button class="btn btn-outline-secondary" type="button" id="reset-input" (click)="clear()">Clear</button>
</div>
</div>
}
<ul class="list-group">
@for(profile of profiles | filter: filterList; let i = $index; track profile.name) {
<li class="list-group-item clickable" tabindex="0" role="option" (click)="addToProfile(profile)">
{{profile.name}}
</li>
}
@if (profiles.length === 0 && !loading) {
<li class="list-group-item">{{t('no-data')}}</li>
}
@if (loading) {
<li class="list-group-item">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">{{t('loading')}}</span>
</div>
</li>
}
</ul>
</div>
</form>
</ng-container>

View file

@ -0,0 +1,3 @@
.clickable:hover, .clickable:focus {
background-color: var(--list-group-hover-bg-color, --primary-color);
}

View file

@ -0,0 +1,81 @@
import {AfterViewInit, ChangeDetectorRef, Component, ElementRef, inject, Input, OnInit, ViewChild} from '@angular/core';
import {NgbActiveModal} from "@ng-bootstrap/ng-bootstrap";
import {ToastrService} from "ngx-toastr";
import {FormControl, FormGroup, ReactiveFormsModule} from "@angular/forms";
import {translate, TranslocoDirective} from "@jsverse/transloco";
import {ReadingList} from "../../../_models/reading-list";
import {ReadingProfileService} from "../../../_services/reading-profile.service";
import {ReadingProfile} from "../../../_models/preferences/reading-profiles";
import {FilterPipe} from "../../../_pipes/filter.pipe";
@Component({
selector: 'app-bulk-add-to-reading-profile',
imports: [
ReactiveFormsModule,
FilterPipe,
TranslocoDirective
],
templateUrl: './bulk-add-to-reading-profile.component.html',
styleUrl: './bulk-add-to-reading-profile.component.scss'
})
export class BulkAddToReadingProfileComponent implements OnInit, AfterViewInit {
private readonly modal = inject(NgbActiveModal);
private readonly readingProfileService = inject(ReadingProfileService);
private readonly toastr = inject(ToastrService);
private readonly cdRef = inject(ChangeDetectorRef);
protected readonly MaxItems = 8;
@Input({required: true}) title!: string;
/**
* Series Ids to add to Collection Tag
*/
@Input() seriesIds: Array<number> = [];
@ViewChild('title') inputElem!: ElementRef<HTMLInputElement>;
profiles: Array<ReadingProfile> = [];
loading: boolean = false;
profileForm: FormGroup = new FormGroup({});
ngOnInit(): void {
this.profileForm.addControl('title', new FormControl(this.title, []));
this.profileForm.addControl('filterQuery', new FormControl('', []));
this.loading = true;
this.cdRef.markForCheck();
this.readingProfileService.all().subscribe(profiles => {
this.profiles = profiles;
this.loading = false;
this.cdRef.markForCheck();
});
}
ngAfterViewInit() {
// Shift focus to input
if (this.inputElem) {
this.inputElem.nativeElement.select();
this.cdRef.markForCheck();
}
}
close() {
this.modal.close();
}
addToProfile(profile: ReadingProfile) {
if (this.seriesIds.length === 0) return;
this.readingProfileService.batchAddToSeries(profile.id, this.seriesIds).subscribe(() => {
this.toastr.success(translate('toasts.series-added-to-reading-profile', {name: profile.name}));
this.modal.close();
});
}
filterList = (listItem: ReadingProfile) => {
return listItem.name.toLowerCase().indexOf((this.profileForm.value.filterQuery || '').toLowerCase()) >= 0;
}
clear() {
this.profileForm.get('filterQuery')?.setValue('');
}
}

View file

@ -144,7 +144,7 @@ export class BulkSelectionService {
*/
getActions(callback: (action: ActionItem<any>, data: any) => void) {
const allowedActions = [Action.AddToReadingList, Action.MarkAsRead, Action.MarkAsUnread, Action.AddToCollection,
Action.Delete, Action.AddToWantToReadList, Action.RemoveFromWantToReadList];
Action.Delete, Action.AddToWantToReadList, Action.RemoveFromWantToReadList, Action.AddToReadingProfile];
if (Object.keys(this.selectedCards).filter(item => item === 'series').length > 0) {
return this.applyFilterToList(this.actionFactory.getSeriesActions(callback), allowedActions);

View file

@ -276,6 +276,9 @@ export class SeriesCardComponent implements OnInit, OnChanges {
case Action.Download:
this.downloadService.download('series', this.series);
break;
case Action.AddToReadingProfile:
this.actionService.addMultipleToReadingProfile([this.series]);
break;
default:
break;
}

View file

@ -149,6 +149,14 @@ export class LibraryDetailComponent implements OnInit {
this.loadPage();
});
break;
case Action.AddToReadingProfile:
this.actionService.addMultipleToReadingProfile(selectedSeries, (success) => {
this.bulkLoader = false;
this.cdRef.markForCheck();
if (!success) return;
this.bulkSelectionService.deselectAll();
this.loadPage();
})
}
}

View file

@ -142,7 +142,7 @@
}
</div>
@if (menuOpen) {
@if (menuOpen && readingProfile !== null) {
<div class="fixed-bottom overlay" [@slideFromBottom]="menuOpen">
@if (pageOptions !== undefined && pageOptions.ceil !== undefined) {
<div class="mb-3">

View file

@ -501,14 +501,15 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
forkJoin([
this.accountService.currentUser$.pipe(take(1)),
this.readingProfileService.getForSeries(this.seriesId)])
.subscribe(([user, profile]) => {
this.readingProfileService.getForSeries(this.seriesId)
]).subscribe(([user, profile]) => {
if (!user) {
this.router.navigateByUrl('/login');
return;
}
this.readingProfile = profile;
if (!this.readingProfile) return; // type hints
this.user = user;
this.hasBookmarkRights = this.accountService.hasBookmarkRole(user) || this.accountService.hasAdminRole(user);
@ -533,7 +534,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
});
// Update implicit reading profile while changing settings
this.generalSettingsForm.valueChanges.pipe(
/*this.generalSettingsForm.valueChanges.pipe(
debounceTime(300),
distinctUntilChanged(),
takeUntilDestroyed(this.destroyRef),
@ -544,7 +545,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
}
})
})
).subscribe();
).subscribe();*/
this.readerModeSubject.next(this.readerMode);
@ -1754,7 +1755,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
// menu only code
savePref() {
this.readingProfileService.updateProfile(this.packReadingProfile(), this.seriesId).subscribe(_ => {
this.readingProfileService.updateProfile(this.packReadingProfile()).subscribe(_ => {
this.toastr.success(translate('manga-reader.user-preferences-updated'));
})
}
@ -1775,8 +1776,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
data.emulateBook = modelSettings.emulateBook;
data.swipeToPaginate = modelSettings.swipeToPaginate;
data.pageSplitOption = parseInt(modelSettings.pageSplitOption, 10);
// TODO: Check if this saves correctly!
data.widthOverride = modelSettings.widthSlider === 'none' ? null : modelSettings.widthOverride;
data.widthOverride = modelSettings.widthSlider === 'none' ? null : modelSettings.widthSlider;
return data;
}

View file

@ -123,7 +123,6 @@ export class ManageReadingProfilesComponent implements OnInit {
this.accountService.currentUser$.pipe(take(1)).subscribe(user => {
if (user) {
this.user = user;
console.log(this.user.preferences.defaultReadingProfileId);
}
});
@ -247,6 +246,8 @@ export class ManageReadingProfilesComponent implements OnInit {
private packData(): ReadingProfile {
const data: ReadingProfile = this.readingProfileForm!.getRawValue();
data.id = this.selectedProfile!.id;
// Hack around readerMode being sent as a string otherwise
data.readerMode = parseInt(data.readerMode as unknown as string);
return data;
}

View file

@ -1266,6 +1266,16 @@
"create": "{{common.create}}"
},
"bulk-add-to-reading-profile": {
"title": "Add to Reading profile",
"close": "{{common.close}}",
"filter-label": "{{common.filter}}",
"clear": "{{common.clear}}",
"no-data": "No collections created yet",
"loading": "{{common.loading}}",
"create": "{{common.create}}"
},
"entity-title": {
"special": "Special",
"issue-num": "{{common.issue-hash-num}}",
@ -2650,7 +2660,8 @@
"bulk-delete-libraries": "Are you sure you want to delete {{count}} libraries?",
"match-success": "Series matched correctly",
"webtoon-override": "Switching to Webtoon mode due to images representing a webtoon.",
"scrobble-gen-init": "Enqueued a job to generate scrobble events from past reading history and ratings, syncing them with connected services."
"scrobble-gen-init": "Enqueued a job to generate scrobble events from past reading history and ratings, syncing them with connected services.",
"series-added-to-reading-profile": "Series added to Reading Profile {{name}}"
},
"read-time-pipe": {
@ -2703,6 +2714,7 @@
"remove-from-want-to-read-tooltip": "Remove series from Want to Read",
"remove-from-on-deck": "Remove From On Deck",
"remove-from-on-deck-tooltip": "Remove series from showing from On Deck",
"add-to-reading-profile": "Add to Reading Profile",
"others": "Others",
"add-to-reading-list": "Add to Reading List",