Polish for Release (#2314)
This commit is contained in:
parent
fe4af4b648
commit
59b950c4bd
54 changed files with 1162 additions and 1056 deletions
1220
UI/Web/package-lock.json
generated
1220
UI/Web/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -13,22 +13,22 @@
|
|||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@angular/animations": "^16.2.7",
|
||||
"@angular/cdk": "^16.2.6",
|
||||
"@angular/common": "^16.2.7",
|
||||
"@angular/compiler": "^16.2.7",
|
||||
"@angular/core": "^16.2.7",
|
||||
"@angular/forms": "^16.2.7",
|
||||
"@angular/localize": "^16.2.7",
|
||||
"@angular/platform-browser": "^16.2.7",
|
||||
"@angular/platform-browser-dynamic": "^16.2.7",
|
||||
"@angular/router": "^16.2.7",
|
||||
"@angular/animations": "^16.2.9",
|
||||
"@angular/cdk": "^16.2.8",
|
||||
"@angular/common": "^16.2.9",
|
||||
"@angular/compiler": "^16.2.9",
|
||||
"@angular/core": "^16.2.9",
|
||||
"@angular/forms": "^16.2.9",
|
||||
"@angular/localize": "^16.2.9",
|
||||
"@angular/platform-browser": "^16.2.9",
|
||||
"@angular/platform-browser-dynamic": "^16.2.9",
|
||||
"@angular/router": "^16.2.9",
|
||||
"@fortawesome/fontawesome-free": "^6.4.2",
|
||||
"@iharbeck/ngx-virtual-scroller": "^16.0.0",
|
||||
"@iplab/ngx-file-upload": "^16.0.2",
|
||||
"@microsoft/signalr": "^7.0.11",
|
||||
"@microsoft/signalr": "^7.0.12",
|
||||
"@ng-bootstrap/ng-bootstrap": "^15.1.1",
|
||||
"@ngneat/transloco": "^5.0.7",
|
||||
"@ngneat/transloco": "^6.0.0",
|
||||
"@ngneat/transloco-locale": "^5.1.1",
|
||||
"@ngneat/transloco-persist-lang": "^5.0.0",
|
||||
"@ngneat/transloco-persist-translations": "^5.0.0",
|
||||
|
@ -41,10 +41,11 @@
|
|||
"eventsource": "^2.0.2",
|
||||
"file-saver": "^2.0.5",
|
||||
"lazysizes": "^5.3.2",
|
||||
"luxon": "^3.4.3",
|
||||
"ng-circle-progress": "^1.7.1",
|
||||
"ng-select2-component": "^13.0.9",
|
||||
"ngx-color-picker": "^15.0.0",
|
||||
"ngx-extended-pdf-viewer": "^16.2.16",
|
||||
"ngx-extended-pdf-viewer": "^18.0.2",
|
||||
"ngx-file-drop": "^16.0.0",
|
||||
"ngx-slider-v2": "^16.0.2",
|
||||
"ngx-stars": "^1.6.5",
|
||||
|
@ -52,27 +53,28 @@
|
|||
"rxjs": "^7.8.0",
|
||||
"screenfull": "^6.0.2",
|
||||
"swiper": "^8.4.6",
|
||||
"tslib": "^2.6.1",
|
||||
"tslib": "^2.6.2",
|
||||
"zone.js": "^0.13.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-devkit/build-angular": "^16.2.4",
|
||||
"@angular-eslint/builder": "^16.1.0",
|
||||
"@angular-eslint/eslint-plugin": "^16.1.0",
|
||||
"@angular-eslint/eslint-plugin-template": "^16.1.0",
|
||||
"@angular-eslint/schematics": "^16.1.0",
|
||||
"@angular-eslint/template-parser": "^16.1.0",
|
||||
"@angular/cli": "^16.2.4",
|
||||
"@angular/compiler-cli": "^16.2.7",
|
||||
"@types/d3": "^7.4.0",
|
||||
"@types/node": "^20.4.8",
|
||||
"@typescript-eslint/eslint-plugin": "^6.3.0",
|
||||
"@typescript-eslint/parser": "^6.3.0",
|
||||
"eslint": "^8.46.0",
|
||||
"@angular-devkit/build-angular": "^16.2.6",
|
||||
"@angular-eslint/builder": "^16.2.0",
|
||||
"@angular-eslint/eslint-plugin": "^16.2.0",
|
||||
"@angular-eslint/eslint-plugin-template": "^16.2.0",
|
||||
"@angular-eslint/schematics": "^16.2.0",
|
||||
"@angular-eslint/template-parser": "^16.2.0",
|
||||
"@angular/cli": "^16.2.6",
|
||||
"@angular/compiler-cli": "^16.2.9",
|
||||
"@types/d3": "^7.4.1",
|
||||
"@types/luxon": "^3.3.2",
|
||||
"@types/node": "^20.8.6",
|
||||
"@typescript-eslint/eslint-plugin": "^6.7.5",
|
||||
"@typescript-eslint/parser": "^6.7.5",
|
||||
"eslint": "^8.51.0",
|
||||
"jsonminify": "^0.4.2",
|
||||
"karma-coverage": "~2.2.0",
|
||||
"ts-node": "~10.9.1",
|
||||
"typescript": "^5.1.6",
|
||||
"webpack-bundle-analyzer": "^4.8.0"
|
||||
"webpack-bundle-analyzer": "^4.9.1"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -31,7 +31,15 @@ export enum FilterField
|
|||
ReadingDate = 27
|
||||
}
|
||||
|
||||
export const allFields = Object.keys(FilterField)
|
||||
|
||||
const enumArray = Object.keys(FilterField)
|
||||
.filter(key => !isNaN(Number(key)) && parseInt(key, 10) >= 0)
|
||||
.map(key => parseInt(key, 10))
|
||||
.sort((a, b) => a - b) as FilterField[];
|
||||
.map(key => {
|
||||
// @ts-ignore
|
||||
return ({key: key, value: FilterField[key]});
|
||||
});
|
||||
|
||||
enumArray.sort((a, b) => a.value.localeCompare(b.value));
|
||||
|
||||
export const allFields = enumArray
|
||||
.map(key => parseInt(key.key, 10))as FilterField[];
|
||||
|
|
|
@ -114,7 +114,7 @@ export class AccountService {
|
|||
.pipe(map(res => res === "true"));
|
||||
}
|
||||
|
||||
login(model: {username: string, password: string}) {
|
||||
login(model: {username: string, password: string, apiKey?: string}) {
|
||||
return this.httpClient.post<User>(this.baseUrl + 'account/login', model).pipe(
|
||||
map((response: User) => {
|
||||
const user = response;
|
||||
|
|
|
@ -9,6 +9,7 @@ import { Series } from '../_models/series';
|
|||
import { Volume } from '../_models/volume';
|
||||
import { AccountService } from './account.service';
|
||||
import { DeviceService } from './device.service';
|
||||
import {SideNavStream} from "../_models/sidenav/sidenav-stream";
|
||||
|
||||
export enum Action {
|
||||
Submenu = -1,
|
||||
|
@ -93,7 +94,9 @@ export enum Action {
|
|||
*/
|
||||
RemoveFromOnDeck = 19,
|
||||
AddRuleGroup = 20,
|
||||
RemoveRuleGroup = 21
|
||||
RemoveRuleGroup = 21,
|
||||
MarkAsVisible = 22,
|
||||
MarkAsInvisible = 23,
|
||||
}
|
||||
|
||||
export interface ActionItem<T> {
|
||||
|
@ -135,6 +138,8 @@ export class ActionFactoryService {
|
|||
|
||||
bookmarkActions: Array<ActionItem<Series>> = [];
|
||||
|
||||
sideNavStreamActions: Array<ActionItem<SideNavStream>> = [];
|
||||
|
||||
isAdmin = false;
|
||||
hasDownloadRole = false;
|
||||
|
||||
|
@ -160,6 +165,10 @@ export class ActionFactoryService {
|
|||
return this.applyCallbackToList(this.seriesActions, callback);
|
||||
}
|
||||
|
||||
getSideNavStreamActions(callback: (action: ActionItem<SideNavStream>, series: SideNavStream) => void) {
|
||||
return this.applyCallbackToList(this.sideNavStreamActions, callback);
|
||||
}
|
||||
|
||||
getVolumeActions(callback: (action: ActionItem<Volume>, volume: Volume) => void) {
|
||||
return this.applyCallbackToList(this.volumeActions, callback);
|
||||
}
|
||||
|
@ -564,6 +573,23 @@ export class ActionFactoryService {
|
|||
children: [],
|
||||
},
|
||||
];
|
||||
|
||||
this.sideNavStreamActions = [
|
||||
{
|
||||
action: Action.MarkAsVisible,
|
||||
title: 'mark-visible',
|
||||
callback: this.dummyCallback,
|
||||
requiresAdmin: false,
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
action: Action.MarkAsInvisible,
|
||||
title: 'mark-invisible',
|
||||
callback: this.dummyCallback,
|
||||
requiresAdmin: false,
|
||||
children: [],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
private applyCallback(action: ActionItem<any>, callback: (action: ActionItem<any>, data: any) => void) {
|
||||
|
|
|
@ -62,6 +62,10 @@ export class NavService {
|
|||
return this.httpClient.post<SideNavStream>(this.baseUrl + 'stream/add-sidenav-stream-from-external-source?externalSourceId=' + externalSourceId, {});
|
||||
}
|
||||
|
||||
bulkToggleSideNavStreamVisibility(streamIds: Array<number>, targetVisibility: boolean) {
|
||||
return this.httpClient.post(this.baseUrl + 'stream/bulk-sidenav-stream-visibility', {ids: streamIds, visibility: targetVisibility});
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows the top nav bar. This should be visible on all pages except the reader.
|
||||
*/
|
||||
|
|
|
@ -44,14 +44,14 @@
|
|||
</thead>
|
||||
<tbody>
|
||||
<tr *ngIf="events.length === 0">
|
||||
<td colspan="6">{{t('no-data')}}/td>
|
||||
<td colspan="6">{{t('no-data')}}</td>
|
||||
</tr>
|
||||
<tr *ngFor="let item of events; let idx = index;">
|
||||
<td>
|
||||
{{item.createdUtc | translocoDate: {dateStyle: 'short', timeStyle: 'medium', } | defaultValue }}
|
||||
{{item.createdUtc | utcToLocalTime | defaultValue}}
|
||||
</td>
|
||||
<td>
|
||||
{{item.lastModifiedUtc | translocoDate: {dateStyle: 'short', timeStyle: 'medium' } | defaultValue }}
|
||||
{{item.lastModifiedUtc | utcToLocalTime | defaultValue }}
|
||||
</td>
|
||||
<td>
|
||||
{{item.scrobbleEventType | scrobbleEventType}}
|
||||
|
|
|
@ -14,11 +14,12 @@ import {FormControl, FormGroup, ReactiveFormsModule} from "@angular/forms";
|
|||
import {TranslocoModule} from "@ngneat/transloco";
|
||||
import {DefaultValuePipe} from "../../pipe/default-value.pipe";
|
||||
import {TranslocoLocaleModule} from "@ngneat/transloco-locale";
|
||||
import {UtcToLocalTimePipe} from "../../pipe/utc-to-local-time.pipe";
|
||||
|
||||
@Component({
|
||||
selector: 'app-user-scrobble-history',
|
||||
standalone: true,
|
||||
imports: [CommonModule, ScrobbleEventTypePipe, NgbPagination, ReactiveFormsModule, SortableHeader, TranslocoModule, DefaultValuePipe, TranslocoLocaleModule],
|
||||
imports: [CommonModule, ScrobbleEventTypePipe, NgbPagination, ReactiveFormsModule, SortableHeader, TranslocoModule, DefaultValuePipe, TranslocoLocaleModule, UtcToLocalTimePipe],
|
||||
templateUrl: './user-scrobble-history.component.html',
|
||||
styleUrls: ['./user-scrobble-history.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
<div class="modal-body">
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="typeahead-focus" class="form-label">{{t('path')}}</label>
|
||||
<label for="typeahead-focus" class="form-label">{{t('path-label')}}</label>
|
||||
<div class="input-group">
|
||||
<input id="typeahead-focus" type="text" class="form-control" [(ngModel)]="path" [ngbTypeahead]="search"
|
||||
(focus)="focus$.next($any($event).target.value)" (click)="click$.next($any($event).target.value)"
|
||||
|
|
|
@ -38,7 +38,7 @@
|
|||
<a href="library/{{item.libraryId}}/series/{{item.seriesId}}" target="_blank">{{item.details}}</a>
|
||||
</td>
|
||||
<td>
|
||||
{{item.createdUtc | translocoDate: {dateStyle: 'short', timeStyle: 'medium' } | defaultValue }}
|
||||
{{item.createdUtc | utcToLocalTime | defaultValue }}
|
||||
</td>
|
||||
<td>
|
||||
{{item.comment}}
|
||||
|
|
|
@ -29,11 +29,12 @@ import {TranslocoModule} from "@ngneat/transloco";
|
|||
import {DefaultDatePipe} from "../../pipe/default-date.pipe";
|
||||
import {DefaultValuePipe} from "../../pipe/default-value.pipe";
|
||||
import {TranslocoLocaleModule} from "@ngneat/transloco-locale";
|
||||
import {UtcToLocalTimePipe} from "../../pipe/utc-to-local-time.pipe";
|
||||
|
||||
@Component({
|
||||
selector: 'app-manage-scrobble-errors',
|
||||
standalone: true,
|
||||
imports: [CommonModule, ReactiveFormsModule, FilterPipe, LoadingComponent, SortableHeader, TranslocoModule, DefaultDatePipe, DefaultValuePipe, TranslocoLocaleModule],
|
||||
imports: [CommonModule, ReactiveFormsModule, FilterPipe, LoadingComponent, SortableHeader, TranslocoModule, DefaultDatePipe, DefaultValuePipe, TranslocoLocaleModule, UtcToLocalTimePipe],
|
||||
templateUrl: './manage-scrobble-errors.component.html',
|
||||
styleUrls: ['./manage-scrobble-errors.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
|
|
|
@ -58,7 +58,7 @@
|
|||
<td>
|
||||
{{task.title | titlecase}}
|
||||
</td>
|
||||
<td>{{task.lastExecutionUtc | translocoDate: {dateStyle: 'short', timeStyle: 'medium' } | defaultValue }}</td>
|
||||
<td>{{task.lastExecutionUtc | utcToLocalTime | defaultValue }}</td>
|
||||
<td>{{task.cron}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
|
|
@ -14,6 +14,7 @@ import {DefaultValuePipe} from '../../pipe/default-value.pipe';
|
|||
import {AsyncPipe, DatePipe, NgFor, NgIf, NgTemplateOutlet, TitleCasePipe} from '@angular/common';
|
||||
import {TranslocoModule, TranslocoService} from "@ngneat/transloco";
|
||||
import {TranslocoLocaleModule} from "@ngneat/transloco-locale";
|
||||
import {UtcToLocalTimePipe} from "../../pipe/utc-to-local-time.pipe";
|
||||
|
||||
interface AdhocTask {
|
||||
name: string;
|
||||
|
@ -29,7 +30,7 @@ interface AdhocTask {
|
|||
styleUrls: ['./manage-tasks-settings.component.scss'],
|
||||
standalone: true,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [NgIf, ReactiveFormsModule, NgbTooltip, NgFor, AsyncPipe, TitleCasePipe, DatePipe, DefaultValuePipe, TranslocoModule, NgTemplateOutlet, TranslocoLocaleModule]
|
||||
imports: [NgIf, ReactiveFormsModule, NgbTooltip, NgFor, AsyncPipe, TitleCasePipe, DatePipe, DefaultValuePipe, TranslocoModule, NgTemplateOutlet, TranslocoLocaleModule, UtcToLocalTimePipe]
|
||||
})
|
||||
export class ManageTasksSettingsComponent implements OnInit {
|
||||
|
||||
|
|
|
@ -430,10 +430,10 @@
|
|||
<div>
|
||||
<div class="row g-0">
|
||||
<div class="col">
|
||||
{{t('added-title')}} {{volume.createdUtc | translocoDate: {dateStyle: 'short' } | defaultDate}}
|
||||
{{t('added-title')}} {{volume.createdUtc | utcToLocalTime | defaultDate}}
|
||||
</div>
|
||||
<div class="col">
|
||||
{{t('last-modified-title')}} {{volume.lastModifiedUtc | translocoDate: {dateStyle: 'short' } | defaultDate}}
|
||||
{{t('last-modified-title')}} {{volume.lastModifiedUtc | utcToLocalTime | translocoDate: {dateStyle: 'short' } | defaultDate}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row g-0">
|
||||
|
|
|
@ -53,7 +53,7 @@ import {ImageComponent} from "../../../shared/image/image.component";
|
|||
import {DefaultValuePipe} from "../../../pipe/default-value.pipe";
|
||||
import {TranslocoModule} from "@ngneat/transloco";
|
||||
import {TranslocoDatePipe} from "@ngneat/transloco-locale";
|
||||
import {Volume} from "../../../_models/volume";
|
||||
import {UtcToLocalTimePipe} from "../../../pipe/utc-to-local-time.pipe";
|
||||
|
||||
enum TabID {
|
||||
General = 0,
|
||||
|
@ -68,32 +68,32 @@ enum TabID {
|
|||
@Component({
|
||||
selector: 'app-edit-series-modal',
|
||||
standalone: true,
|
||||
imports: [
|
||||
ReactiveFormsModule,
|
||||
NgbNav,
|
||||
NgbNavContent,
|
||||
NgbNavItem,
|
||||
NgbNavLink,
|
||||
CommonModule,
|
||||
TypeaheadComponent,
|
||||
CoverImageChooserComponent,
|
||||
EditSeriesRelationComponent,
|
||||
SentenceCasePipe,
|
||||
MangaFormatPipe,
|
||||
DefaultDatePipe,
|
||||
TimeAgoPipe,
|
||||
TagBadgeComponent,
|
||||
PublicationStatusPipe,
|
||||
NgbTooltip,
|
||||
BytesPipe,
|
||||
ImageComponent,
|
||||
NgbCollapse,
|
||||
NgbNavOutlet,
|
||||
DefaultValuePipe,
|
||||
TranslocoModule,
|
||||
TranslocoDatePipe,
|
||||
|
||||
],
|
||||
imports: [
|
||||
ReactiveFormsModule,
|
||||
NgbNav,
|
||||
NgbNavContent,
|
||||
NgbNavItem,
|
||||
NgbNavLink,
|
||||
CommonModule,
|
||||
TypeaheadComponent,
|
||||
CoverImageChooserComponent,
|
||||
EditSeriesRelationComponent,
|
||||
SentenceCasePipe,
|
||||
MangaFormatPipe,
|
||||
DefaultDatePipe,
|
||||
TimeAgoPipe,
|
||||
TagBadgeComponent,
|
||||
PublicationStatusPipe,
|
||||
NgbTooltip,
|
||||
BytesPipe,
|
||||
ImageComponent,
|
||||
NgbCollapse,
|
||||
NgbNavOutlet,
|
||||
DefaultValuePipe,
|
||||
TranslocoModule,
|
||||
TranslocoDatePipe,
|
||||
UtcToLocalTimePipe,
|
||||
],
|
||||
templateUrl: './edit-series-modal.component.html',
|
||||
styleUrls: ['./edit-series-modal.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<ng-container *transloco="let t; read: 'bulk-operations'">
|
||||
<ng-container *ngIf="bulkSelectionService.selections$ | async as selectionCount">
|
||||
<div *ngIf="selectionCount > 0" class="bulk-select mb-3 fixed-top" [ngStyle]="{'margin-top': topOffset + 'px'}">
|
||||
<div *ngIf="selectionCount > 0" class="bulk-select mb-3 {{modalMode ? '' : 'fixed-top}}" [ngStyle]="{'margin-top': topOffset + 'px'}">
|
||||
<div class="d-flex justify-content-around align-items-center">
|
||||
|
||||
<span class="highlight">
|
||||
|
|
|
@ -32,19 +32,23 @@ import {CardActionablesComponent} from "../../_single-module/card-actionables/ca
|
|||
export class BulkOperationsComponent implements OnInit {
|
||||
|
||||
@Input({required: true}) actionCallback!: (action: ActionItem<any>, data: any) => void;
|
||||
|
||||
topOffset: number = 56;
|
||||
/**
|
||||
* Modal mode means don't fix to the top
|
||||
*/
|
||||
@Input() modalMode = false;
|
||||
@Input() topOffset: number = 56;
|
||||
hasMarkAsRead: boolean = false;
|
||||
hasMarkAsUnread: boolean = false;
|
||||
actions: Array<ActionItem<any>> = [];
|
||||
private readonly destroyRef = inject(DestroyRef);
|
||||
private readonly cdRef = inject(ChangeDetectorRef);
|
||||
private readonly actionFactoryService = inject(ActionFactoryService);
|
||||
public readonly bulkSelectionService = inject(BulkSelectionService);
|
||||
|
||||
get Action() {
|
||||
return Action;
|
||||
}
|
||||
protected readonly Action = Action;
|
||||
|
||||
constructor(public bulkSelectionService: BulkSelectionService, private readonly cdRef: ChangeDetectorRef,
|
||||
private actionFactoryService: ActionFactoryService) { }
|
||||
|
||||
constructor() { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.bulkSelectionService.actions$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(actions => {
|
||||
|
|
|
@ -1,16 +1,16 @@
|
|||
import { Injectable } from '@angular/core';
|
||||
import { NavigationStart, Router } from '@angular/router';
|
||||
import { ReplaySubject } from 'rxjs';
|
||||
import { filter } from 'rxjs/operators';
|
||||
import { Action, ActionFactoryService, ActionItem } from '../_services/action-factory.service';
|
||||
import {Injectable} from '@angular/core';
|
||||
import {NavigationStart, Router} from '@angular/router';
|
||||
import {ReplaySubject} from 'rxjs';
|
||||
import {filter} from 'rxjs/operators';
|
||||
import {Action, ActionFactoryService, ActionItem} from '../_services/action-factory.service';
|
||||
|
||||
type DataSource = 'volume' | 'chapter' | 'special' | 'series' | 'bookmark';
|
||||
type DataSource = 'volume' | 'chapter' | 'special' | 'series' | 'bookmark' | 'sideNavStream';
|
||||
|
||||
/**
|
||||
* Responsible for handling selections on cards. Can handle multiple card sources next to each other in different loops.
|
||||
* This will clear selections between pages.
|
||||
*
|
||||
* Remakrs: Page which renders cards is responsible for listening for shift keydown/keyup and updating our state variable.
|
||||
* Remarks: Page which renders cards is responsible for listening for shift keydown/keyup and updating our state variable.
|
||||
*/
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
|
@ -151,6 +151,10 @@ export class BulkSelectionService {
|
|||
return this.actionFactory.getBookmarkActions(callback);
|
||||
}
|
||||
|
||||
if (Object.keys(this.selectedCards).filter(item => item === 'sideNavStream').length > 0) {
|
||||
return this.applyFilterToList(this.actionFactory.getSideNavStreamActions(callback), [Action.MarkAsInvisible, Action.MarkAsVisible]);
|
||||
}
|
||||
|
||||
return this.applyFilterToList(this.actionFactory.getVolumeActions(callback), allowedActions);
|
||||
}
|
||||
|
||||
|
|
|
@ -64,7 +64,7 @@
|
|||
<div class="vr d-none d-lg-block m-2"></div>
|
||||
<div class="col-auto">
|
||||
<app-icon-and-title [label]="t('date-added-title')" [clickable]="false" fontClasses="fa-solid fa-file-import" [title]="t('date-added-title')">
|
||||
{{chapter.createdUtc | translocoDate: {dateStyle: 'short', timeStyle: 'short' } | defaultDate}}
|
||||
{{chapter.createdUtc | utcToLocalTime | translocoDate: {dateStyle: 'short', timeStyle: 'short' } | defaultDate}}
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
|
|
@ -28,11 +28,12 @@ import {MetadataDetailComponent} from "../../series-detail/_components/metadata-
|
|||
import {TranslocoModule} from "@ngneat/transloco";
|
||||
import {TranslocoLocaleModule} from "@ngneat/transloco-locale";
|
||||
import {FilterField} from "../../_models/metadata/v2/filter-field";
|
||||
import {UtcToLocalTimePipe} from "../../pipe/utc-to-local-time.pipe";
|
||||
|
||||
@Component({
|
||||
selector: 'app-entity-info-cards',
|
||||
standalone: true,
|
||||
imports: [CommonModule, IconAndTitleComponent, SafeHtmlPipe, DefaultDatePipe, BytesPipe, CompactNumberPipe, AgeRatingPipe, NgbTooltip, MetadataDetailComponent, TranslocoModule, CompactNumberPipe, TranslocoLocaleModule],
|
||||
imports: [CommonModule, IconAndTitleComponent, SafeHtmlPipe, DefaultDatePipe, BytesPipe, CompactNumberPipe, AgeRatingPipe, NgbTooltip, MetadataDetailComponent, TranslocoModule, CompactNumberPipe, TranslocoLocaleModule, UtcToLocalTimePipe],
|
||||
templateUrl: './entity-info-cards.component.html',
|
||||
styleUrls: ['./entity-info-cards.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
|
|
37
UI/Web/src/app/pipe/utc-to-local-time.pipe.ts
Normal file
37
UI/Web/src/app/pipe/utc-to-local-time.pipe.ts
Normal file
|
@ -0,0 +1,37 @@
|
|||
import { Pipe, PipeTransform } from '@angular/core';
|
||||
import { DateTime } from 'luxon';
|
||||
|
||||
type UtcToLocalTimeFormat = 'full' | 'short' | 'shortDate' | 'shortTime';
|
||||
|
||||
// FULL = 'full', // 'EEE, MMMM d, y, h:mm:ss a zzzz' - Monday, June 15, 2015 at 9:03:01 AM GMT+01:00
|
||||
// SHORT = 'short', // 'd/M/yy, h:mm - 15/6/15, 9:03
|
||||
// SHORT_DATE = 'shortDate', // 'd/M/yy' - 15/6/15
|
||||
// SHORT_TIME = 'shortTime', // 'h:mm' - 9:03
|
||||
|
||||
|
||||
@Pipe({
|
||||
name: 'utcToLocalTime',
|
||||
standalone: true
|
||||
})
|
||||
export class UtcToLocalTimePipe implements PipeTransform {
|
||||
|
||||
transform(utcDate: string, format: UtcToLocalTimeFormat = 'short'): string {
|
||||
const browserLanguage = navigator.language;
|
||||
const dateTime = DateTime.fromISO(utcDate, { zone: 'utc' }).toLocal().setLocale(browserLanguage);
|
||||
|
||||
switch (format) {
|
||||
case 'short':
|
||||
return dateTime.toLocaleString(DateTime.DATETIME_SHORT);
|
||||
case 'shortDate':
|
||||
return dateTime.toLocaleString(DateTime.DATE_MED);
|
||||
case 'shortTime':
|
||||
return dateTime.toLocaleString(DateTime.TIME_SIMPLE);
|
||||
case 'full':
|
||||
return dateTime.toString();
|
||||
default:
|
||||
console.error('No logic in place for utc date format, format: ', format);
|
||||
return utcDate;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -1,24 +1,16 @@
|
|||
<ng-container *transloco="let t; read: 'draggable-ordered-list'">
|
||||
|
||||
<ng-container *ngIf="items.length > 100; else dragList">
|
||||
<ng-container *ngIf="items.length > virtualizeAfter; else dragList">
|
||||
<div class="example-list list-group-flush">
|
||||
<virtual-scroller #scroll [items]="items" [bufferAmount]="BufferAmount" [parentScroll]="parentScroll">
|
||||
<div class="example-box" *ngFor="let item of scroll.viewPortItems; index as i; trackBy: trackByIdentity">
|
||||
<div class="d-flex list-container">
|
||||
<div class="me-3 align-middle">
|
||||
<div style="padding-top: 40px">
|
||||
<label for="reorder-{{item.order}}" class="form-label visually-hidden">{{t('reorder-label')}}</label>
|
||||
<input *ngIf="accessibilityMode" id="reorder-{{i}}" class="form-control" type="number" inputmode="numeric" min="0" [max]="items.length - 1" [value]="item.order" style="width: 60px"
|
||||
(focusout)="updateIndex(item.order, item)" (keydown.enter)="updateIndex(item.order, item)" aria-describedby="instructions">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex list-container">
|
||||
|
||||
<ng-container [ngTemplateOutlet]="handle" [ngTemplateOutletContext]="{ $implicit: item, idx: i, isVirtualized: true }"></ng-container>
|
||||
<ng-container [ngTemplateOutlet]="itemTemplate" [ngTemplateOutletContext]="{ $implicit: item, idx: i }"></ng-container>
|
||||
|
||||
<button class="btn btn-icon float-end" (click)="removeItem(item, i)" *ngIf="showRemoveButton" [disabled]="disabled">
|
||||
<i class="fa fa-times" aria-hidden="true"></i>
|
||||
<span class="visually-hidden" attr.aria-labelledby="item.id--{{i}}">{{t('remove-item-alt')}}</span>
|
||||
</button>
|
||||
<ng-container [ngTemplateOutlet]="removeBtn" [ngTemplateOutletContext]="{$implicit: item, idx: i}"></ng-container>
|
||||
</div>
|
||||
</div>
|
||||
</virtual-scroller>
|
||||
|
@ -26,29 +18,47 @@
|
|||
</ng-container>
|
||||
<ng-template #dragList>
|
||||
<div cdkDropList class="{{items.length > 0 ? 'example-list list-group-flush' : ''}}" (cdkDropListDropped)="drop($event)">
|
||||
<div class="example-box" *ngFor="let item of items; index as i" cdkDrag [cdkDragData]="item" cdkDragBoundary=".example-list">
|
||||
<div class="example-box" *ngFor="let item of items; index as i" cdkDrag
|
||||
[cdkDragData]="item" cdkDragBoundary=".example-list"
|
||||
[cdkDragDisabled]="accessibilityMode || disabled || bulkMode" cdkDragPreviewContainer="parent">
|
||||
<div class="d-flex list-container">
|
||||
<div class="me-3 align-middle">
|
||||
<div class="align-middle" style="padding-top: 40px" *ngIf="accessibilityMode">
|
||||
<label for="reorder-{{i}}" class="form-label visually-hidden">{{t('reorder-label')}}</label>
|
||||
<input id="reorder-{{i}}" class="form-control" type="number" inputmode="numeric" min="0" [max]="items.length - 1" [value]="i" style="width: 60px"
|
||||
(focusout)="updateIndex(i, item)" (keydown.enter)="updateIndex(i, item)" aria-describedby="instructions">
|
||||
</div>
|
||||
<i *ngIf="!accessibilityMode && !disabled" class="fa fa-grip-vertical drag-handle" aria-hidden="true" cdkDragHandle></i>
|
||||
</div>
|
||||
|
||||
<ng-container [ngTemplateOutlet]="handle" [ngTemplateOutletContext]="{ $implicit: item, idx: i, isVirtualized: false }"></ng-container>
|
||||
<ng-container [ngTemplateOutlet]="itemTemplate" [ngTemplateOutletContext]="{ $implicit: item, idx: i }"></ng-container>
|
||||
|
||||
<button class="btn btn-icon float-end" (click)="removeItem(item, i)" *ngIf="showRemoveButton" [disabled]="disabled">
|
||||
<i class="fa fa-times" aria-hidden="true"></i>
|
||||
<span class="visually-hidden" attr.aria-labelledby="item.id--{{i}}">{{t('remove-item-alt')}}</span>
|
||||
</button>
|
||||
<ng-container [ngTemplateOutlet]="removeBtn" [ngTemplateOutletContext]="{$implicit: item, idx: i}"></ng-container>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #removeBtn let-item let-idx>
|
||||
<button class="btn btn-icon float-end" (click)="removeItem(item, idx)" *ngIf="showRemoveButton" [disabled]="disabled">
|
||||
<i class="fa fa-times" aria-hidden="true"></i>
|
||||
<span class="visually-hidden" attr.aria-labelledby="item.id--{{idx}}">{{t('remove-item-alt')}}</span>
|
||||
</button>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #handle let-item let-idx="idx" let-isVirtualized="isVirtualized">
|
||||
<div class="me-3 align-middle">
|
||||
<div class="align-middle" [ngClass]="{'accessibility-padding': accessibilityMode, 'bulk-padding': bulkMode}" *ngIf="accessibilityMode || bulkMode">
|
||||
<ng-container *ngIf="accessibilityMode">
|
||||
<label for="reorder-{{idx}}" class="form-label visually-hidden">{{t('reorder-label')}}</label>
|
||||
<input id="reorder-{{idx}}" class="form-control manual-input" type="number" inputmode="numeric" min="0"
|
||||
[max]="items.length - 1" [value]="idx"
|
||||
(focusout)="updateIndex(idx, item)" (keydown.enter)="updateIndex(idx, item)" aria-describedby="instructions">
|
||||
</ng-container>
|
||||
<ng-container *ngIf="bulkMode">
|
||||
<label for="select-{{idx}}" class="form-label visually-hidden">{{t('bulk-select-label')}}</label>
|
||||
<input id="select-{{idx}}" class="form-check-input mt-0" type="checkbox" (change)="selectItem($event, item, idx)"
|
||||
[value]="bulkSelectionService.isCardSelected('sideNavStream', idx)">
|
||||
</ng-container>
|
||||
|
||||
|
||||
</div>
|
||||
<i *ngIf="!isVirtualized && !(accessibilityMode || bulkMode) && !disabled" class="fa fa-grip-vertical drag-handle" aria-hidden="true" cdkDragHandle></i>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
<p class="visually-hidden" id="instructions">
|
||||
{{t('instructions-alt')}}
|
||||
|
|
|
@ -61,6 +61,12 @@
|
|||
width: 100%;
|
||||
}
|
||||
|
||||
.accessibility-padding {
|
||||
padding-top: 12px;
|
||||
}
|
||||
.bulk-padding {
|
||||
padding-top: 20px;
|
||||
}
|
||||
|
||||
|
||||
.virtual-scroller, virtual-scroller {
|
||||
|
@ -71,3 +77,7 @@
|
|||
virtual-scroller.empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.manual-input {
|
||||
width: 83px;
|
||||
}
|
||||
|
|
|
@ -1,8 +1,22 @@
|
|||
import { CdkDragDrop, moveItemInArray, CdkDropList, CdkDrag, CdkDragHandle } from '@angular/cdk/drag-drop';
|
||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ContentChild, EventEmitter, Input, Output, TemplateRef, TrackByFunction } from '@angular/core';
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
ContentChild,
|
||||
EventEmitter,
|
||||
inject,
|
||||
Input,
|
||||
Output,
|
||||
TemplateRef,
|
||||
TrackByFunction
|
||||
} from '@angular/core';
|
||||
import { VirtualScrollerModule } from '@iharbeck/ngx-virtual-scroller';
|
||||
import { NgIf, NgFor, NgTemplateOutlet } from '@angular/common';
|
||||
import {NgIf, NgFor, NgTemplateOutlet, NgClass} from '@angular/common';
|
||||
import {TranslocoDirective} from "@ngneat/transloco";
|
||||
import {BulkSelectionService} from "../../../cards/bulk-selection.service";
|
||||
import {SeriesCardComponent} from "../../../cards/series-card/series-card.component";
|
||||
import {SideNavStream} from "../../../_models/sidenav/sidenav-stream";
|
||||
|
||||
export interface IndexUpdateEvent {
|
||||
fromPosition: number;
|
||||
|
@ -22,10 +36,14 @@ export interface ItemRemoveEvent {
|
|||
styleUrls: ['./draggable-ordered-list.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: true,
|
||||
imports: [NgIf, VirtualScrollerModule, NgFor, NgTemplateOutlet, CdkDropList, CdkDrag, CdkDragHandle, TranslocoDirective]
|
||||
imports: [NgIf, VirtualScrollerModule, NgFor, NgTemplateOutlet, CdkDropList, CdkDrag, CdkDragHandle, TranslocoDirective, NgClass, SeriesCardComponent]
|
||||
})
|
||||
export class DraggableOrderedListComponent {
|
||||
|
||||
/**
|
||||
* After this many elements, drag and drop is disabled and we use a virtualized list instead
|
||||
*/
|
||||
@Input() virtualizeAfter = 100;
|
||||
@Input() accessibilityMode: boolean = false;
|
||||
/**
|
||||
* Shows the remove button on the list item
|
||||
|
@ -40,11 +58,17 @@ export class DraggableOrderedListComponent {
|
|||
* Disables drag and drop functionality. Useful if a filter is present which will skew actual index.
|
||||
*/
|
||||
@Input() disabled: boolean = false;
|
||||
/**
|
||||
* When enabled, draggability is disabled and a checkbox renders instead of order box or drag handle
|
||||
*/
|
||||
@Input() bulkMode: boolean = false;
|
||||
@Input() trackByIdentity: TrackByFunction<any> = (index: number, item: any) => `${item.id}_${item.order}_${item.title}`;
|
||||
@Output() orderUpdated: EventEmitter<IndexUpdateEvent> = new EventEmitter<IndexUpdateEvent>();
|
||||
@Output() itemRemove: EventEmitter<ItemRemoveEvent> = new EventEmitter<ItemRemoveEvent>();
|
||||
@ContentChild('draggableItem') itemTemplate!: TemplateRef<any>;
|
||||
|
||||
public readonly bulkSelectionService = inject(BulkSelectionService);
|
||||
|
||||
get BufferAmount() {
|
||||
return Math.min(this.items.length / 20, 20);
|
||||
}
|
||||
|
@ -85,4 +109,11 @@ export class DraggableOrderedListComponent {
|
|||
});
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
selectItem(updatedVal: Event, item: SideNavStream, index: number) {
|
||||
const boolVal = (updatedVal.target as HTMLInputElement).value == 'true';
|
||||
|
||||
this.bulkSelectionService.handleCardSelection('sideNavStream', index, this.items.length, boolVal);
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
<h4 class="modal-title">{{t('title-' + activeTab)}}</h4>
|
||||
<button type="button" class="btn-close" [attr.aria-label]="t('close')" (click)="close()"></button>
|
||||
</div>
|
||||
<div class="modal-body scrollable-modal {{utilityService.getActiveBreakpoint() === Breakpoint.Mobile ? '' : 'd-flex'}}">
|
||||
<div #modalBody class="modal-body scrollable-modal {{utilityService.getActiveBreakpoint() === Breakpoint.Mobile ? '' : 'd-flex'}}">
|
||||
<ul ngbNav #nav="ngbNav" [(activeId)]="activeTab" class="nav-pills" orientation="{{utilityService.getActiveBreakpoint() === Breakpoint.Mobile ? 'horizontal' : 'vertical'}}" style="min-width: 154px;">
|
||||
<li [ngbNavItem]="TabID.Dashboard">
|
||||
<a ngbNavLink>{{t(TabID.Dashboard)}}</a>
|
||||
|
|
|
@ -49,6 +49,12 @@ export class CustomizeDashboardStreamsComponent {
|
|||
constructor(public modal: NgbActiveModal) {
|
||||
forkJoin([this.dashboardService.getDashboardStreams(false), this.filterService.getAllFilters()]).subscribe(results => {
|
||||
this.items = results[0];
|
||||
|
||||
// After 100 items, drag and drop is disabled to use virtualization
|
||||
if (this.items.length > 100) {
|
||||
this.accessibilityMode = true;
|
||||
}
|
||||
|
||||
const smartFilterStreams = new Set(results[0].filter(d => !d.isProvided).map(d => d.name));
|
||||
this.smartFilters = results[1].filter(d => !smartFilterStreams.has(d.name));
|
||||
this.cdRef.markForCheck();
|
||||
|
|
|
@ -10,14 +10,25 @@
|
|||
<span role="alert" class="mt-1" *ngIf="listForm.get('filterSideNavStream')?.value">{{t('reorder-when-filter-present')}}</span>
|
||||
</div>
|
||||
<div class="col-3">
|
||||
<div class="form-check form-check-inline" style="margin-top: 35px; margin-left: 10px">
|
||||
<input class="form-check-input" type="checkbox" id="accessibility-mode" [value]="accessibilityMode" (change)="updateAccessibilityMode()">
|
||||
<label class="form-check-label" for="accessibility-mode">{{t('order-numbers-label')}}</label>
|
||||
</div>
|
||||
<form [formGroup]="pageOperationsForm">
|
||||
<div class="form-check form-check-inline" style="margin-top: 23px; margin-left: 10px">
|
||||
<input class="form-check-input" type="checkbox" id="accessibility-mode" formControlName="accessibilityMode">
|
||||
<label class="form-check-label" for="accessibility-mode">{{t('order-numbers-label')}}</label>
|
||||
</div>
|
||||
<div class="form-check form-check-inline" style="margin-left: 10px">
|
||||
<input class="form-check-input" type="checkbox" id="bulk-mode" formControlName="bulkMode" >
|
||||
<label class="form-check-label" for="bulk-mode">{{t('bulk-mode-label')}}</label>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<app-draggable-ordered-list [items]="items | filter: filterSideNavStreams" (orderUpdated)="orderUpdated($event)" [accessibilityMode]="accessibilityMode"
|
||||
[showRemoveButton]="false" [disabled]="listForm.get('filterSideNavStream')?.value">
|
||||
<app-bulk-operations [modalMode]="true" [topOffset]="0" [actionCallback]="bulkActionCallback"></app-bulk-operations>
|
||||
<app-draggable-ordered-list [items]="items | filter: filterSideNavStreams" (orderUpdated)="orderUpdated($event)"
|
||||
[accessibilityMode]="pageOperationsForm.get('accessibilityMode')!.value"
|
||||
[showRemoveButton]="false" [disabled]="listForm.get('filterSideNavStream')?.value"
|
||||
[bulkMode]="pageOperationsForm.get('bulkMode')!.value"
|
||||
[virtualizeAfter]="100"
|
||||
>
|
||||
<ng-template #draggableItem let-position="idx" let-item>
|
||||
<app-sidenav-stream-list-item [item]="item" [position]="position" (hide)="updateVisibility($event, position)"></app-sidenav-stream-list-item>
|
||||
</ng-template>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import {ChangeDetectionStrategy, ChangeDetectorRef, Component, inject} from '@angular/core';
|
||||
import {ChangeDetectionStrategy, ChangeDetectorRef, Component, DestroyRef, inject, OnDestroy} from '@angular/core';
|
||||
import {CommonModule} from '@angular/common';
|
||||
import {SmartFilter} from "../../../_models/metadata/v2/smart-filter";
|
||||
import {FilterService} from "../../../_services/filter.service";
|
||||
|
@ -11,36 +11,43 @@ import {
|
|||
import {SideNavStream} from "../../../_models/sidenav/sidenav-stream";
|
||||
import {NavService} from "../../../_services/nav.service";
|
||||
import {DashboardStreamListItemComponent} from "../dashboard-stream-list-item/dashboard-stream-list-item.component";
|
||||
import {CommonStream} from "../../../_models/common-stream";
|
||||
import {TranslocoDirective} from "@ngneat/transloco";
|
||||
import {SidenavStreamListItemComponent} from "../sidenav-stream-list-item/sidenav-stream-list-item.component";
|
||||
import {ExternalSourceService} from "../../../external-source.service";
|
||||
import {ExternalSource} from "../../../_models/sidenav/external-source";
|
||||
import {StreamType} from "../../../_models/dashboard/stream-type.enum";
|
||||
import {SideNavStreamType} from "../../../_models/sidenav/sidenav-stream-type.enum";
|
||||
import {FormControl, FormGroup, ReactiveFormsModule} from "@angular/forms";
|
||||
import {FilterPipe} from "../../../pipe/filter.pipe";
|
||||
import {BulkOperationsComponent} from "../../../cards/bulk-operations/bulk-operations.component";
|
||||
import {Action, ActionItem} from "../../../_services/action-factory.service";
|
||||
import {BulkSelectionService} from "../../../cards/bulk-selection.service";
|
||||
import {filter, tap} from "rxjs/operators";
|
||||
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
|
||||
|
||||
@Component({
|
||||
selector: 'app-customize-sidenav-streams',
|
||||
standalone: true,
|
||||
imports: [CommonModule, DraggableOrderedListComponent, DashboardStreamListItemComponent, TranslocoDirective, SidenavStreamListItemComponent, ReactiveFormsModule, FilterPipe],
|
||||
imports: [CommonModule, DraggableOrderedListComponent, DashboardStreamListItemComponent, TranslocoDirective, SidenavStreamListItemComponent, ReactiveFormsModule, FilterPipe, BulkOperationsComponent],
|
||||
templateUrl: './customize-sidenav-streams.component.html',
|
||||
styleUrls: ['./customize-sidenav-streams.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class CustomizeSidenavStreamsComponent {
|
||||
export class CustomizeSidenavStreamsComponent implements OnDestroy {
|
||||
|
||||
//@Input({required: true}) parentScrollElem!: Element | Window;
|
||||
items: SideNavStream[] = [];
|
||||
smartFilters: SmartFilter[] = [];
|
||||
externalSources: ExternalSource[] = [];
|
||||
accessibilityMode: boolean = false;
|
||||
|
||||
listForm: FormGroup = new FormGroup({
|
||||
'filterSideNavStream': new FormControl('', []),
|
||||
'filterSmartFilter': new FormControl('', []),
|
||||
'filterExternalSource': new FormControl('', []),
|
||||
});
|
||||
pageOperationsForm: FormGroup = new FormGroup({
|
||||
'accessibilityMode': new FormControl(false, []),
|
||||
'bulkMode': new FormControl(false, [])
|
||||
})
|
||||
|
||||
filterSideNavStreams = (listItem: SideNavStream) => {
|
||||
const filterVal = (this.listForm.value.filterSideNavStream || '').toLowerCase();
|
||||
|
@ -57,17 +64,87 @@ export class CustomizeSidenavStreamsComponent {
|
|||
return listItem.name.toLowerCase().indexOf(filterVal) >= 0;
|
||||
}
|
||||
|
||||
bulkActionCallback = (action: ActionItem<SideNavStream>, data: SideNavStream) => {
|
||||
const streams = this.bulkSelectionService.getSelectedCardsForSource('sideNavStream').map(index => this.items[parseInt(index, 10)]);
|
||||
let visibleState = false;
|
||||
switch (action.action) {
|
||||
case Action.MarkAsVisible:
|
||||
visibleState = true;
|
||||
break;
|
||||
case Action.MarkAsInvisible:
|
||||
visibleState = false;
|
||||
break;
|
||||
}
|
||||
|
||||
for(let index of this.bulkSelectionService.getSelectedCardsForSource('sideNavStream').map(s => parseInt(s, 10))) {
|
||||
this.items[index].visible = visibleState;
|
||||
this.items[index] = {...this.items[index]};
|
||||
}
|
||||
this.cdRef.markForCheck();
|
||||
// Make bulk call
|
||||
this.sideNavService.bulkToggleSideNavStreamVisibility(streams.map(s => s.id), visibleState).subscribe(() => this.bulkSelectionService.deselectAll());
|
||||
}
|
||||
|
||||
|
||||
private readonly sideNavService = inject(NavService);
|
||||
private readonly filterService = inject(FilterService);
|
||||
private readonly externalSourceService = inject(ExternalSourceService);
|
||||
private readonly cdRef = inject(ChangeDetectorRef);
|
||||
private readonly destroyRef = inject(DestroyRef);
|
||||
private readonly bulkSelectionService = inject(BulkSelectionService);
|
||||
|
||||
constructor(public modal: NgbActiveModal) {
|
||||
|
||||
this.pageOperationsForm.get('accessibilityMode')?.valueChanges.pipe(
|
||||
tap(_ => {
|
||||
const accessibleValue = this.pageOperationsForm.get('accessibilityMode')?.value;
|
||||
if (accessibleValue) {
|
||||
if (this.pageOperationsForm.get('bulkMode')?.disabled) return;
|
||||
this.pageOperationsForm.get('bulkMode')?.disable();
|
||||
} else {
|
||||
if (!this.pageOperationsForm.get('bulkMode')?.disabled) return;
|
||||
this.pageOperationsForm.get('bulkMode')?.enable();
|
||||
}
|
||||
this.cdRef.markForCheck();
|
||||
}),
|
||||
takeUntilDestroyed(this.destroyRef)
|
||||
).subscribe();
|
||||
|
||||
this.pageOperationsForm.get('bulkMode')?.valueChanges.pipe(
|
||||
tap(_ => {
|
||||
const bulkValue = this.pageOperationsForm.get('bulkMode')?.value;
|
||||
if (bulkValue) {
|
||||
if (this.pageOperationsForm.get('accessibilityMode')?.disabled) return;
|
||||
this.pageOperationsForm.get('accessibilityMode')?.disable();
|
||||
} else {
|
||||
if (this.pageOperationsForm.get('accessibilityMode')?.disabled) return;
|
||||
this.pageOperationsForm.get('accessibilityMode')?.enable();
|
||||
}
|
||||
}),
|
||||
takeUntilDestroyed(this.destroyRef)
|
||||
).subscribe();
|
||||
|
||||
this.pageOperationsForm.valueChanges.pipe(
|
||||
tap(_ => {
|
||||
if (this.pageOperationsForm.value.accessibilityMode || this.pageOperationsForm.value.bulkMode) {
|
||||
this.listForm.get('filterSideNavStream')?.disable();
|
||||
return;
|
||||
}
|
||||
this.listForm.get('filterSideNavStream')?.enable();
|
||||
}),
|
||||
takeUntilDestroyed(this.destroyRef)
|
||||
).subscribe();
|
||||
|
||||
forkJoin([this.sideNavService.getSideNavStreams(false),
|
||||
this.filterService.getAllFilters(), this.externalSourceService.getExternalSources()
|
||||
]).subscribe(results => {
|
||||
this.items = results[0];
|
||||
|
||||
// After 100 items, drag and drop is disabled to use virtualization
|
||||
if (this.items.length > 100) {
|
||||
this.pageOperationsForm.get('accessibilityMode')?.setValue(true);
|
||||
}
|
||||
|
||||
const existingSmartFilterStreams = new Set(results[0].filter(d => !d.isProvided && d.streamType === SideNavStreamType.SmartFilter).map(d => d.name));
|
||||
this.smartFilters = results[1].filter(d => !existingSmartFilterStreams.has(d.name));
|
||||
|
||||
|
@ -77,6 +154,10 @@ export class CustomizeSidenavStreamsComponent {
|
|||
});
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.bulkSelectionService.deselectAll();
|
||||
}
|
||||
|
||||
resetSideNavFilter() {
|
||||
this.listForm.get('filterSideNavStream')?.setValue('');
|
||||
this.cdRef.markForCheck();
|
||||
|
@ -108,11 +189,6 @@ export class CustomizeSidenavStreamsComponent {
|
|||
});
|
||||
}
|
||||
|
||||
updateAccessibilityMode() {
|
||||
this.accessibilityMode = !this.accessibilityMode;
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
|
||||
orderUpdated(event: IndexUpdateEvent) {
|
||||
this.sideNavService.updateSideNavStreamPosition(event.item.name, event.item.id, event.fromPosition, event.toPosition).subscribe(() => {
|
||||
|
@ -128,8 +204,8 @@ export class CustomizeSidenavStreamsComponent {
|
|||
updateVisibility(item: SideNavStream, position: number) {
|
||||
const stream = this.items.filter(s => s.id == item.id)[0];
|
||||
stream.visible = !stream.visible;
|
||||
this.sideNavService.updateSideNavStream(stream).subscribe();
|
||||
this.cdRef.markForCheck();
|
||||
this.sideNavService.updateSideNavStream(stream).subscribe();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -24,5 +24,10 @@
|
|||
(sourceDelete)="deleteSource(idx, $event)"
|
||||
[isViewMode]="externalSource.id !== 0"></app-edit-external-source-item>
|
||||
</ng-container>
|
||||
<ul>
|
||||
<li class="list-group-item" *ngIf="externalSources.length === 0">
|
||||
{{t('no-data')}}
|
||||
</li>
|
||||
</ul>
|
||||
</ng-container>
|
||||
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
ul {
|
||||
margin:0;
|
||||
padding: 0;
|
||||
|
||||
li {
|
||||
padding: 0.5rem 1rem;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
border-radius: 5px;
|
||||
margin: 5px 0;
|
||||
color: var(--list-group-hover-text-color);
|
||||
background-color: var(--card-bg-color);
|
||||
|
||||
span {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -12,7 +12,7 @@ ul {
|
|||
border-radius: 5px;
|
||||
margin: 5px 0;
|
||||
color: var(--list-group-hover-text-color);
|
||||
background-color: var(--list-group-hover-bg-color);
|
||||
background-color: var(--card-bg-color);
|
||||
|
||||
span {
|
||||
cursor: pointer;
|
||||
|
|
|
@ -159,11 +159,11 @@ export class SideNavComponent implements OnInit {
|
|||
}
|
||||
|
||||
handleHomeActions() {
|
||||
this.ngbModal.open(CustomizeDashboardModalComponent, {size: 'xl'});
|
||||
this.ngbModal.open(CustomizeDashboardModalComponent, {size: 'xl', fullscreen: 'md'});
|
||||
}
|
||||
|
||||
importCbl() {
|
||||
this.ngbModal.open(ImportCblModalComponent, {size: 'xl'});
|
||||
this.ngbModal.open(ImportCblModalComponent, {size: 'xl', fullscreen: 'md'});
|
||||
}
|
||||
|
||||
performAction(action: ActionItem<Library>, library: Library) {
|
||||
|
|
|
@ -2,7 +2,7 @@ import {
|
|||
ChangeDetectionStrategy,
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
DestroyRef,
|
||||
DestroyRef, importProvidersFrom,
|
||||
inject,
|
||||
OnDestroy,
|
||||
OnInit
|
||||
|
@ -49,6 +49,11 @@ import { SideNavCompanionBarComponent } from '../../sidenav/_components/side-nav
|
|||
import {LocalizationService} from "../../_services/localization.service";
|
||||
import {Language} from "../../_models/metadata/language";
|
||||
import {translate, TranslocoDirective} from "@ngneat/transloco";
|
||||
import {
|
||||
provideTranslocoPersistTranslations,
|
||||
TranslocoPersistTranslations
|
||||
} from "@ngneat/transloco-persist-translations";
|
||||
import {HttpLoader} from "../../../httpLoader";
|
||||
|
||||
enum AccordionPanelID {
|
||||
ImageReader = 'image-reader',
|
||||
|
@ -76,7 +81,7 @@ enum FragmentID {
|
|||
ChangePasswordComponent, ChangeAgeRestrictionComponent, AnilistKeyComponent, ReactiveFormsModule, NgbAccordionDirective, NgbAccordionItem, NgbAccordionHeader,
|
||||
NgbAccordionToggle, NgbAccordionButton, NgbCollapse, NgbAccordionCollapse, NgbAccordionBody, NgbTooltip, NgTemplateOutlet, ColorPickerModule, ApiKeyComponent,
|
||||
ThemeManagerComponent, ManageDevicesComponent, UserStatsComponent, UserScrobbleHistoryComponent, UserHoldsComponent, NgbNavOutlet, TitleCasePipe, SentenceCasePipe,
|
||||
TranslocoDirective]
|
||||
TranslocoDirective],
|
||||
})
|
||||
export class UserPreferencesComponent implements OnInit, OnDestroy {
|
||||
|
||||
|
@ -114,20 +119,22 @@ export class UserPreferencesComponent implements OnInit, OnDestroy {
|
|||
opdsEnabled: boolean = false;
|
||||
opdsUrl: string = '';
|
||||
makeUrl: (val: string) => string = (val: string) => { return this.opdsUrl; };
|
||||
|
||||
private readonly destroyRef = inject(DestroyRef);
|
||||
|
||||
get AccordionPanelID() {
|
||||
return AccordionPanelID;
|
||||
}
|
||||
|
||||
get FragmentID() {
|
||||
return FragmentID;
|
||||
}
|
||||
private readonly accountService = inject(AccountService);
|
||||
private readonly toastr = inject(ToastrService);
|
||||
private readonly bookService = inject(BookService);
|
||||
private readonly titleService = inject(Title);
|
||||
private readonly route = inject(ActivatedRoute);
|
||||
private readonly settingsService = inject(SettingsService);
|
||||
private readonly router = inject(Router);
|
||||
private readonly cdRef = inject(ChangeDetectorRef);
|
||||
private readonly localizationService = inject(LocalizationService);
|
||||
protected readonly AccordionPanelID = AccordionPanelID;
|
||||
protected readonly FragmentID = FragmentID;
|
||||
|
||||
|
||||
constructor(private accountService: AccountService, private toastr: ToastrService, private bookService: BookService,
|
||||
private titleService: Title, private route: ActivatedRoute, private settingsService: SettingsService,
|
||||
private router: Router, private readonly cdRef: ChangeDetectorRef, public localizationService: LocalizationService) {
|
||||
constructor() {
|
||||
this.fontFamilies = this.bookService.getFontFamilies().map(f => f.title);
|
||||
this.cdRef.markForCheck();
|
||||
|
||||
|
@ -306,6 +313,7 @@ export class UserPreferencesComponent implements OnInit, OnDestroy {
|
|||
this.toastr.success(translate('user-preferences.success-toast'));
|
||||
if (this.user) {
|
||||
this.user.preferences = updatedPrefs;
|
||||
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
this.resetForm();
|
||||
|
|
|
@ -1315,6 +1315,7 @@
|
|||
"draggable-ordered-list": {
|
||||
"instructions-alt": "When you put a number in the reorder input, the item will be inserted at that location and all other items will have their order updated.",
|
||||
"reorder-label": "Reorder",
|
||||
"bulk-select-label": "Bulk Select item",
|
||||
"remove-item-alt": "Remove item"
|
||||
},
|
||||
|
||||
|
@ -1771,15 +1772,17 @@
|
|||
"smart-filters-title": "Smart Filters",
|
||||
"external-sources-title": "{{customize-dashboard-modal.external-sources}}",
|
||||
"reorder-when-filter-present": "You cannot reorder items via drag & drop while a filter is present. Use {{customize-sidenav-streams.order-numbers-label}}",
|
||||
"order-numbers-label": "{{reading-list-detail.order-numbers-label}}"
|
||||
"order-numbers-label": "{{reading-list-detail.order-numbers-label}}",
|
||||
"bulk-mode-label": "Bulk Mode"
|
||||
},
|
||||
|
||||
"manage-external-sources": {
|
||||
"add-source": "Add",
|
||||
"help-link": "More information",
|
||||
"description": "Add External Servers to your account and then add them to your Side Nav for a quick way to switch between your and your friend's server.",
|
||||
"description": "Enhance your experience by adding external servers and conveniently include them in your Side Nav for quick access to both your server and your friend's server.",
|
||||
"clear": "{{common.clear}}",
|
||||
"filter": "{{common.filter}}"
|
||||
"filter": "{{common.filter}}",
|
||||
"no-data": "No External Sources exist"
|
||||
},
|
||||
|
||||
"manage-smart-filters": {
|
||||
|
@ -1978,7 +1981,9 @@
|
|||
"add-rule-group-and": "Add Rule Group (AND)",
|
||||
"add-rule-group-or": "Add Rule Group (OR)",
|
||||
"remove-rule-group": "Remove Rule Group",
|
||||
"customize": "Customize"
|
||||
"customize": "Customize",
|
||||
"mark-visible": "Mark as Visible",
|
||||
"mark-invisible": "Mark as Invisible"
|
||||
},
|
||||
|
||||
"preferences": {
|
||||
|
|
|
@ -15,8 +15,8 @@ import { JwtInterceptor } from './app/_interceptors/jwt.interceptor';
|
|||
import { ErrorInterceptor } from './app/_interceptors/error.interceptor';
|
||||
import {HTTP_INTERCEPTORS, withInterceptorsFromDi, provideHttpClient} from '@angular/common/http';
|
||||
import {
|
||||
provideTransloco,
|
||||
TranslocoService
|
||||
provideTransloco, TranslocoConfig,
|
||||
TranslocoService
|
||||
} from "@ngneat/transloco";
|
||||
import {environment} from "./environments/environment";
|
||||
import {HttpLoader} from "./httpLoader";
|
||||
|
@ -96,7 +96,7 @@ const languageCodes = [
|
|||
'syr', 'syr-SY', 'ta', 'ta-IN', 'te', 'te-IN', 'th', 'th-TH', 'tl', 'tl-PH', 'tn',
|
||||
'tn-ZA', 'tr', 'tr-TR', 'tt', 'tt-RU', 'ts', 'uk', 'uk-UA', 'ur', 'ur-PK', 'uz',
|
||||
'uz-UZ', 'uz-UZ', 'vi', 'vi-VN', 'xh', 'xh-ZA', 'zh', 'zh-CN', 'zh-HK', 'zh-MO',
|
||||
'zh-SG', 'zh-TW', 'zu', 'zu-ZA', 'zh_Hans',
|
||||
'zh-SG', 'zh-TW', 'zu', 'zu-ZA', 'zh_Hans', 'zh_Hant',
|
||||
];
|
||||
|
||||
const translocoOptions = {
|
||||
|
@ -109,8 +109,8 @@ const translocoOptions = {
|
|||
missingHandler: {
|
||||
useFallbackTranslation: true,
|
||||
allowEmpty: false,
|
||||
}
|
||||
}
|
||||
},
|
||||
} as TranslocoConfig
|
||||
};
|
||||
|
||||
bootstrapApplication(AppComponent, {
|
||||
|
@ -133,7 +133,8 @@ bootstrapApplication(AppComponent, {
|
|||
}),
|
||||
provideTranslocoPersistTranslations({
|
||||
loader: HttpLoader,
|
||||
storage: { useValue: localStorage }
|
||||
storage: { useValue: localStorage },
|
||||
ttl: 604800
|
||||
}),
|
||||
provideTranslocoPersistLang({
|
||||
storage: {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue