UX Overhaul Part 1 (#3047)

Co-authored-by: Joseph Milazzo <joseph.v.milazzo@gmail.com>
This commit is contained in:
Robbie Davis 2024-08-09 13:55:31 -04:00 committed by GitHub
parent 5934d516f3
commit ff79710ac6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
324 changed files with 11589 additions and 4598 deletions

View file

@ -6,7 +6,7 @@
</div>
<form style="width: 100%" [formGroup]="listForm">
<div class="modal-body">
@if (lists.length >= 5) {
@if (lists.length >= MaxItems) {
<div class="mb-3">
<label for="filter" class="form-label">{{t('filter-label')}}</label>
<div class="input-group">

View file

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

View file

@ -35,6 +35,7 @@ export class BulkAddToCollectionComponent implements OnInit, AfterViewInit {
private readonly collectionService = inject(CollectionTagService);
private readonly toastr = inject(ToastrService);
private readonly cdRef = inject(ChangeDetectorRef);
protected readonly MaxItems = 8;
@Input({required: true}) title!: string;
/**

View file

@ -14,7 +14,6 @@ import {ToastrService} from 'ngx-toastr';
import {debounceTime, distinctUntilChanged, forkJoin, switchMap, tap} from 'rxjs';
import {ConfirmService} from 'src/app/shared/confirm.service';
import {Breakpoint, UtilityService} from 'src/app/shared/_services/utility.service';
import {SelectionModel} from 'src/app/typeahead/_components/typeahead.component';
import {UserCollection} from 'src/app/_models/collection-tag';
import {Pagination} from 'src/app/_models/pagination';
import {Series} from 'src/app/_models/series';
@ -24,12 +23,11 @@ import {LibraryService} from 'src/app/_services/library.service';
import {SeriesService} from 'src/app/_services/series.service';
import {UploadService} from 'src/app/_services/upload.service';
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
import {CommonModule, DatePipe, DecimalPipe, NgIf, NgTemplateOutlet} from "@angular/common";
import {DatePipe, DecimalPipe, NgIf, NgTemplateOutlet} from "@angular/common";
import {CoverImageChooserComponent} from "../../cover-image-chooser/cover-image-chooser.component";
import {translate, TranslocoDirective} from "@ngneat/transloco";
import {ScrobbleProvider} from "../../../_services/scrobbling.service";
import {FilterPipe} from "../../../_pipes/filter.pipe";
import {ScrobbleError} from "../../../_models/scrobbling/scrobble-error";
import {AccountService} from "../../../_services/account.service";
import {DefaultDatePipe} from "../../../_pipes/default-date.pipe";
import {ReadMoreComponent} from "../../../shared/read-more/read-more.component";
@ -38,6 +36,7 @@ import {SafeUrlPipe} from "../../../_pipes/safe-url.pipe";
import {MangaFormatPipe} from "../../../_pipes/manga-format.pipe";
import {SentenceCasePipe} from "../../../_pipes/sentence-case.pipe";
import {TagBadgeComponent} from "../../../shared/tag-badge/tag-badge.component";
import {SelectionModel} from "../../../typeahead/_models/selection-model";
enum TabID {

View file

@ -1,5 +1,5 @@
.bulk-select {
background-color: var(--navbar-bg-color);
background-color: var(--bulk-background-color);
border-bottom: 2px solid var(--primary-color);
color: var(--bulk-selection-text-color) !important;

View file

@ -36,7 +36,7 @@ export class BulkOperationsComponent implements OnInit {
* Modal mode means don't fix to the top
*/
@Input() modalMode = false;
@Input() topOffset: number = 56;
@Input() topOffset: number = 60;
hasMarkAsRead: boolean = false;
hasMarkAsUnread: boolean = false;
actions: Array<ActionItem<any>> = [];

View file

@ -50,7 +50,7 @@
</ng-template>
</li>
<li [ngbNavItem]="tabs[TabID.Cover]" [disabled]="(isAdmin$ | async) === false">
<li [ngbNavItem]="tabs[TabID.Cover]" [disabled]="(accountService.isAdmin$ | async) === false">
<a ngbNavLink>{{t(tabs[TabID.Cover].title)}}</a>
<ng-template ngbNavContent>
<app-cover-image-chooser [(imageUrls)]="imageUrls" (imageSelected)="updateCoverImageIndex($event)"
@ -59,7 +59,7 @@
</ng-template>
</li>
<li [ngbNavItem]="tabs[TabID.Files]" [disabled]="(isAdmin$ | async) === false">
<li [ngbNavItem]="tabs[TabID.Files]" [disabled]="(accountService.isAdmin$ | async) === false">
<a ngbNavLink>{{t(tabs[TabID.Files].title)}}</a>
<ng-template ngbNavContent>
@if (!utilityService.isChapter(data)) {

View file

@ -17,7 +17,7 @@ import {
NgbNavOutlet
} from '@ng-bootstrap/ng-bootstrap';
import { ToastrService } from 'ngx-toastr';
import { Observable, of, map, shareReplay } from 'rxjs';
import { Observable, of } from 'rxjs';
import { DownloadService } from 'src/app/shared/_services/download.service';
import { Breakpoint, UtilityService } from 'src/app/shared/_services/utility.service';
import {Chapter, LooseLeafOrDefaultNumber} from 'src/app/_models/chapter';
@ -32,9 +32,7 @@ import { ActionService } from 'src/app/_services/action.service';
import { ImageService } from 'src/app/_services/image.service';
import { LibraryService } from 'src/app/_services/library.service';
import { ReaderService } from 'src/app/_services/reader.service';
import { SeriesService } from 'src/app/_services/series.service';
import { UploadService } from 'src/app/_services/upload.service';
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
import {CommonModule} from "@angular/common";
import {EntityTitleComponent} from "../entity-title/entity-title.component";
import {ImageComponent} from "../../shared/image/image.component";
@ -69,6 +67,19 @@ enum TabID {
})
export class CardDetailDrawerComponent implements OnInit {
protected readonly utilityService = inject(UtilityService);
protected readonly imageService = inject(ImageService);
private readonly uploadService = inject(UploadService);
private readonly toastr = inject(ToastrService);
protected readonly accountService = inject(AccountService);
private readonly actionFactoryService = inject(ActionFactoryService);
private readonly actionService = inject(ActionService);
private readonly router = inject(Router);
private readonly libraryService = inject(LibraryService);
private readonly readerService = inject(ReaderService);
protected readonly activeOffcanvas = inject(NgbActiveOffcanvas);
private readonly downloadService = inject(DownloadService);
private readonly cdRef = inject(ChangeDetectorRef);
private readonly destroyRef = inject(DestroyRef);
protected readonly MangaFormat = MangaFormat;
@ -115,22 +126,6 @@ export class CardDetailDrawerComponent implements OnInit {
summary: string = '';
downloadInProgress: boolean = false;
constructor(public utilityService: UtilityService,
public imageService: ImageService, private uploadService: UploadService, private toastr: ToastrService,
private accountService: AccountService, private actionFactoryService: ActionFactoryService,
private actionService: ActionService, private router: Router, private libraryService: LibraryService,
private seriesService: SeriesService, private readerService: ReaderService,
public activeOffcanvas: NgbActiveOffcanvas, private downloadService: DownloadService, private readonly cdRef: ChangeDetectorRef) {
this.isAdmin$ = this.accountService.currentUser$.pipe(
takeUntilDestroyed(this.destroyRef),
map(user => (user && this.accountService.hasAdminRole(user)) || false),
shareReplay()
);
}
ngOnInit(): void {
this.imageUrls = this.chapters.map(c => this.imageService.getChapterCoverImage(c.id));
this.isChapter = this.utilityService.isChapter(this.data);

View file

@ -1,35 +1,42 @@
<ng-container *transloco="let t; read: 'card-detail-layout'">
<div class="row mt-2 g-0 pb-2" *ngIf="header !== undefined && header.length > 0">
<div class="col me-auto">
<h2>
<span *ngIf="actions.length > 0" class="">
<app-card-actionables (actionHandler)="performAction($event)" [actions]="actions" [labelBy]="header"></app-card-actionables>&nbsp;
</span>
<span *ngIf="header !== undefined && header.length > 0">
{{header}}&nbsp;
<span class="badge bg-primary rounded-pill"
[attr.aria-label]="t('total-items', {count: pagination.totalItems})"
*ngIf="pagination !== undefined">{{pagination.totalItems}}</span>
</span>
</h2>
@if (header.length > 0) {
<div class="row mt-2 g-0 pb-2">
<div class="col me-auto">
<h4>
@if (actions.length > 0) {
<span>
<app-card-actionables (actionHandler)="performAction($event)" [actions]="actions" [labelBy]="header"></app-card-actionables>&nbsp;
</span>
}
<span>
{{header}}&nbsp;
@if (pagination !== undefined) {
<span class="badge bg-primary rounded-pill"
[attr.aria-label]="t('total-items', {count: pagination.totalItems})">{{pagination.totalItems}}</span>
}
</span>
</h4>
</div>
</div>
</div>
}
<app-metadata-filter [filterSettings]="filterSettings" [filterOpen]="filterOpen" (applyFilter)="applyMetadataFilter($event)"></app-metadata-filter>
<div class="viewport-container" [ngClass]="{'empty': items.length === 0 && !isLoading}">
<div class="viewport-container ms-1" [ngClass]="{'empty': items.length === 0 && !isLoading}">
<div class="content-container">
<div class="card-container mt-2 mb-2">
<p *ngIf="items.length === 0 && !isLoading">
<ng-container [ngTemplateOutlet]="noDataTemplate"></ng-container>
</p>
@if (items.length === 0 && !isLoading) {
<p><ng-container [ngTemplateOutlet]="noDataTemplate"></ng-container></p>
}
<virtual-scroller [ngClass]="{'empty': items.length === 0 && !isLoading}" #scroll [items]="items" [bufferAmount]="bufferAmount" [parentScroll]="parentScroll">
<div class="grid row g-0" #container>
<!-- TODO: @for (item of scroll.viewPortItems; track trackByIdentity; let i = $index;) { works -->
<div class="card col-auto mt-2 mb-2"
(click)="tryToSaveJumpKey(item)"
*ngFor="let item of scroll.viewPortItems; trackBy:trackByIdentity; index as i" id="jumpbar-index--{{i}}"
(click)="tryToSaveJumpKey()"
*ngFor="let item of scroll.viewPortItems; trackBy:trackByIdentity; index as i"
id="jumpbar-index--{{i}}"
[attr.jumpbar-index]="i">
<ng-container [ngTemplateOutlet]="itemTemplate" [ngTemplateOutletContext]="{ $implicit: item, idx: scroll.viewPortInfo.startIndexWithBuffer + i }"></ng-container>
</div>
@ -38,34 +45,43 @@
</div>
</div>
<ng-container *ngIf="jumpBarKeysToRender.length >= 4 && items.length > 0 && scroll.viewPortInfo.maxScrollPosition > 0" [ngTemplateOutlet]="jumpBar" [ngTemplateOutletContext]="{ id: 'jumpbar' }"></ng-container>
@if (jumpBarKeysToRender.length >= 4 && items.length > 0 && scroll.viewPortInfo.maxScrollPosition > 0) {
<ng-container [ngTemplateOutlet]="jumpBar" [ngTemplateOutletContext]="{ id: 'jumpbar' }"></ng-container>
}
</div>
<ng-template #cardTemplate>
<virtual-scroller #scroll [items]="items" [bufferAmount]="bufferAmount">
<div class="grid row g-0" #container>
<div class="card col-auto mt-2 mb-2" (click)="tryToSaveJumpKey(item)" *ngFor="let item of scroll.viewPortItems; trackBy:trackByIdentity; index as i" id="jumpbar-index--{{i}}" [attr.jumpbar-index]="i">
<div class="card col-auto mt-2 mb-2" *ngFor="let item of scroll.viewPortItems; trackBy:trackByIdentity; index as i" (click)="tryToSaveJumpKey()" id="jumpbar-index--{{i}}" [attr.jumpbar-index]="i">
<ng-container [ngTemplateOutlet]="itemTemplate" [ngTemplateOutletContext]="{ $implicit: item, idx: i }"></ng-container>
</div>
</div>
</virtual-scroller>
<div class="mx-auto" *ngIf="items.length === 0 && !isLoading" style="width: 200px;">
<p>
<ng-container [ngTemplateOutlet]="noDataTemplate"></ng-container>
</p>
</div>
@if (items.length === 0 && !isLoading) {
<div class="mx-auto" style="width: 200px;">
<p>
<ng-container [ngTemplateOutlet]="noDataTemplate"></ng-container>
</p>
</div>
}
</ng-template>
<app-loading [loading]="isLoading"></app-loading>
<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]="t('jumpkey-count', {count: jumpKey.size})" placement="left">
{{jumpKey.title}}
@for(jumpKey of jumpBarKeysToRender; track jumpKey.key; let i = $index) {
<button class="btn btn-link flip-button" [ngClass]="{'disabled': hasCustomSort()}"
(click)="scrollTo(jumpKey)">
<div class="flip-button-inner">
<div class="flip-button-front">{{jumpKey.title}}</div>
<div class="flip-button-back">{{jumpKey.size}}</div>
</div>
</button>
</ng-container>
}
</div>
</ng-template>
</ng-container>

View file

@ -27,7 +27,7 @@
display: grid;
grid-template-columns: repeat(auto-fill, 158px);
grid-gap: 0.5rem;
justify-content: space-around;
justify-content: space-between;
width: 100%;
overflow-y: auto;
overflow-x: hidden;
@ -103,3 +103,46 @@ h2 {
display: inline-block;
word-break: break-all;
}
.flip-button {
cursor: pointer;
padding: 0;
margin: 0;
width: 40px;
height: 25px;
position: relative;
overflow: hidden;
}
.flip-button-inner {
position: absolute;
width: 100%;
height: 100%;
text-align: center;
transition: transform 0.6s;
transform-style: preserve-3d;
top: 0;
left: 0;
}
.flip-button:hover .flip-button-inner {
transform: rotateY(180deg);
}
.flip-button-front, .flip-button-back {
position: absolute;
width: 100%;
height: 100%;
backface-visibility: hidden;
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
border-radius: 4px;
}
.flip-button-back {
transform: rotateY(180deg);
}

View file

@ -1,4 +1,4 @@
import {CommonModule, DOCUMENT} from '@angular/common';
import {DOCUMENT, NgClass, NgForOf, NgTemplateOutlet} from '@angular/common';
import {
ChangeDetectionStrategy,
ChangeDetectorRef,
@ -28,7 +28,6 @@ import {Pagination} from 'src/app/_models/pagination';
import {FilterEvent, FilterItem, SortField} from 'src/app/_models/metadata/series-filter';
import {ActionItem} from 'src/app/_services/action-factory.service';
import {JumpbarService} from 'src/app/_services/jumpbar.service';
import {ScrollService} from 'src/app/_services/scroll.service';
import {LoadingComponent} from "../../shared/loading/loading.component";
@ -44,7 +43,8 @@ const ANIMATION_TIME_MS = 0;
@Component({
selector: 'app-card-detail-layout',
standalone: true,
imports: [CommonModule, LoadingComponent, VirtualScrollerModule, CardActionablesComponent, NgbTooltip, MetadataFilterComponent, TranslocoDirective],
imports: [LoadingComponent, VirtualScrollerModule, CardActionablesComponent, NgbTooltip, MetadataFilterComponent,
TranslocoDirective, NgTemplateOutlet, NgClass, NgForOf],
templateUrl: './card-detail-layout.component.html',
styleUrls: ['./card-detail-layout.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
@ -56,7 +56,8 @@ export class CardDetailLayoutComponent implements OnInit, OnChanges {
private readonly cdRef = inject(ChangeDetectorRef);
private readonly jumpbarService = inject(JumpbarService);
private readonly router = inject(Router);
private readonly scrollService = inject(ScrollService);
protected readonly Breakpoint = Breakpoint;
@Input() header: string = '';
@Input() isLoading: boolean = false;
@ -101,10 +102,9 @@ export class CardDetailLayoutComponent implements OnInit, OnChanges {
libraries: Array<FilterItem<Library>> = [];
updateApplied: number = 0;
hasResumedJumpKey: boolean = false;
bufferAmount: number = 1;
protected readonly Breakpoint = Breakpoint;
constructor(@Inject(DOCUMENT) private document: Document) {}
@ -145,34 +145,16 @@ 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) {
const resumeKey = this.jumpbarService.getResumeKey(this.router.url);
if (resumeKey === '') return;
const keys = this.jumpBarKeysToRender.filter(k => k.key === resumeKey);
if (keys.length < 1) return;
this.hasResumedJumpKey = true;
setTimeout(() => this.scrollTo(keys[0]), 100);
}
const startIndex = this.jumpbarService.getResumePosition(this.router.url);
if (startIndex > 0) {
setTimeout(() => this.virtualScroller.scrollToIndex(startIndex, true, 0, ANIMATION_TIME_MS), 10);
}
// 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() {
if (this.filteringDisabled) return false;
const hasCustomSort = this.filter?.sortOptions?.sortField != SortField.SortName || !this.filter?.sortOptions.isAscending;
const hasNonDefaultSortField = this.filterSettings?.presetsV2?.sortOptions?.sortField != SortField.SortName;
//const hasNonDefaultSortField = this.filterSettings?.presetsV2?.sortOptions?.sortField != SortField.SortName;
return hasCustomSort;
}
@ -201,20 +183,10 @@ export class CardDetailLayoutComponent implements OnInit, OnChanges {
}
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();
setTimeout(() => this.jumpbarService.saveResumePosition(this.router.url, this.virtualScroller.viewPortInfo.startIndex), ANIMATION_TIME_MS + 100);
}
tryToSaveJumpKey(item: any) {
let name = '';
if (item.hasOwnProperty('name')) {
name = item.name;
} else if (item.hasOwnProperty('title')) {
name = item.title;
}
this.jumpbarService.saveResumeKey(this.router.url, name.charAt(0));
tryToSaveJumpKey() {
this.jumpbarService.saveResumePosition(this.router.url, this.virtualScroller.viewPortInfo.startIndex);
}
}