mirror of
https://github.com/Kareadita/Kavita.git
synced 2025-07-09 03:04:19 -04:00
Scrolling Enhancements (#2417)
This commit is contained in:
parent
8875bc3585
commit
f4e8daf983
29
UI/Web/package-lock.json
generated
29
UI/Web/package-lock.json
generated
@ -21,8 +21,6 @@
|
||||
"@fortawesome/fontawesome-free": "^6.4.2",
|
||||
"@iharbeck/ngx-virtual-scroller": "^16.0.0",
|
||||
"@iplab/ngx-file-upload": "^16.0.2",
|
||||
"@lithiumjs/angular": "^7.3.0",
|
||||
"@lithiumjs/ngx-virtual-scroll": "^0.3.0",
|
||||
"@microsoft/signalr": "^7.0.12",
|
||||
"@ng-bootstrap/ng-bootstrap": "^15.1.2",
|
||||
"@ngneat/transloco": "^6.0.0",
|
||||
@ -3716,33 +3714,6 @@
|
||||
"integrity": "sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@lithiumjs/angular": {
|
||||
"version": "7.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@lithiumjs/angular/-/angular-7.3.0.tgz",
|
||||
"integrity": "sha512-81nXyT9I2J+VpeFEDtOvfP4imlrLueoqFYBZR8PCrlY9cjDzgFAZBq7mCOLxOhi0xL5wF9hM0iDqlmI9LDct1Q==",
|
||||
"dependencies": {
|
||||
"tslib": "^2.3.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@angular/core": ">=11.0.0 <17.0.0",
|
||||
"rxjs": ">=7.x.x",
|
||||
"typescript": ">=4.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@lithiumjs/ngx-virtual-scroll": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@lithiumjs/ngx-virtual-scroll/-/ngx-virtual-scroll-0.3.0.tgz",
|
||||
"integrity": "sha512-fYZR1S66c4ATg6mDVwJaZxsZ8rT/jcJ07b95x5sZVV7gtiDv7DDUCiMa4mtvj71fqMzcoeRe99G0FivVUwvZ0Q==",
|
||||
"dependencies": {
|
||||
"tslib": "^2.3.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@angular/common": "8.x.x - 16.x.x",
|
||||
"@angular/core": "8.x.x - 16.x.x",
|
||||
"@lithiumjs/angular": ">=7.0.0",
|
||||
"rxjs": "6.x.x - 7.x.x"
|
||||
}
|
||||
},
|
||||
"node_modules/@microsoft/signalr": {
|
||||
"version": "7.0.12",
|
||||
"resolved": "https://registry.npmjs.org/@microsoft/signalr/-/signalr-7.0.12.tgz",
|
||||
|
@ -26,8 +26,6 @@
|
||||
"@fortawesome/fontawesome-free": "^6.4.2",
|
||||
"@iharbeck/ngx-virtual-scroller": "^16.0.0",
|
||||
"@iplab/ngx-file-upload": "^16.0.2",
|
||||
"@lithiumjs/angular": "^7.3.0",
|
||||
"@lithiumjs/ngx-virtual-scroll": "^0.3.0",
|
||||
"@microsoft/signalr": "^7.0.12",
|
||||
"@ng-bootstrap/ng-bootstrap": "^15.1.2",
|
||||
"@ngneat/transloco": "^6.0.0",
|
||||
|
@ -10,7 +10,7 @@ export interface Library {
|
||||
lastScanned: string;
|
||||
type: LibraryType;
|
||||
folders: string[];
|
||||
coverImage?: string;
|
||||
coverImage?: string | null;
|
||||
folderWatching: boolean;
|
||||
includeInDashboard: boolean;
|
||||
includeInRecommended: boolean;
|
||||
|
@ -24,7 +24,7 @@
|
||||
<ng-container [ngTemplateOutlet]="noDataTemplate"></ng-container>
|
||||
</p>
|
||||
|
||||
<virtual-scroller [ngClass]="{'empty': items.length === 0 && !isLoading}" #scroll [items]="items" [bufferAmount]="bufferAmount" [parentScroll]="parentScroll" >
|
||||
<virtual-scroller [ngClass]="{'empty': items.length === 0 && !isLoading}" #scroll [items]="items" [bufferAmount]="bufferAmount" [parentScroll]="parentScroll">
|
||||
<div class="grid row g-0" #container>
|
||||
<div class="card col-auto mt-2 mb-2"
|
||||
(click)="tryToSaveJumpKey(item)"
|
||||
@ -61,7 +61,7 @@
|
||||
<ng-template #jumpBar>
|
||||
<div class="jump-bar">
|
||||
<ng-container *ngFor="let jumpKey of jumpBarKeysToRender; let i = index;">
|
||||
<button class="btn btn-link" [ngClass]="{'disabled': hasCustomSort()}" (click)="scrollTo(jumpKey)" [ngbTooltip]="jumpKey.size + ' Series'" placement="left">
|
||||
<button class="btn btn-link" [ngClass]="{'disabled': hasCustomSort()}" (click)="scrollTo(jumpKey)" [ngbTooltip]="t('jumpkey-count', {count: jumpKey.size})" placement="left">
|
||||
{{jumpKey.title}}
|
||||
</button>
|
||||
</ng-container>
|
||||
|
@ -39,6 +39,9 @@ import {TranslocoDirective} from "@ngneat/transloco";
|
||||
import {CardActionablesComponent} from "../../_single-module/card-actionables/card-actionables.component";
|
||||
import {SeriesFilterV2} from "../../_models/metadata/v2/series-filter-v2";
|
||||
|
||||
|
||||
const ANIMATION_TIME_MS = 0;
|
||||
|
||||
@Component({
|
||||
selector: 'app-card-detail-layout',
|
||||
standalone: true,
|
||||
@ -49,6 +52,13 @@ import {SeriesFilterV2} from "../../_models/metadata/v2/series-filter-v2";
|
||||
})
|
||||
export class CardDetailLayoutComponent implements OnInit, OnChanges {
|
||||
|
||||
private readonly filterUtilityService = inject(FilterUtilitiesService);
|
||||
protected readonly utilityService = inject(UtilityService);
|
||||
private readonly cdRef = inject(ChangeDetectorRef);
|
||||
private readonly jumpbarService = inject(JumpbarService);
|
||||
private readonly router = inject(Router);
|
||||
private readonly scrollService = inject(ScrollService);
|
||||
|
||||
@Input() header: string = '';
|
||||
@Input() isLoading: boolean = false;
|
||||
@Input() items: any[] = [];
|
||||
@ -89,7 +99,6 @@ export class CardDetailLayoutComponent implements OnInit, OnChanges {
|
||||
|
||||
@ViewChild(VirtualScrollerComponent) private virtualScroller!: VirtualScrollerComponent;
|
||||
|
||||
private readonly filterUtilityService = inject(FilterUtilitiesService);
|
||||
filter: SeriesFilterV2 = this.filterUtilityService.createSeriesV2Filter();
|
||||
libraries: Array<FilterItem<Library>> = [];
|
||||
|
||||
@ -97,15 +106,9 @@ export class CardDetailLayoutComponent implements OnInit, OnChanges {
|
||||
hasResumedJumpKey: boolean = false;
|
||||
bufferAmount: number = 1;
|
||||
|
||||
protected readonly Breakpoint = Breakpoint;
|
||||
|
||||
get Breakpoint() {
|
||||
return Breakpoint;
|
||||
}
|
||||
|
||||
constructor(public utilityService: UtilityService,
|
||||
@Inject(DOCUMENT) private document: Document, private cdRef: ChangeDetectorRef,
|
||||
private jumpbarService: JumpbarService, private router: Router, private scrollService: ScrollService) {
|
||||
}
|
||||
constructor(@Inject(DOCUMENT) private document: Document) {}
|
||||
|
||||
@HostListener('window:resize', ['$event'])
|
||||
@HostListener('window:orientationchange', ['$event'])
|
||||
@ -136,6 +139,8 @@ export class CardDetailLayoutComponent implements OnInit, OnChanges {
|
||||
this.virtualScroller.refresh();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
@ -143,6 +148,8 @@ export class CardDetailLayoutComponent implements OnInit, OnChanges {
|
||||
this.jumpBarKeysToRender = [...this.jumpBarKeys];
|
||||
this.resizeJumpBar();
|
||||
|
||||
// TODO: I wish I had signals so I can tap into when isLoading is false and trigger the scroll code
|
||||
|
||||
// Don't resume jump key when there is a custom sort order, as it won't work
|
||||
if (!this.hasCustomSort()) {
|
||||
if (!this.hasResumedJumpKey && this.jumpBarKeysToRender.length > 0) {
|
||||
@ -154,14 +161,15 @@ export class CardDetailLayoutComponent implements OnInit, OnChanges {
|
||||
this.hasResumedJumpKey = true;
|
||||
setTimeout(() => this.scrollTo(keys[0]), 100);
|
||||
}
|
||||
} else {
|
||||
// I will come back and refactor this to work
|
||||
// const scrollPosition = this.jumpbarService.getResumePosition(this.router.url);
|
||||
// console.log('scroll position: ', scrollPosition);
|
||||
// if (scrollPosition > 0) {
|
||||
// setTimeout(() => this.virtualScroller.scrollToIndex(scrollPosition, true, 0, 1000), 100);
|
||||
// }
|
||||
}
|
||||
// else {
|
||||
// // I will come back and refactor this to work
|
||||
// // const scrollPosition = this.jumpbarService.getResumePosition(this.router.url);
|
||||
// // console.log('scroll position: ', scrollPosition);
|
||||
// // if (scrollPosition > 0) {
|
||||
// // setTimeout(() => this.virtualScroller.scrollToIndex(scrollPosition, true, 0, 1000), 100);
|
||||
// // }
|
||||
// }
|
||||
}
|
||||
|
||||
hasCustomSort() {
|
||||
@ -192,10 +200,11 @@ export class CardDetailLayoutComponent implements OnInit, OnChanges {
|
||||
targetIndex += this.jumpBarKeys[i].size;
|
||||
}
|
||||
|
||||
this.virtualScroller.scrollToIndex(targetIndex, true, 0, 1000);
|
||||
this.virtualScroller.scrollToIndex(targetIndex, true, 0, ANIMATION_TIME_MS);
|
||||
this.jumpbarService.saveResumeKey(this.router.url, jumpKey.key);
|
||||
// TODO: This doesn't work, we need the offset from virtual scroller
|
||||
this.jumpbarService.saveScrollOffset(this.router.url, this.scrollService.scrollPosition);
|
||||
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
|
@ -243,7 +243,6 @@ export class CollectionDetailComponent implements OnInit, AfterContentChecked {
|
||||
this.pagination = series.pagination;
|
||||
this.jumpbarKeys = this.jumpbarService.getJumpKeys(this.series, (series: Series) => series.name);
|
||||
this.isLoading = false;
|
||||
window.scrollTo(0, 0);
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
}
|
||||
|
@ -281,10 +281,8 @@ export class LibraryDetailComponent implements OnInit {
|
||||
this.pagination = series.pagination;
|
||||
this.loadingSeries = false;
|
||||
this.cdRef.markForCheck();
|
||||
window.scrollTo(0, 0);
|
||||
});
|
||||
}
|
||||
|
||||
trackByIdentity = (index: number, item: Series) => `${item.id}_${item.name}_${item.localizedName}_${item.pagesRead}`;
|
||||
protected readonly undefined = undefined;
|
||||
}
|
||||
|
@ -18,7 +18,6 @@ import {BulkSelectionService} from "../../../cards/bulk-selection.service";
|
||||
import {SeriesCardComponent} from "../../../cards/series-card/series-card.component";
|
||||
import {FormsModule} from "@angular/forms";
|
||||
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
|
||||
import {NgxVirtualScrollModule} from "@lithiumjs/ngx-virtual-scroll";
|
||||
|
||||
export interface IndexUpdateEvent {
|
||||
fromPosition: number;
|
||||
@ -39,8 +38,7 @@ export interface ItemRemoveEvent {
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: true,
|
||||
imports: [NgIf, VirtualScrollerModule, NgFor, NgTemplateOutlet, CdkDropList, CdkDrag,
|
||||
CdkDragHandle, TranslocoDirective, NgClass, SeriesCardComponent, FormsModule,
|
||||
NgxVirtualScrollModule, NgxVirtualScrollModule]
|
||||
CdkDragHandle, TranslocoDirective, NgClass, SeriesCardComponent, FormsModule]
|
||||
})
|
||||
export class DraggableOrderedListComponent {
|
||||
|
||||
|
@ -115,7 +115,6 @@ export class ReadingListsComponent implements OnInit {
|
||||
this.loadingLists = false;
|
||||
this.actions = {};
|
||||
this.lists.forEach(l => this.actions[l.id] = this.getActions(l));
|
||||
window.scrollTo(0, 0);
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
}
|
||||
|
@ -777,8 +777,8 @@ export class SeriesDetailComponent implements OnInit, AfterContentChecked {
|
||||
const modalRef = this.modalService.open(EditSeriesModalComponent, { size: 'xl' });
|
||||
modalRef.componentInstance.series = this.series;
|
||||
modalRef.closed.subscribe((closeResult: {success: boolean, series: Series, coverImageUpdate: boolean}) => {
|
||||
window.scrollTo(0, 0);
|
||||
if (closeResult.success) {
|
||||
window.scrollTo(0, 0);
|
||||
this.loadSeries(this.seriesId);
|
||||
}
|
||||
|
||||
|
@ -271,13 +271,20 @@ export class DownloadService {
|
||||
}
|
||||
|
||||
private save(blob: Blob, filename: string) {
|
||||
const saveLink = document.createElement( 'a' );
|
||||
if (saveLink.href) {
|
||||
URL.revokeObjectURL(saveLink.href);
|
||||
}
|
||||
saveLink.href = URL.createObjectURL(blob);
|
||||
const saveLink = document.createElement('a');
|
||||
saveLink.style.display = 'none';
|
||||
document.body.appendChild(saveLink);
|
||||
|
||||
const url = URL.createObjectURL(blob);
|
||||
saveLink.href = url;
|
||||
saveLink.download = filename;
|
||||
saveLink.dispatchEvent( new MouseEvent( 'click' ) );
|
||||
|
||||
// Trigger the click event
|
||||
saveLink.click();
|
||||
|
||||
// Cleanup
|
||||
URL.revokeObjectURL(url);
|
||||
document.body.removeChild(saveLink);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -80,7 +80,7 @@
|
||||
<p *ngIf="isAddLibrary" class="alert alert-secondary" role="alert">{{t('cover-description')}}</p>
|
||||
<p>{{t('cover-description-extra')}}</p>
|
||||
<app-cover-image-chooser [(imageUrls)]="imageUrls" (imageSelected)="updateCoverImageIndex($event)"
|
||||
(selectedBase64Url)="applyCoverImage($event)" [showReset]="library.coverImage !== undefined "
|
||||
(selectedBase64Url)="applyCoverImage($event)" [showReset]="library.coverImage !== null"
|
||||
(resetClicked)="resetCoverImage()"></app-cover-image-chooser>
|
||||
</ng-template>
|
||||
</li>
|
||||
|
@ -55,9 +55,10 @@ enum StepID {
|
||||
})
|
||||
export class LibrarySettingsModalComponent implements OnInit {
|
||||
|
||||
@Input({required: true}) library!: Library;
|
||||
private readonly destroyRef = inject(DestroyRef);
|
||||
|
||||
@Input({required: true}) library!: Library;
|
||||
|
||||
active = TabID.General;
|
||||
imageUrls: Array<string> = [];
|
||||
|
||||
@ -81,8 +82,8 @@ export class LibrarySettingsModalComponent implements OnInit {
|
||||
isAddLibrary = false;
|
||||
setupStep = StepID.General;
|
||||
|
||||
get Breakpoint() { return Breakpoint; }
|
||||
get TabID() { return TabID; }
|
||||
protected readonly Breakpoint = Breakpoint;
|
||||
protected readonly TabID = TabID;
|
||||
|
||||
constructor(public utilityService: UtilityService, private uploadService: UploadService, private modalService: NgbModal,
|
||||
private settingService: SettingsService, public modal: NgbActiveModal, private confirmService: ConfirmService,
|
||||
|
@ -176,7 +176,6 @@ export class WantToReadComponent implements OnInit, AfterContentChecked {
|
||||
this.pagination = paginatedList.pagination;
|
||||
this.jumpbarKeys = this.jumpbarService.getJumpKeys(this.series, (series: Series) => series.name);
|
||||
this.isLoading = false;
|
||||
window.scrollTo(0, 0);
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
}
|
||||
|
@ -892,7 +892,8 @@
|
||||
},
|
||||
|
||||
"card-detail-layout": {
|
||||
"total-items": "{{count}} total items"
|
||||
"total-items": "{{count}} total items",
|
||||
"jumpkey-count": "{{count}} Series"
|
||||
},
|
||||
|
||||
"card-item": {
|
||||
|
Loading…
x
Reference in New Issue
Block a user