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

@ -0,0 +1,14 @@
<ng-container *transloco="let t;">
<div class="container-fluid">
<ng-content></ng-content>
@if (subtitle) {
<div class="description text-muted" [innerHTML]="subtitle | safeHtml"></div>
}
</div>
</ng-container>

View file

@ -0,0 +1,3 @@
.description {
font-size: .9rem; //14px
}

View file

@ -0,0 +1,23 @@
import {ChangeDetectionStrategy, Component, Input} from '@angular/core';
import {SafeHtmlPipe} from "../../../_pipes/safe-html.pipe";
import {TranslocoDirective} from "@ngneat/transloco";
/**
* Use with btn-sm
*/
@Component({
selector: 'app-setting-button',
standalone: true,
imports: [
SafeHtmlPipe,
TranslocoDirective
],
templateUrl: './setting-button.component.html',
styleUrl: './setting-button.component.scss',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class SettingButtonComponent {
@Input({required:true}) subtitle: string = '';
}

View file

@ -0,0 +1,40 @@
<ng-container *transloco="let t;">
<div class="container-fluid">
<div class="row g-0">
<div class="col-11">
<h6 class="section-title">
@if(labelId) {
<label class="reset-label" [for]="labelId">{{title}}</label>
} @else {
{{title}}
}
@if (titleExtraRef) {
<ng-container [ngTemplateOutlet]="titleExtraRef"></ng-container>
}
</h6>
</div>
<div class="col-1">
@if (showEdit) {
<button class="btn btn-text btn-sm" (click)="toggleEditMode()" [disabled]="!canEdit">
{{isEditMode ? t('common.close') : (editLabel || t('common.edit'))}}
</button>
}
@if (titleActionsRef) {
<ng-container [ngTemplateOutlet]="titleActionsRef"></ng-container>
}
</div>
</div>
@if (isEditMode) {
<ng-container [ngTemplateOutlet]="valueEditRef"></ng-container>
} @else {
<span class="view-value" (click)="toggleEditMode()"><ng-container [ngTemplateOutlet]="valueViewRef"></ng-container></span>
}
@if (subtitle) {
<div class="text-muted mt-2" [innerHTML]="subtitle | safeHtml"></div>
}
</div>
</ng-container>

View file

@ -0,0 +1,13 @@
.text-muted {
font-size: 0.9rem;
a {
color: var(--primary-color);
}
}
.view-value {
font-size: 18px;
color: var(--primary-color);
cursor: pointer;
}

View file

@ -0,0 +1,92 @@
import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
ContentChild, ElementRef, EventEmitter, HostListener,
inject,
Input, Output,
TemplateRef
} from '@angular/core';
import {TranslocoDirective} from "@ngneat/transloco";
import {NgTemplateOutlet} from "@angular/common";
import {SafeHtmlPipe} from "../../../_pipes/safe-html.pipe";
import {filter, fromEvent, tap} from "rxjs";
@Component({
selector: 'app-setting-item',
standalone: true,
imports: [
TranslocoDirective,
NgTemplateOutlet,
SafeHtmlPipe
],
templateUrl: './setting-item.component.html',
styleUrl: './setting-item.component.scss',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class SettingItemComponent {
private readonly cdRef = inject(ChangeDetectorRef);
@Input({required:true}) title: string = '';
@Input() editLabel: string | undefined = undefined;
@Input() canEdit: boolean = true;
@Input() showEdit: boolean = true;
@Input() isEditMode: boolean = false;
@Input() subtitle: string | undefined = undefined;
@Input() labelId: string | undefined = undefined;
@Input() toggleOnViewClick: boolean = true;
@Output() editMode = new EventEmitter<boolean>();
/**
* Extra information to show next to the title
*/
@ContentChild('titleExtra') titleExtraRef!: TemplateRef<any>;
/**
* View in View mode
*/
@ContentChild('view') valueViewRef!: TemplateRef<any>;
/**
* View in Edit mode
*/
@ContentChild('edit') valueEditRef!: TemplateRef<any>;
/**
* Extra button controls to show instead of Edit
*/
@ContentChild('titleActions') titleActionsRef!: TemplateRef<any>;
@HostListener('click', ['$event'])
onClickInside(event: MouseEvent) {
event.stopPropagation(); // Prevent the click from bubbling up
}
constructor(elementRef: ElementRef) {
if (!this.toggleOnViewClick) return;
fromEvent(window, 'click')
.pipe(
filter((event: Event) => {
if (!this.toggleOnViewClick) return false;
const mouseEvent = event as MouseEvent;
return !elementRef.nativeElement.contains(mouseEvent.target)
}),
tap(() => {
this.isEditMode = false;
this.editMode.emit(this.isEditMode);
this.cdRef.markForCheck();
})
)
.subscribe();
}
toggleEditMode() {
if (!this.toggleOnViewClick) return;
this.isEditMode = !this.isEditMode;
this.editMode.emit(this.isEditMode);
this.cdRef.markForCheck();
}
}

View file

@ -0,0 +1,19 @@
<ng-container *transloco="let t;">
<div class="container-fluid">
<div class="row g-0 mb-2">
<div class="col-11">
<h6 class="section-title" [id]="id || title">{{title}}</h6>
</div>
<div class="col-1">
@if (switchRef) {
<ng-container [ngTemplateOutlet]="switchRef"></ng-container>
}
</div>
</div>
@if (subtitle) {
<div class="text-muted mt-2" [innerHTML]="subtitle | safeHtml"></div>
}
</div>
</ng-container>

View file

@ -0,0 +1,3 @@
.text-muted {
font-size: 14px;
}

View file

@ -0,0 +1,33 @@
import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component, ContentChild,
inject,
Input,
TemplateRef
} from '@angular/core';
import {NgTemplateOutlet} from "@angular/common";
import {TranslocoDirective} from "@ngneat/transloco";
import {SafeHtmlPipe} from "../../../_pipes/safe-html.pipe";
@Component({
selector: 'app-setting-switch',
standalone: true,
imports: [
NgTemplateOutlet,
TranslocoDirective,
SafeHtmlPipe
],
templateUrl: './setting-switch.component.html',
styleUrl: './setting-switch.component.scss',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class SettingSwitchComponent {
private readonly cdRef = inject(ChangeDetectorRef);
@Input({required:true}) title: string = '';
@Input() subtitle: string | undefined = undefined;
@Input() id: string | undefined = undefined;
@ContentChild('switch') switchRef!: TemplateRef<any>;
}

View file

@ -0,0 +1,16 @@
<ng-container *transloco="let t;">
<div class="container-fluid">
<div class="row g-0 mb-2">
<div class="col-11">
<h6 class="section-title" [id]="id || title">{{title}}
@if (titleExtraRef) {
<ng-container [ngTemplateOutlet]="titleExtraRef"></ng-container>
}
</h6>
</div>
<div class="col-1">
<button class="btn btn-text btn-sm" (click)="toggleViewMode()" [disabled]="!canEdit">{{isEditMode ? t('common.close') : t('common.edit')}}</button>
</div>
</div>
</div>
</ng-container>

View file

@ -0,0 +1,41 @@
import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component, ContentChild,
EventEmitter,
inject,
Input,
Output, TemplateRef
} from '@angular/core';
import {NgTemplateOutlet} from "@angular/common";
import {TranslocoDirective} from "@ngneat/transloco";
@Component({
selector: 'app-setting-title',
standalone: true,
imports: [
NgTemplateOutlet,
TranslocoDirective
],
templateUrl: './setting-title.component.html',
styleUrl: './setting-title.component.scss',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class SettingTitleComponent {
private readonly cdRef = inject(ChangeDetectorRef);
@Input({required:true}) title: string = '';
@Input() id: string | undefined = undefined;
@Input() canEdit: boolean = true;
@Input() isEditMode: boolean = false;
@Output() editMode = new EventEmitter<boolean>();
@ContentChild('extra') titleExtraRef!: TemplateRef<any>;
toggleViewMode() {
this.isEditMode = !this.isEditMode;
this.editMode.emit(this.isEditMode);
this.cdRef.markForCheck();
}
}

View file

@ -0,0 +1,180 @@
<ng-container *transloco="let t; read:'settings'">
<app-side-nav-companion-bar>
<h2 title>
{{fragment | settingFragment}}
</h2>
</app-side-nav-companion-bar>
<div class="row col-me-4 pb-3">
@if (accountService.currentUser$ | async; as user) {
@if (accountService.hasAdminRole(user)) {
@defer (when fragment === SettingsTabId.General; prefetch on idle) {
@if (fragment === SettingsTabId.General) {
<div class="col-md-6 col-sm-12">
<app-manage-settings></app-manage-settings>
</div>
}
}
@defer (when fragment === SettingsTabId.Email; prefetch on idle) {
@if (fragment === SettingsTabId.Email) {
<div class="col-md-6 col-sm-12">
<app-manage-email-settings></app-manage-email-settings>
</div>
}
}
@defer (when fragment === SettingsTabId.Media; prefetch on idle) {
@if (fragment === SettingsTabId.Media) {
<div class="col-md-6 col-sm-12">
<app-manage-media-settings></app-manage-media-settings>
</div>
}
}
@defer (when fragment === SettingsTabId.Users; prefetch on idle) {
@if (fragment === SettingsTabId.Users) {
<div class="col-md-12">
<app-manage-users></app-manage-users>
</div>
}
}
@defer (when fragment === SettingsTabId.Libraries; prefetch on idle) {
@if (fragment === SettingsTabId.Libraries) {
<div class="col-md-12">
<app-manage-library></app-manage-library>
</div>
}
}
@defer (when fragment === SettingsTabId.MediaIssues; prefetch on idle) {
@if (fragment === SettingsTabId.MediaIssues) {
<div class="col-md-12">
<app-manage-media-issues></app-manage-media-issues>
</div>
}
}
@defer (when fragment === SettingsTabId.System; prefetch on idle) {
@if (fragment === SettingsTabId.System) {
<div class="col-md-12">
<app-manage-system></app-manage-system>
</div>
}
}
@defer (when fragment === SettingsTabId.Statistics; prefetch on idle) {
@if (fragment === SettingsTabId.Statistics) {
<div class="col-md-12">
<app-server-stats></app-server-stats>
</div>
}
}
@defer (when fragment === SettingsTabId.Tasks; prefetch on idle) {
@if (fragment === SettingsTabId.Tasks) {
<div class="col-md-12">
<app-manage-tasks-settings></app-manage-tasks-settings>
</div>
}
}
@defer (when fragment === SettingsTabId.KavitaPlus; prefetch on idle) {
@if (fragment === SettingsTabId.KavitaPlus) {
<div class="col-md-12">
<app-manage-kavitaplus></app-manage-kavitaplus>
</div>
}
}
}
@defer (when fragment === SettingsTabId.Account; prefetch on idle) {
@if (fragment === SettingsTabId.Account) {
<div class="col-md-6 col-sm-12">
<app-change-email></app-change-email>
<div class="setting-section-break"></div>
<app-change-password></app-change-password>
<div class="setting-section-break"></div>
<app-change-age-restriction></app-change-age-restriction>
<div class="setting-section-break"></div>
<app-manage-scrobbling-providers></app-manage-scrobbling-providers>
</div>
}
}
@defer (when fragment === SettingsTabId.Preferences; prefetch on idle) {
@if (fragment === SettingsTabId.Preferences) {
<div class="col-md-6 col-sm-12">
<app-manga-user-preferences></app-manga-user-preferences>
</div>
}
}
@defer (when fragment === SettingsTabId.Customize; prefetch on idle) {
@if (fragment === SettingsTabId.Customize) {
<div class="col-md-12">
<app-manage-customization></app-manage-customization>
</div>
}
}
@defer (when fragment === SettingsTabId.Clients; prefetch on idle) {
@if (fragment === SettingsTabId.Clients) {
<div class="col-md-6 col-sm-12">
<app-manage-opds></app-manage-opds>
</div>
}
}
@defer (when fragment === SettingsTabId.Theme; prefetch on idle) {
@if (fragment === SettingsTabId.Theme) {
<div class="col-md-12">
<app-theme-manager></app-theme-manager>
</div>
}
}
@defer (when fragment === SettingsTabId.Devices; prefetch on idle) {
@if (fragment === SettingsTabId.Devices) {
<div class="col-md-12">
<app-manage-devices></app-manage-devices>
</div>
}
}
@defer (when fragment === SettingsTabId.UserStats; prefetch on idle) {
@if (fragment === SettingsTabId.UserStats) {
<div class="col-md-12">
<app-user-stats></app-user-stats>
</div>
}
}
@defer (when fragment === SettingsTabId.CBLImport; prefetch on idle) {
@if (fragment === SettingsTabId.CBLImport) {
<div class="col-md-12">
<app-import-cbl></app-import-cbl>
</div>
}
}
@defer (when fragment === SettingsTabId.Scrobbling; prefetch on idle) {
@if(hasActiveLicense && fragment === SettingsTabId.Scrobbling) {
<div class="col-md-12">
<app-manage-scrobling></app-manage-scrobling>
</div>
}
}
@defer (when fragment === SettingsTabId.MALStackImport; prefetch on idle) {
@if(hasActiveLicense && fragment === SettingsTabId.MALStackImport) {
<div class="col-md-12">
<app-import-mal-collection></app-import-mal-collection>
</div>
}
}
}
</div>
</ng-container>

View file

@ -0,0 +1,4 @@
h2 {
color: white;
font-weight: bold;
}

View file

@ -0,0 +1,138 @@
import {ChangeDetectionStrategy, ChangeDetectorRef, Component, DestroyRef, inject} from '@angular/core';
import {
ChangeAgeRestrictionComponent
} from "../../../user-settings/change-age-restriction/change-age-restriction.component";
import {ChangeEmailComponent} from "../../../user-settings/change-email/change-email.component";
import {ChangePasswordComponent} from "../../../user-settings/change-password/change-password.component";
import {ManageDevicesComponent} from "../../../user-settings/manage-devices/manage-devices.component";
import {ManageOpdsComponent} from "../../../user-settings/manage-opds/manage-opds.component";
import {
ManageScrobblingProvidersComponent
} from "../../../user-settings/manage-scrobbling-providers/manage-scrobbling-providers.component";
import {
ManageUserPreferencesComponent
} from "../../../user-settings/manga-user-preferences/manage-user-preferences.component";
import {NgbNav, NgbNavContent, NgbNavLinkBase} from "@ng-bootstrap/ng-bootstrap";
import {ActivatedRoute, Router, RouterLink} from "@angular/router";
import {
SideNavCompanionBarComponent
} from "../../../sidenav/_components/side-nav-companion-bar/side-nav-companion-bar.component";
import {ThemeManagerComponent} from "../../../user-settings/theme-manager/theme-manager.component";
import {TranslocoDirective} from "@ngneat/transloco";
import {ScrobblingHoldsComponent} from "../../../user-settings/user-holds/scrobbling-holds.component";
import {
UserScrobbleHistoryComponent
} from "../../../_single-module/user-scrobble-history/user-scrobble-history.component";
import {UserStatsComponent} from "../../../statistics/_components/user-stats/user-stats.component";
import {SettingsTabId} from "../../../sidenav/preference-nav/preference-nav.component";
import {AsyncPipe} from "@angular/common";
import {AccountService} from "../../../_services/account.service";
import {WikiLink} from "../../../_models/wiki";
import {LicenseComponent} from "../../../admin/license/license.component";
import {ManageEmailSettingsComponent} from "../../../admin/manage-email-settings/manage-email-settings.component";
import {ManageLibraryComponent} from "../../../admin/manage-library/manage-library.component";
import {ManageMediaSettingsComponent} from "../../../admin/manage-media-settings/manage-media-settings.component";
import {ManageSettingsComponent} from "../../../admin/manage-settings/manage-settings.component";
import {ManageSystemComponent} from "../../../admin/manage-system/manage-system.component";
import {ManageTasksSettingsComponent} from "../../../admin/manage-tasks-settings/manage-tasks-settings.component";
import {ManageUsersComponent} from "../../../admin/manage-users/manage-users.component";
import {ServerStatsComponent} from "../../../statistics/_components/server-stats/server-stats.component";
import {SettingFragmentPipe} from "../../../_pipes/setting-fragment.pipe";
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
import {tap} from "rxjs";
import {
KavitaplusMetadataBreakdownStatsComponent
} from "../../../statistics/_components/kavitaplus-metadata-breakdown-stats/kavitaplus-metadata-breakdown-stats.component";
import {ManageKavitaplusComponent} from "../../../admin/manage-kavitaplus/manage-kavitaplus.component";
import {ManageScrobblingComponent} from "../../../admin/manage-scrobling/manage-scrobbling.component";
import {ManageMediaIssuesComponent} from "../../../admin/manage-media-issues/manage-media-issues.component";
import {
ManageCustomizationComponent
} from "../../../sidenav/_components/manage-customization/manage-customization.component";
import {
ImportMalCollectionComponent
} from "../../../collections/_components/import-mal-collection/import-mal-collection.component";
import {ImportCblComponent} from "../../../reading-list/_components/import-cbl/import-cbl.component";
@Component({
selector: 'app-settings',
standalone: true,
imports: [
ChangeAgeRestrictionComponent,
ChangeEmailComponent,
ChangePasswordComponent,
ManageDevicesComponent,
ManageOpdsComponent,
ManageScrobblingProvidersComponent,
ManageUserPreferencesComponent,
NgbNav,
NgbNavContent,
NgbNavLinkBase,
RouterLink,
SideNavCompanionBarComponent,
ThemeManagerComponent,
TranslocoDirective,
ScrobblingHoldsComponent,
UserScrobbleHistoryComponent,
UserStatsComponent,
AsyncPipe,
LicenseComponent,
ManageEmailSettingsComponent,
ManageLibraryComponent,
ManageMediaSettingsComponent,
ManageSettingsComponent,
ManageSystemComponent,
ManageTasksSettingsComponent,
ManageUsersComponent,
ServerStatsComponent,
SettingFragmentPipe,
KavitaplusMetadataBreakdownStatsComponent,
ManageKavitaplusComponent,
ManageScrobblingComponent,
ManageMediaIssuesComponent,
ManageCustomizationComponent,
ImportMalCollectionComponent,
ImportCblComponent
],
templateUrl: './settings.component.html',
styleUrl: './settings.component.scss',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class SettingsComponent {
private readonly route = inject(ActivatedRoute);
private readonly cdRef = inject(ChangeDetectorRef);
private readonly destroyRef = inject(DestroyRef);
private readonly router = inject(Router);
protected readonly accountService = inject(AccountService);
protected readonly SettingsTabId = SettingsTabId;
protected readonly WikiLink = WikiLink;
fragment: SettingsTabId = SettingsTabId.Account;
hasActiveLicense = false;
constructor() {
this.route.fragment.pipe(tap(frag => {
if (frag === null) {
frag = SettingsTabId.Account;
}
if (!Object.values(SettingsTabId).includes(frag as SettingsTabId)) {
this.router.navigate(['home']);
return;
}
this.fragment = frag as SettingsTabId;
//this.titleService.setTitle('Kavita - ' + translate('admin-dashboard.title'));
this.cdRef.markForCheck();
}), takeUntilDestroyed(this.destroyRef)).subscribe();
this.accountService.hasValidLicense$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(res => {
if (res) {
this.hasActiveLicense = true;
this.cdRef.markForCheck();
}
});
}
}