Localization - First Pass (#2174)
* Started designing the backend localization service * Worked in Transloco for initial PoC * Worked in Transloco for initial PoC * Translated the login screen * translated dashboard screen * Started work on the backend * Fixed a logic bug * translated edit-user screen * Hooked up the backend for having a locale property. * Hooked up the ability to view the available locales and switch to them. * Made the localization service languages be derived from what's in langs/ directory. * Fixed up localization switching * Switched when we check for a license on UI bootstrap * Tweaked some code * Fixed the bug where dashboard wasn't loading and made it so language switching is working. * Fixed a bug on dashboard with languagePath * Converted user-scrobble-history.component.html * Converted spoiler.component.html * Converted review-series-modal.component.html * Converted review-card-modal.component.html * Updated the readme * Translated using Weblate (English) Currently translated at 100.0% (54 of 54 strings) Translation: Kavita/ui Translate-URL: https://hosted.weblate.org/projects/kavita/ui/en/ * Converted review-card.component.html * Deleted dead component * Converted want-to-read.component.html * Added translation using Weblate (Korean) * Translated using Weblate (Spanish) Currently translated at 40.7% (22 of 54 strings) Translation: Kavita/ui Translate-URL: https://hosted.weblate.org/projects/kavita/ui/es/ * Translated using Weblate (Korean) Currently translated at 62.9% (34 of 54 strings) Translation: Kavita/ui Translate-URL: https://hosted.weblate.org/projects/kavita/ui/ko/ * Converted user-preferences.component.html * Translated using Weblate (Korean) Currently translated at 92.5% (50 of 54 strings) Translation: Kavita/ui Translate-URL: https://hosted.weblate.org/projects/kavita/ui/ko/ * Converted user-holds.component.html * Converted theme-manager.component.html * Converted restriction-selector.component.html * Converted manage-devices.component.html * Converted edit-device.component.html * Converted change-password.component.html * Converted change-email.component.html * Converted change-age-restriction.component.html * Converted api-key.component.html * Converted anilist-key.component.html * Converted typeahead.component.html * Converted user-stats-info-cards.component.html * Converted user-stats.component.html * Converted top-readers.component.html * Converted some pipes and ensure translation is loaded before the app. * Finished all but one pipe for localization * Converted directory-picker.component.html * Converted library-access-modal.component.html * Converted a few components * Converted a few components * Converted a few components * Converted a few components * Converted a few components * Merged weblate in * ... -> … update * Updated the readme * Updateded all fonts to be woff2 * Cleaned up some strings to increase re-use * Removed an old flow (that doesn't exist in backend any longer) from when we introduced emails on Kavita. * Converted Series detail * Lots more converted * Lots more converted & hooked up the ability to flatten during prod build the language files. * Lots more converted * Lots more converted & fixed a bunch of broken pipes due to inject() * Lots more converted * Lots more converted * Lots more converted & fixed some bad keys * Lots more converted * Fixed some bugs with admin dasbhoard nested tabs not rendering on first load due to not using onpush change detection * Fixed up some localization errors and fixed forgot password error when the user doesn't have change password permission * Fixed a stupid build issue again * Started adding errors for interceptor and backend. * Finished off manga-reader * More translations * Few fixes * Fixed a bug where character tag badges weren't showing the name on chapter info * All components are translated * All toasts are translated * All confirm/alerts are translated * Trying something new for the backend * Migrated the localization strings for the backend into a new file. * Updated the localization service to be able to do backend localization with fallback to english. * Cleaned up some external reviews code to reduce looping * Localized AccountController.cs * 60% done with controllers * All controllers are done * All KavitaExceptions are covered * Some shakeout fixes * Prep for initial merge * Everything is done except options and basic shakeout proves response times are good. Unit tests are broken. * Fixed up the unit tests * All unit tests are now working * Removed some quantifier * I'm not sure I can support localization for some Volume/Chapter/Book strings within the codebase. --------- Co-authored-by: Robbie Davis <robbie@therobbiedavis.com> Co-authored-by: majora2007 <kavitareader@gmail.com> Co-authored-by: expertjun <jtrobin@naver.com> Co-authored-by: ThePromidius <thepromidiusyt@gmail.com>
This commit is contained in:
parent
670bf82c38
commit
3b23d63234
389 changed files with 13652 additions and 7925 deletions
|
|
@ -1,23 +1,23 @@
|
|||
<ng-container>
|
||||
<ng-container *transloco="let t; read: 'draggable-ordered-list'">
|
||||
|
||||
<ng-container *ngIf="items.length > 100; else dragList">
|
||||
<div class="example-list list-group-flush">
|
||||
<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-{{i}}" class="form-label visually-hidden">Reorder</label>
|
||||
<input *ngIf="accessibilityMode" id="reorder-{{i}}" class="form-control" type="number" inputmode="numeric" min="0" [max]="items.length - 1" [value]="i" style="width: 60px"
|
||||
<label for="reorder-{{i}}" class="form-label visually-hidden">{{t('reorder')}}</label>
|
||||
<input *ngIf="accessibilityMode" 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>
|
||||
</div>
|
||||
|
||||
|
||||
<ng-container [ngTemplateOutlet]="itemTemplate" [ngTemplateOutletContext]="{ $implicit: item, idx: i }"></ng-container>
|
||||
|
||||
|
||||
<button class="btn btn-icon float-end" (click)="removeItem(item, i)" *ngIf="showRemoveButton">
|
||||
<i class="fa fa-times" aria-hidden="true"></i>
|
||||
<span class="visually-hidden" attr.aria-labelledby="item.id--{{i}}">Remove item</span>
|
||||
<span class="visually-hidden" attr.aria-labelledby="item.id--{{i}}">{{t('remove-item-alt')}}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -25,34 +25,34 @@
|
|||
</div>
|
||||
</ng-container>
|
||||
<ng-template #dragList>
|
||||
<div cdkDropList class="{{items.length > 0 ? 'example-list list-group-flush' : ''}}" (cdkDropListDropped)="drop($event)">
|
||||
<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="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">Reorder</label>
|
||||
<input id="reorder-{{i}}" class="form-control" type="number" inputmode="numeric" min="0" [max]="items.length - 1" [value]="i" style="width: 60px"
|
||||
<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" class="fa fa-grip-vertical drag-handle" aria-hidden="true" cdkDragHandle></i>
|
||||
</div>
|
||||
|
||||
|
||||
<ng-container [ngTemplateOutlet]="itemTemplate" [ngTemplateOutletContext]="{ $implicit: item, idx: i }"></ng-container>
|
||||
|
||||
<button class="btn btn-icon float-end" (click)="removeItem(item, i)" *ngIf="showRemoveButton">
|
||||
<i class="fa fa-times" aria-hidden="true"></i>
|
||||
<span class="visually-hidden" attr.aria-labelledby="item.id--{{i}}">Remove item</span>
|
||||
<span class="visually-hidden" attr.aria-labelledby="item.id--{{i}}">{{t('remove-item')}}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<p class="visually-hidden" id="instructions">
|
||||
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.
|
||||
{{t('instructions-alt')}}
|
||||
</p>
|
||||
|
||||
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
import { CdkDragDrop, moveItemInArray, CdkDropList, CdkDrag, CdkDragHandle } from '@angular/cdk/drag-drop';
|
||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ContentChild, EventEmitter, Input, Output, TemplateRef, TrackByFunction, ViewChild } from '@angular/core';
|
||||
import { VirtualScrollerComponent, VirtualScrollerModule } from '@iharbeck/ngx-virtual-scroller';
|
||||
import { VirtualScrollerModule } from '@iharbeck/ngx-virtual-scroller';
|
||||
import { NgIf, NgFor, NgTemplateOutlet } from '@angular/common';
|
||||
import {TranslocoModule} from "@ngneat/transloco";
|
||||
|
||||
export interface IndexUpdateEvent {
|
||||
fromPosition: number;
|
||||
|
|
@ -20,7 +21,7 @@ export interface ItemRemoveEvent {
|
|||
styleUrls: ['./draggable-ordered-list.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: true,
|
||||
imports: [NgIf, VirtualScrollerModule, NgFor, NgTemplateOutlet, CdkDropList, CdkDrag, CdkDragHandle]
|
||||
imports: [NgIf, VirtualScrollerModule, NgFor, NgTemplateOutlet, CdkDropList, CdkDrag, CdkDragHandle, TranslocoModule]
|
||||
})
|
||||
export class DraggableOrderedListComponent {
|
||||
|
||||
|
|
|
|||
|
|
@ -1,136 +1,139 @@
|
|||
<app-side-nav-companion-bar [hasExtras]="readingList !== undefined" [extraDrawer]="extrasDrawer">
|
||||
<h2 title>
|
||||
<app-card-actionables (actionHandler)="performAction($event)" [actions]="actions" [attr.aria-labelledby]="readingList?.title" *ngIf="actions.length > 0"></app-card-actionables>
|
||||
{{readingList?.title}}
|
||||
<span *ngIf="readingList?.promoted" class="ms-1">(<i class="fa fa-angle-double-up" aria-hidden="true"></i>)</span>
|
||||
</h2>
|
||||
<h6 subtitle class="subtitle-with-actionables">{{items.length | number}} Items</h6>
|
||||
<ng-container *transloco="let t; read: 'reading-list-detail'">
|
||||
<app-side-nav-companion-bar [hasExtras]="readingList !== undefined" [extraDrawer]="extrasDrawer">
|
||||
<h2 title>
|
||||
<app-card-actionables (actionHandler)="performAction($event)" [actions]="actions" [attr.aria-labelledby]="readingList?.title" *ngIf="actions.length > 0"></app-card-actionables>
|
||||
{{readingList?.title}}
|
||||
<span *ngIf="readingList?.promoted" class="ms-1">(<i class="fa fa-angle-double-up" aria-hidden="true"></i>)</span>
|
||||
</h2>
|
||||
<h6 subtitle class="subtitle-with-actionables">{{t('item-count', {num: items.length | number})}}</h6>
|
||||
|
||||
<ng-template #extrasDrawer let-offcanvas>
|
||||
<div style="margin-top: 56px" *ngIf="readingList">
|
||||
<ng-template #extrasDrawer let-offcanvas>
|
||||
<div style="margin-top: 56px" *ngIf="readingList">
|
||||
<div class="offcanvas-header">
|
||||
<h4 class="offcanvas-title" id="offcanvas-basic-title">Page Settings</h4>
|
||||
<button type="button" class="btn-close" aria-label="Close" (click)="offcanvas.dismiss()"></button>
|
||||
<h4 class="offcanvas-title" id="offcanvas-basic-title">{{t('page-settings-title')}}</h4>
|
||||
<button type="button" class="btn-close" aria-label="Close" (click)="offcanvas.dismiss()"></button>
|
||||
</div>
|
||||
<div class="offcanvas-body">
|
||||
<div class="row g-0">
|
||||
<div class="col-md-12 col-sm-12 pe-2 mb-3">
|
||||
<button class="btn btn-danger" (click)="removeRead()" [disabled]="readingList.promoted && !this.isAdmin">
|
||||
<button class="btn btn-danger" (click)="removeRead()" [disabled]="readingList.promoted && !this.isAdmin">
|
||||
<span>
|
||||
<i class="fa fa-check"></i>
|
||||
</span>
|
||||
<span class="read-btn--text"> Remove Read</span>
|
||||
<span class="read-btn--text"> {{t('remove-read')}}</span>
|
||||
</button>
|
||||
|
||||
<div class="col-auto ms-2 mt-2" *ngIf="!(readingList?.promoted && !this.isAdmin)">
|
||||
<div class="form-check form-check-inline">
|
||||
<input class="form-check-input" type="checkbox" id="accessibility-mode" [value]="accessibilityMode" (change)="updateAccesibilityMode()">
|
||||
<input class="form-check-input" type="checkbox" id="accessibility-mode" [value]="accessibilityMode" (change)="updateAccessibilityMode()">
|
||||
<label class="form-check-label" for="accessibility-mode">Order Numbers</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
</app-side-nav-companion-bar>
|
||||
<div class="container-fluid mt-2" *ngIf="readingList" >
|
||||
|
||||
<div class="row mb-2">
|
||||
<div class="col-md-2 col-xs-4 col-sm-6 d-none d-sm-block" *ngIf="readingList.coverImage !== '' && readingList.coverImage !== undefined && readingList.coverImage !== null">
|
||||
<app-image maxWidth="300px" maxHeight="400px" [imageUrl]="readingListImage"></app-image>
|
||||
</div>
|
||||
<div class="col-md-10 col-xs-8 col-sm-6 mt-2">
|
||||
<div class="row g-0 mb-3">
|
||||
<div class="col-auto me-2">
|
||||
<!-- Action row-->
|
||||
<div class="btn-group me-3">
|
||||
<button type="button" class="btn btn-primary" (click)="continue()">
|
||||
<span>
|
||||
<i class="fa fa-book-open" aria-hidden="true"></i>
|
||||
<span class="read-btn--text"> Continue</span>
|
||||
</span>
|
||||
</button>
|
||||
<div class="btn-group" ngbDropdown role="group" aria-label="Read options">
|
||||
<button type="button" class="btn btn-primary dropdown-toggle-split" ngbDropdownToggle></button>
|
||||
<div class="dropdown-menu" ngbDropdownMenu>
|
||||
<button ngbDropdownItem (click)="read()">
|
||||
<span>
|
||||
<i class="fa fa-book" aria-hidden="true"></i>
|
||||
<span class="read-btn--text"> Read</span>
|
||||
</span>
|
||||
</button>
|
||||
<button ngbDropdownItem (click)="continue(true)">
|
||||
<span>
|
||||
<i class="fa fa-book-open" aria-hidden="true"></i>
|
||||
<span class="read-btn--text"> Continue</span>
|
||||
(<i class="fa fa-glasses ms-1" aria-hidden="true"></i>)
|
||||
<span class="visually-hidden">(Incognito)</span>
|
||||
</span>
|
||||
</button>
|
||||
<button ngbDropdownItem (click)="read(true)">
|
||||
<span>
|
||||
<i class="fa fa-book" aria-hidden="true"></i>
|
||||
<span class="read-btn--text"> Read</span>
|
||||
(<i class="fa fa-glasses ms-1" aria-hidden="true"></i>)
|
||||
<span class="visually-hidden">(Incognito)</span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row g-0 mt-2" *ngIf="readingList.startingYear !== 0">
|
||||
<h4 class="reading-list-years">
|
||||
<ng-container *ngIf="readingList.startingMonth > 0">{{(readingList.startingMonth +'/01/2020')| date:'MMM'}}</ng-container>
|
||||
<ng-container *ngIf="readingList.startingMonth > 0 && readingList.startingYear > 0">, </ng-container>
|
||||
<ng-container *ngIf="readingList.startingYear > 0">{{readingList.startingYear}}</ng-container>
|
||||
—
|
||||
<ng-container *ngIf="readingList.endingYear > 0">
|
||||
<ng-container *ngIf="readingList.endingMonth > 0">{{(readingList.endingMonth +'/01/2020') | date:'MMM'}}</ng-container>
|
||||
<ng-container *ngIf="readingList.endingMonth > 0 && readingList.endingYear > 0">, </ng-container>
|
||||
<ng-container *ngIf="readingList.endingYear > 0">{{readingList.endingYear}}</ng-container>
|
||||
</ng-container>
|
||||
</ng-template>
|
||||
</app-side-nav-companion-bar>
|
||||
<div class="container-fluid mt-2" *ngIf="readingList" >
|
||||
|
||||
</h4>
|
||||
</div>
|
||||
<!-- Summary row-->
|
||||
<div class="row g-0 mt-2">
|
||||
<app-read-more [text]="readingListSummary" [maxLength]="250"></app-read-more>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ng-container *ngIf="characters$ | async as characters">
|
||||
<div class="row mb-2">
|
||||
<div class="row" *ngIf="characters && characters.length > 0">
|
||||
<h5>Characters</h5>
|
||||
<app-badge-expander [items]="characters">
|
||||
<ng-template #badgeExpanderItem let-item let-position="idx">
|
||||
<app-person-badge a11y-click="13,32" class="col-auto" [person]="item"></app-person-badge>
|
||||
</ng-template>
|
||||
</app-badge-expander>
|
||||
<div class="col-md-2 col-xs-4 col-sm-6 d-none d-sm-block" *ngIf="readingList.coverImage !== '' && readingList.coverImage !== undefined && readingList.coverImage !== null">
|
||||
<app-image maxWidth="300px" maxHeight="400px" [imageUrl]="readingListImage"></app-image>
|
||||
</div>
|
||||
<div class="col-md-10 col-xs-8 col-sm-6 mt-2">
|
||||
<div class="row g-0 mb-3">
|
||||
<div class="col-auto me-2">
|
||||
<!-- Action row-->
|
||||
<div class="btn-group me-3">
|
||||
<button type="button" class="btn btn-primary" (click)="continue()">
|
||||
<span>
|
||||
<i class="fa fa-book-open" aria-hidden="true"></i>
|
||||
<span class="read-btn--text">{{t('continue')}}</span>
|
||||
</span>
|
||||
</button>
|
||||
<div class="btn-group" ngbDropdown role="group" aria-label="Read options">
|
||||
<button type="button" class="btn btn-primary dropdown-toggle-split" ngbDropdownToggle></button>
|
||||
<div class="dropdown-menu" ngbDropdownMenu>
|
||||
<button ngbDropdownItem (click)="read()">
|
||||
<span>
|
||||
<i class="fa fa-book" aria-hidden="true"></i>
|
||||
<span class="read-btn--text"> {{t('read')}}</span>
|
||||
</span>
|
||||
</button>
|
||||
<button ngbDropdownItem (click)="continue(true)">
|
||||
<span>
|
||||
<i class="fa fa-book-open" aria-hidden="true"></i>
|
||||
<span class="read-btn--text">{{t('continue')}}</span>
|
||||
(<i class="fa fa-glasses ms-1" aria-hidden="true"></i>)
|
||||
<span class="visually-hidden">{{t('incognito-alt')}}</span>
|
||||
</span>
|
||||
</button>
|
||||
<button ngbDropdownItem (click)="read(true)">
|
||||
<span>
|
||||
<i class="fa fa-book" aria-hidden="true"></i>
|
||||
<span class="read-btn--text"> {{t('read')}}</span>
|
||||
(<i class="fa fa-glasses ms-1" aria-hidden="true"></i>)
|
||||
<span class="visually-hidden">{{t('incognito-alt')}}</span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row g-0 mt-2" *ngIf="readingList.startingYear !== 0">
|
||||
<h4 class="reading-list-years">
|
||||
<ng-container *ngIf="readingList.startingMonth > 0">{{(readingList.startingMonth +'/01/2020')| date:'MMM'}}</ng-container>
|
||||
<ng-container *ngIf="readingList.startingMonth > 0 && readingList.startingYear > 0">, </ng-container>
|
||||
<ng-container *ngIf="readingList.startingYear > 0">{{readingList.startingYear}}</ng-container>
|
||||
—
|
||||
<ng-container *ngIf="readingList.endingYear > 0">
|
||||
<ng-container *ngIf="readingList.endingMonth > 0">{{(readingList.endingMonth +'/01/2020') | date:'MMM'}}</ng-container>
|
||||
<ng-container *ngIf="readingList.endingMonth > 0 && readingList.endingYear > 0">, </ng-container>
|
||||
<ng-container *ngIf="readingList.endingYear > 0">{{readingList.endingYear}}</ng-container>
|
||||
</ng-container>
|
||||
|
||||
</h4>
|
||||
</div>
|
||||
<!-- Summary row-->
|
||||
<div class="row g-0 mt-2">
|
||||
<app-read-more [text]="readingListSummary" [maxLength]="250"></app-read-more>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<div class="row mb-1 scroll-container" #scrollingBlock>
|
||||
<ng-container *ngIf="items.length === 0 && !isLoading; else loading">
|
||||
<div class="mx-auto" style="width: 200px;">
|
||||
Nothing added
|
||||
<ng-container *ngIf="characters$ | async as characters">
|
||||
<div class="row mb-2">
|
||||
<div class="row" *ngIf="characters && characters.length > 0">
|
||||
<h5>Characters</h5>
|
||||
<app-badge-expander [items]="characters">
|
||||
<ng-template #badgeExpanderItem let-item let-position="idx">
|
||||
<app-person-badge a11y-click="13,32" class="col-auto" [person]="item"></app-person-badge>
|
||||
</ng-template>
|
||||
</app-badge-expander>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
<ng-template #loading>
|
||||
<app-loading *ngIf="isLoading" [loading]="isLoading"></app-loading>
|
||||
</ng-template>
|
||||
|
||||
<app-draggable-ordered-list [items]="items" (orderUpdated)="orderUpdated($event)" [accessibilityMode]="accessibilityMode"
|
||||
[showRemoveButton]="false">
|
||||
<div class="row mb-1 scroll-container" #scrollingBlock>
|
||||
<ng-container *ngIf="items.length === 0 && !isLoading; else loading">
|
||||
<div class="mx-auto" style="width: 200px;">
|
||||
{{t('no-data')}}
|
||||
</div>
|
||||
</ng-container>
|
||||
<ng-template #loading>
|
||||
<app-loading *ngIf="isLoading" [loading]="isLoading"></app-loading>
|
||||
</ng-template>
|
||||
|
||||
<app-draggable-ordered-list [items]="items" (orderUpdated)="orderUpdated($event)" [accessibilityMode]="accessibilityMode"
|
||||
[showRemoveButton]="false">
|
||||
<ng-template #draggableItem let-item let-position="idx">
|
||||
<app-reading-list-item [ngClass]="{'content-container': items.length < 100, 'non-virtualized-container': items.length >= 100}" [item]="item" [position]="position" [libraryTypes]="libraryTypes"
|
||||
[promoted]="item.promoted" (read)="readChapter($event)" (remove)="itemRemoved($event, position)"></app-reading-list-item>
|
||||
[promoted]="item.promoted" (read)="readChapter($event)" (remove)="itemRemoved($event, position)"></app-reading-list-item>
|
||||
</ng-template>
|
||||
</app-draggable-ordered-list>
|
||||
</app-draggable-ordered-list>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</ng-container>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnInit } from '@angular/core';
|
||||
import {ChangeDetectionStrategy, ChangeDetectorRef, Component, inject, OnInit} from '@angular/core';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { ToastrService } from 'ngx-toastr';
|
||||
import { take } from 'rxjs/operators';
|
||||
|
|
@ -28,6 +28,7 @@ import { ImageComponent } from '../../../shared/image/image.component';
|
|||
import { CardActionablesComponent } from '../../../cards/card-item/card-actionables/card-actionables.component';
|
||||
import { NgIf, NgClass, AsyncPipe, DecimalPipe, DatePipe } from '@angular/common';
|
||||
import { SideNavCompanionBarComponent } from '../../../sidenav/_components/side-nav-companion-bar/side-nav-companion-bar.component';
|
||||
import {TranslocoModule, TranslocoService} from "@ngneat/transloco";
|
||||
|
||||
@Component({
|
||||
selector: 'app-reading-list-detail',
|
||||
|
|
@ -35,7 +36,7 @@ import { SideNavCompanionBarComponent } from '../../../sidenav/_components/side-
|
|||
styleUrls: ['./reading-list-detail.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: true,
|
||||
imports: [SideNavCompanionBarComponent, NgIf, CardActionablesComponent, ImageComponent, NgbDropdown, NgbDropdownToggle, NgbDropdownMenu, NgbDropdownItem, ReadMoreComponent, BadgeExpanderComponent, PersonBadgeComponent, A11yClickDirective, LoadingComponent, DraggableOrderedListComponent, ReadingListItemComponent, NgClass, AsyncPipe, DecimalPipe, DatePipe]
|
||||
imports: [SideNavCompanionBarComponent, NgIf, CardActionablesComponent, ImageComponent, NgbDropdown, NgbDropdownToggle, NgbDropdownMenu, NgbDropdownItem, ReadMoreComponent, BadgeExpanderComponent, PersonBadgeComponent, A11yClickDirective, LoadingComponent, DraggableOrderedListComponent, ReadingListItemComponent, NgClass, AsyncPipe, DecimalPipe, DatePipe, TranslocoModule]
|
||||
})
|
||||
export class ReadingListDetailComponent implements OnInit {
|
||||
items: Array<ReadingListItem> = [];
|
||||
|
|
@ -56,6 +57,8 @@ export class ReadingListDetailComponent implements OnInit {
|
|||
libraryTypes: {[key: number]: LibraryType} = {};
|
||||
characters$!: Observable<Person[]>;
|
||||
|
||||
private translocoService = inject(TranslocoService);
|
||||
|
||||
get MangaFormat(): typeof MangaFormat {
|
||||
return MangaFormat;
|
||||
}
|
||||
|
|
@ -156,10 +159,10 @@ export class ReadingListDetailComponent implements OnInit {
|
|||
}
|
||||
|
||||
async deleteList(readingList: ReadingList) {
|
||||
if (!await this.confirmService.confirm('Are you sure you want to delete the reading list? This cannot be undone.')) return;
|
||||
if (!await this.confirmService.confirm(this.translocoService.translate('toasts.confirm-delete-reading-list'))) return;
|
||||
|
||||
this.readingListService.delete(readingList.id).subscribe(() => {
|
||||
this.toastr.success('Reading list deleted');
|
||||
this.toastr.success(this.translocoService.translate('toasts.reading-list-deleted'));
|
||||
this.router.navigateByUrl('/lists');
|
||||
});
|
||||
}
|
||||
|
|
@ -175,7 +178,7 @@ export class ReadingListDetailComponent implements OnInit {
|
|||
this.items.splice(position, 1);
|
||||
this.items = [...this.items];
|
||||
this.cdRef.markForCheck();
|
||||
this.toastr.success('Item removed');
|
||||
this.toastr.success(this.translocoService.translate('toasts.item-removed'));
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -185,22 +188,22 @@ export class ReadingListDetailComponent implements OnInit {
|
|||
this.cdRef.markForCheck();
|
||||
this.readingListService.removeRead(this.readingList.id).subscribe((resp) => {
|
||||
if (resp === 'Nothing to remove') {
|
||||
this.toastr.info(resp);
|
||||
this.toastr.info(this.translocoService.translate('toasts.nothing-to-remove'));
|
||||
return;
|
||||
}
|
||||
this.getListItems();
|
||||
});
|
||||
}
|
||||
|
||||
read(inconitoMode: boolean = false) {
|
||||
read(incognitoMode: boolean = false) {
|
||||
if (!this.readingList) return;
|
||||
const firstItem = this.items[0];
|
||||
this.router.navigate(
|
||||
this.readerService.getNavigationArray(firstItem.libraryId, firstItem.seriesId, firstItem.chapterId, firstItem.seriesFormat),
|
||||
{queryParams: {readingListId: this.readingList.id, inconitoMode: inconitoMode}});
|
||||
{queryParams: {readingListId: this.readingList.id, incognitoMode: incognitoMode}});
|
||||
}
|
||||
|
||||
continue(inconitoMode: boolean = false) {
|
||||
continue(incognitoMode: boolean = false) {
|
||||
// TODO: Can I do this in the backend?
|
||||
if (!this.readingList) return;
|
||||
let currentlyReadingChapter = this.items[0];
|
||||
|
|
@ -214,10 +217,10 @@ export class ReadingListDetailComponent implements OnInit {
|
|||
|
||||
this.router.navigate(
|
||||
this.readerService.getNavigationArray(currentlyReadingChapter.libraryId, currentlyReadingChapter.seriesId, currentlyReadingChapter.chapterId, currentlyReadingChapter.seriesFormat),
|
||||
{queryParams: {readingListId: this.readingList.id, inconitoMode: inconitoMode}});
|
||||
{queryParams: {readingListId: this.readingList.id, incognitoMode: incognitoMode}});
|
||||
}
|
||||
|
||||
updateAccesibilityMode() {
|
||||
updateAccessibilityMode() {
|
||||
this.accessibilityMode = !this.accessibilityMode;
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,40 +1,41 @@
|
|||
<div class="d-flex flex-row g-0 mb-2">
|
||||
<div class="pe-2">
|
||||
<app-image width="106px" maxHeight="125px" class="img-top me-3" [imageUrl]="imageService.getChapterCoverImage(item.chapterId)"></app-image>
|
||||
<ng-container *ngIf="item.pagesRead === 0 && item.pagesTotal > 0">
|
||||
<div class="not-read-badge" ></div>
|
||||
</ng-container>
|
||||
<div class="progress-banner" *ngIf="item.pagesRead < item.pagesTotal && item.pagesTotal > 0 && item.pagesRead !== item.pagesTotal">
|
||||
<ng-container *transloco="let t; read: 'reading-list-item'">
|
||||
<div class="d-flex flex-row g-0 mb-2">
|
||||
<div class="pe-2">
|
||||
<app-image width="106px" maxHeight="125px" class="img-top me-3" [imageUrl]="imageService.getChapterCoverImage(item.chapterId)"></app-image>
|
||||
<ng-container *ngIf="item.pagesRead === 0 && item.pagesTotal > 0">
|
||||
<div class="not-read-badge" ></div>
|
||||
</ng-container>
|
||||
<div class="progress-banner" *ngIf="item.pagesRead < item.pagesTotal && item.pagesTotal > 0 && item.pagesRead !== item.pagesTotal">
|
||||
<p><ngb-progressbar type="primary" height="5px" [value]="item.pagesRead" [max]="item.pagesTotal"></ngb-progressbar></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex-grow-1">
|
||||
<div class="g-0">
|
||||
<div class="flex-grow-1">
|
||||
<div class="g-0">
|
||||
<h5 class="mb-1 pb-0" id="item.id--{{position}}">
|
||||
{{item.title}}
|
||||
<div class="float-end">
|
||||
<button class="btn btn-danger" (click)="remove.emit(item)">
|
||||
{{item.title}}
|
||||
<div class="float-end">
|
||||
<button class="btn btn-danger" (click)="remove.emit(item)">
|
||||
<span>
|
||||
<i class="fa fa-trash me-1" aria-hidden="true"></i>
|
||||
</span>
|
||||
<span class="d-none d-sm-inline-block">Remove</span>
|
||||
</button>
|
||||
<button class="btn btn-primary ms-2" (click)="readChapter(item)">
|
||||
<span class="d-none d-sm-inline-block">{{t('remove')}}</span>
|
||||
</button>
|
||||
<button class="btn btn-primary ms-2" (click)="readChapter(item)">
|
||||
<span>
|
||||
<i class="fa fa-book me-1" aria-hidden="true"></i>
|
||||
</span>
|
||||
<span class="d-none d-sm-inline-block">Read</span>
|
||||
<span class="d-none d-sm-inline-block">{{t('read')}}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
</h5>
|
||||
<div class="ps-1 d-none d-md-inline-block">
|
||||
<ng-container *ngIf="item.seriesFormat | mangaFormat as formatString">
|
||||
<i class="fa {{item.seriesFormat | mangaFormatIcon}}" aria-hidden="true" *ngIf="item.seriesFormat !== MangaFormat.UNKNOWN" title="{{formatString}}"></i>
|
||||
<span class="visually-hidden">{{formatString}}</span>
|
||||
</ng-container>
|
||||
|
||||
|
||||
<a href="/library/{{item.libraryId}}/series/{{item.seriesId}}">{{item.seriesName}}</a>
|
||||
</div>
|
||||
|
||||
|
|
@ -43,7 +44,8 @@
|
|||
<div class="ps-1 mt-2" *ngIf="item.releaseDate !== '0001-01-01T00:00:00'">
|
||||
Released: {{item.releaseDate | date:'longDate'}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import { MangaFormatPipe } from '../../../pipe/manga-format.pipe';
|
|||
import { NgbProgressbar } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { NgIf, DatePipe } from '@angular/common';
|
||||
import { ImageComponent } from '../../../shared/image/image.component';
|
||||
import {TranslocoModule} from "@ngneat/transloco";
|
||||
|
||||
@Component({
|
||||
selector: 'app-reading-list-item',
|
||||
|
|
@ -15,7 +16,7 @@ import { ImageComponent } from '../../../shared/image/image.component';
|
|||
styleUrls: ['./reading-list-item.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: true,
|
||||
imports: [ImageComponent, NgIf, NgbProgressbar, DatePipe, MangaFormatPipe, MangaFormatIconPipe]
|
||||
imports: [ImageComponent, NgIf, NgbProgressbar, DatePipe, MangaFormatPipe, MangaFormatIconPipe, TranslocoModule]
|
||||
})
|
||||
export class ReadingListItemComponent {
|
||||
|
||||
|
|
|
|||
|
|
@ -1,25 +1,28 @@
|
|||
<app-side-nav-companion-bar>
|
||||
<ng-container *transloco="let t; read: 'reading-lists'">
|
||||
<app-side-nav-companion-bar>
|
||||
<h2 title>
|
||||
<app-card-actionables [actions]="globalActions" (actionHandler)="performGlobalAction($event)"></app-card-actionables>
|
||||
<span>Reading Lists</span>
|
||||
<app-card-actionables [actions]="globalActions" (actionHandler)="performGlobalAction($event)"></app-card-actionables>
|
||||
<span>Reading Lists</span>
|
||||
</h2>
|
||||
<h6 subtitle class="subtitle-with-actionables" *ngIf="pagination">{{pagination.totalItems | number}} Items</h6>
|
||||
</app-side-nav-companion-bar>
|
||||
<h6 subtitle class="subtitle-with-actionables" *ngIf="pagination">{{t('item-count', {num: pagination.totalItems | number})}}</h6>
|
||||
</app-side-nav-companion-bar>
|
||||
|
||||
<app-card-detail-layout
|
||||
<app-card-detail-layout
|
||||
[isLoading]="loadingLists"
|
||||
[items]="lists"
|
||||
[pagination]="pagination"
|
||||
[jumpBarKeys]="jumpbarKeys"
|
||||
[filteringDisabled]="true"
|
||||
>
|
||||
>
|
||||
<ng-template #cardItem let-item let-position="idx" >
|
||||
<app-card-item [title]="item.title" [entity]="item" [actions]="actions[item.id]"
|
||||
[suppressLibraryLink]="true" [imageUrl]="imageService.getReadingListCoverImage(item.id)"
|
||||
(clicked)="handleClick(item)"></app-card-item>
|
||||
<app-card-item [title]="item.title" [entity]="item" [actions]="actions[item.id]"
|
||||
[suppressLibraryLink]="true" [imageUrl]="imageService.getReadingListCoverImage(item.id)"
|
||||
(clicked)="handleClick(item)"></app-card-item>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #noData>
|
||||
There are no reading lists. Try creating <a href="https://wiki.kavitareader.com/en/guides/get-started-using-your-library#reading-list" rel="noopener noreferrer" target="_blank">one <i class="fa fa-external-link-alt" aria-hidden="true"></i></a>.
|
||||
{{t('no-data')}} {{t('create-one-part-1')}} <a href="https://wiki.kavitareader.com/en/guides/get-started-using-your-library#reading-list" rel="noopener noreferrer" target="_blank">{{t('create-one-part-2')}}<i class="fa fa-external-link-alt ms-1" aria-hidden="true"></i></a>.
|
||||
</ng-template>
|
||||
</app-card-detail-layout>
|
||||
</app-card-detail-layout>
|
||||
|
||||
</ng-container>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnInit } from '@angular/core';
|
||||
import {ChangeDetectionStrategy, ChangeDetectorRef, Component, inject, OnInit} from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { ToastrService } from 'ngx-toastr';
|
||||
|
|
@ -18,6 +18,7 @@ import { CardDetailLayoutComponent } from '../../../cards/card-detail-layout/car
|
|||
import { NgIf, DecimalPipe } from '@angular/common';
|
||||
import { CardActionablesComponent } from '../../../cards/card-item/card-actionables/card-actionables.component';
|
||||
import { SideNavCompanionBarComponent } from '../../../sidenav/_components/side-nav-companion-bar/side-nav-companion-bar.component';
|
||||
import {TranslocoModule, TranslocoService} from "@ngneat/transloco";
|
||||
|
||||
@Component({
|
||||
selector: 'app-reading-lists',
|
||||
|
|
@ -25,7 +26,7 @@ import { SideNavCompanionBarComponent } from '../../../sidenav/_components/side-
|
|||
styleUrls: ['./reading-lists.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: true,
|
||||
imports: [SideNavCompanionBarComponent, CardActionablesComponent, NgIf, CardDetailLayoutComponent, CardItemComponent, DecimalPipe]
|
||||
imports: [SideNavCompanionBarComponent, CardActionablesComponent, NgIf, CardDetailLayoutComponent, CardItemComponent, DecimalPipe, TranslocoModule]
|
||||
})
|
||||
export class ReadingListsComponent implements OnInit {
|
||||
|
||||
|
|
@ -37,6 +38,7 @@ export class ReadingListsComponent implements OnInit {
|
|||
actions: {[key: number]: Array<ActionItem<ReadingList>>} = {};
|
||||
globalActions: Array<ActionItem<any>> = [{action: Action.Import, title: 'Import CBL', children: [], requiresAdmin: true, callback: this.importCbl.bind(this)}];
|
||||
|
||||
translocoService = inject(TranslocoService);
|
||||
constructor(private readingListService: ReadingListService, public imageService: ImageService, private actionFactoryService: ActionFactoryService,
|
||||
private accountService: AccountService, private toastr: ToastrService, private router: Router, private actionService: ActionService,
|
||||
private jumpbarService: JumpbarService, private readonly cdRef: ChangeDetectorRef, private ngbModal: NgbModal) { }
|
||||
|
|
@ -77,7 +79,7 @@ export class ReadingListsComponent implements OnInit {
|
|||
switch(action.action) {
|
||||
case Action.Delete:
|
||||
this.readingListService.delete(readingList.id).subscribe(() => {
|
||||
this.toastr.success('Reading list deleted');
|
||||
this.toastr.success(this.translocoService.translate('toasts.reading-list-deleted'));
|
||||
this.loadPage();
|
||||
});
|
||||
break;
|
||||
|
|
|
|||
|
|
@ -1,44 +1,45 @@
|
|||
<ng-container *transloco="let t; read: 'add-to-list-modal'">
|
||||
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title" id="modal-basic-title">Add to Reading List</h4>
|
||||
<button type="button" class="btn-close" aria-label="Close" (click)="close()">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title" id="modal-basic-title">{{t('title')}}</h4>
|
||||
<button type="button" class="btn-close" [attr.aria-label]="t('close')" (click)="close()">
|
||||
|
||||
</button>
|
||||
</div>
|
||||
<form style="width: 100%" [formGroup]="listForm">
|
||||
<div class="modal-body scrollable-modal">
|
||||
<div class="mb-3" *ngIf="lists.length >= 5">
|
||||
<label for="filter" class="form-label">Filter</label>
|
||||
</div>
|
||||
<form style="width: 100%" [formGroup]="listForm">
|
||||
<div class="modal-body scrollable-modal">
|
||||
<div class="mb-3" *ngIf="lists.length >= 5">
|
||||
<label for="filter" class="form-label">{{t('filter-label')}}</label>
|
||||
<div class="input-group">
|
||||
<input id="filter" autocomplete="off" class="form-control" formControlName="filterQuery" type="text" aria-describedby="reset-input">
|
||||
<button class="btn btn-outline-secondary" type="button" id="reset-input" (click)="listForm.get('filterQuery')?.setValue('');">Clear</button>
|
||||
<input id="filter" autocomplete="off" class="form-control" formControlName="filterQuery" type="text" aria-describedby="reset-input">
|
||||
<button class="btn btn-outline-secondary" type="button" id="reset-input" (click)="listForm.get('filterQuery')?.setValue('');">Clear</button>
|
||||
</div>
|
||||
</div>
|
||||
<ul class="list-group">
|
||||
</div>
|
||||
<ul class="list-group">
|
||||
<li class="list-group-item clickable" tabindex="0" role="option" *ngFor="let readingList of lists | filter: filterList; let i = index" (click)="addToList(readingList)">
|
||||
{{readingList.title}} <i class="fa fa-angle-double-up" *ngIf="readingList.promoted" title="Promoted"></i>
|
||||
{{readingList.title}} <i class="fa fa-angle-double-up" *ngIf="readingList.promoted" [title]="t('promoted-alt')"></i>
|
||||
</li>
|
||||
<li class="list-group-item" *ngIf="lists.length === 0 && !loading">No lists created yet</li>
|
||||
<li class="list-group-item" *ngIf="lists.length === 0 && !loading">{{t('no-data')}}</li>
|
||||
<li class="list-group-item" *ngIf="loading">
|
||||
<div class="spinner-border text-primary" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="modal-footer" style="justify-content: normal">
|
||||
<div style="width: 100%;">
|
||||
<div class="d-flex">
|
||||
<div class="col-9 col-lg-10">
|
||||
<label class="form-label visually-hidden" for="add-rlist">Reading List</label>
|
||||
<input width="100%" #title ngbAutofocus type="text" class="form-control mb-2" id="add-rlist" formControlName="title">
|
||||
</div>
|
||||
<div class="col-2 ps-2">
|
||||
<button type="submit" class="btn btn-primary" (click)="create()">Create</button>
|
||||
</div>
|
||||
<div class="spinner-border text-primary" role="status">
|
||||
<span class="visually-hidden">{{t('loading')}}</span>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="modal-footer" style="justify-content: normal">
|
||||
<div style="width: 100%;">
|
||||
<div class="d-flex">
|
||||
<div class="col-9 col-lg-10">
|
||||
<label class="form-label visually-hidden" for="add-rlist">{{t('reading-list-label')}}</label>
|
||||
<input width="100%" #title ngbAutofocus type="text" class="form-control mb-2" id="add-rlist" formControlName="title">
|
||||
</div>
|
||||
<div class="col-2 ps-2">
|
||||
<button type="submit" class="btn btn-primary" (click)="create()">{{t('create')}}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
</ng-container>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { AfterViewInit, Component, ElementRef, Input, OnInit, ViewChild } from '@angular/core';
|
||||
import {AfterViewInit, Component, ElementRef, inject, Input, OnInit, ViewChild} from '@angular/core';
|
||||
import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms';
|
||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { ToastrService } from 'ngx-toastr';
|
||||
|
|
@ -6,6 +6,7 @@ import { ReadingList } from 'src/app/_models/reading-list';
|
|||
import { ReadingListService } from 'src/app/_services/reading-list.service';
|
||||
import { FilterPipe } from '../../../pipe/filter.pipe';
|
||||
import { NgIf, NgFor } from '@angular/common';
|
||||
import {TranslocoModule, TranslocoService} from "@ngneat/transloco";
|
||||
|
||||
export enum ADD_FLOW {
|
||||
Series = 0,
|
||||
|
|
@ -20,7 +21,7 @@ export enum ADD_FLOW {
|
|||
templateUrl: './add-to-list-modal.component.html',
|
||||
styleUrls: ['./add-to-list-modal.component.scss'],
|
||||
standalone: true,
|
||||
imports: [ReactiveFormsModule, NgIf, NgFor, FilterPipe]
|
||||
imports: [ReactiveFormsModule, NgIf, NgFor, FilterPipe, TranslocoModule]
|
||||
})
|
||||
export class AddToListModalComponent implements OnInit, AfterViewInit {
|
||||
|
||||
|
|
@ -68,6 +69,8 @@ export class AddToListModalComponent implements OnInit, AfterViewInit {
|
|||
return listItem.title.toLowerCase().indexOf((this.listForm.value.filterQuery || '').toLowerCase()) >= 0;
|
||||
}
|
||||
|
||||
translocoService = inject(TranslocoService);
|
||||
|
||||
|
||||
constructor(private modal: NgbActiveModal, private readingListService: ReadingListService, private toastr: ToastrService) { }
|
||||
|
||||
|
|
@ -106,7 +109,7 @@ export class AddToListModalComponent implements OnInit, AfterViewInit {
|
|||
|
||||
if (this.type === ADD_FLOW.Multiple_Series && this.seriesIds !== undefined) {
|
||||
this.readingListService.updateByMultipleSeries(readingList.id, this.seriesIds).subscribe(() => {
|
||||
this.toastr.success('Series added to reading list');
|
||||
this.toastr.success(this.translocoService.translate('toasts.series-added-to-reading-list'));
|
||||
this.modal.close();
|
||||
});
|
||||
}
|
||||
|
|
@ -115,22 +118,22 @@ export class AddToListModalComponent implements OnInit, AfterViewInit {
|
|||
|
||||
if (this.type === ADD_FLOW.Series && this.seriesId !== undefined) {
|
||||
this.readingListService.updateBySeries(readingList.id, this.seriesId).subscribe(() => {
|
||||
this.toastr.success('Series added to reading list');
|
||||
this.toastr.success(this.translocoService.translate('toasts.series-added-to-reading-list'));
|
||||
this.modal.close();
|
||||
});
|
||||
} else if (this.type === ADD_FLOW.Volume && this.volumeId !== undefined) {
|
||||
this.readingListService.updateByVolume(readingList.id, this.seriesId, this.volumeId).subscribe(() => {
|
||||
this.toastr.success('Volumes added to reading list');
|
||||
this.toastr.success(this.translocoService.translate('toasts.volumes-added-to-reading-list'));
|
||||
this.modal.close();
|
||||
});
|
||||
} else if (this.type === ADD_FLOW.Chapter && this.chapterId !== undefined) {
|
||||
this.readingListService.updateByChapter(readingList.id, this.seriesId, this.chapterId).subscribe(() => {
|
||||
this.toastr.success('Chapter added to reading list');
|
||||
this.toastr.success(this.translocoService.translate('toasts.chapter-added-to-reading-list'));
|
||||
this.modal.close();
|
||||
});
|
||||
} else if (this.type === ADD_FLOW.Multiple && this.volumeIds !== undefined && this.chapterIds !== undefined) {
|
||||
this.readingListService.updateByMultiple(readingList.id, this.seriesId, this.volumeIds, this.chapterIds).subscribe(() => {
|
||||
this.toastr.success('Chapters and Volumes added to reading list');
|
||||
this.toastr.success(this.translocoService.translate('toasts.multiple-added-to-reading-list'));
|
||||
this.modal.close();
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,115 +1,118 @@
|
|||
<ng-container *transloco="let t; read: 'edit-reading-list-modal'">
|
||||
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title" id="modal-basic-title">Edit Reading List: {{readingList.title}}</h4>
|
||||
<button type="button" class="btn-close" aria-label="Close" (click)="close()"></button>
|
||||
</div>
|
||||
<div class="modal-body scrollable-modal {{utilityService.getActiveBreakpoint() === Breakpoint.Mobile ? '' : 'd-flex'}}">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title" id="modal-basic-title">{{t('title', {name: readingList.title})}}</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'}}">
|
||||
<ul ngbNav #nav="ngbNav" [(activeId)]="active" class="nav-pills"
|
||||
orientation="{{utilityService.getActiveBreakpoint() === Breakpoint.Mobile ? 'horizontal' : 'vertical'}}" style="min-width: 135px;">
|
||||
<li [ngbNavItem]="TabID.General">
|
||||
<a ngbNavLink>{{TabID.General}}</a>
|
||||
<ng-template ngbNavContent>
|
||||
<form [formGroup]="reviewGroup">
|
||||
<div class="row g-0 mb-3">
|
||||
<div class="col-md-8 col-sm-12">
|
||||
<label for="library-name" class="form-label">Name</label>
|
||||
<input id="library-name" class="form-control" formControlName="title" type="text" [class.is-invalid]="reviewGroup.get('title')?.invalid && reviewGroup.get('title')?.touched">
|
||||
<div id="inviteForm-validations" class="invalid-feedback" *ngIf="reviewGroup.dirty || reviewGroup.touched">
|
||||
<div *ngIf="reviewGroup.get('title')?.errors?.required">
|
||||
This field is required
|
||||
</div>
|
||||
<div *ngIf="reviewGroup.get('title')?.errors?.duplicateName">
|
||||
Name must be unique
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ng-container *ngIf="(accountService.currentUser$ | async) as user">
|
||||
<div class="col-md-3 col-sm-12 ms-2" *ngIf="accountService.hasAdminRole(user)">
|
||||
<div class="form-check form-switch">
|
||||
<input type="checkbox" id="tag-promoted" role="switch" formControlName="promoted" class="form-check-input"
|
||||
aria-labelledby="auto-close-label" aria-describedby="tag-promoted-help">
|
||||
<label class="form-check-label me-1" for="tag-promoted">Promote</label>
|
||||
<i class="fa fa-info-circle" aria-hidden="true" placement="left" [ngbTooltip]="promotedTooltip" role="button" tabindex="0"></i>
|
||||
<ng-template #promotedTooltip>Promotion means that the tag can be seen server-wide, not just for admin users. All series that have this tag will still have user-access restrictions placed on them.</ng-template>
|
||||
<span class="visually-hidden" id="tag-promoted-help"><ng-container [ngTemplateOutlet]="promotedTooltip"></ng-container></span>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
orientation="{{utilityService.getActiveBreakpoint() === Breakpoint.Mobile ? 'horizontal' : 'vertical'}}" style="min-width: 135px;">
|
||||
<li [ngbNavItem]="TabID.General">
|
||||
<a ngbNavLink>{{t(TabID.General)}}</a>
|
||||
<ng-template ngbNavContent>
|
||||
<form [formGroup]="reviewGroup">
|
||||
<div class="row g-0 mb-3">
|
||||
<div class="col-md-8 col-sm-12">
|
||||
<label for="library-name" class="form-label">Name</label>
|
||||
<input id="library-name" class="form-control" formControlName="title" type="text" [class.is-invalid]="reviewGroup.get('title')?.invalid && reviewGroup.get('title')?.touched">
|
||||
<div id="inviteForm-validations" class="invalid-feedback" *ngIf="reviewGroup.dirty || reviewGroup.touched">
|
||||
<div *ngIf="reviewGroup.get('title')?.errors?.required">
|
||||
{{t('required-field')}}
|
||||
</div>
|
||||
<div *ngIf="reviewGroup.get('title')?.errors?.duplicateName">
|
||||
{{t('name-unique-validation')}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ng-container *ngIf="(accountService.currentUser$ | async) as user">
|
||||
<div class="col-md-3 col-sm-12 ms-2" *ngIf="accountService.hasAdminRole(user)">
|
||||
<div class="form-check form-switch">
|
||||
<input type="checkbox" id="tag-promoted" role="switch" formControlName="promoted" class="form-check-input"
|
||||
aria-labelledby="auto-close-label" aria-describedby="tag-promoted-help">
|
||||
<label class="form-check-label me-1" for="tag-promoted">{{t('promote-label')}}</label>
|
||||
<i class="fa fa-info-circle" aria-hidden="true" placement="left" [ngbTooltip]="promotedTooltip" role="button" tabindex="0"></i>
|
||||
<ng-template #promotedTooltip>{{t('promote-tooltip')}}</ng-template>
|
||||
<span class="visually-hidden" id="tag-promoted-help"><ng-container [ngTemplateOutlet]="promotedTooltip"></ng-container></span>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
|
||||
<div class="row g-0 mb-3">
|
||||
<div class="col-md-6 col-sm-12">
|
||||
<h6 id="starting-year-header">Starting</h6>
|
||||
<div class="col-md-6 col-sm-12" *ngIf="reviewGroup.get('startingMonth') as formControl" style="width: 90%">
|
||||
<label for="start-month" class="form-label">Month</label>
|
||||
<input id="start-month" class="form-control" formControlName="startingMonth"
|
||||
type="number" inputmode="numeric" [class.is-invalid]="formControl?.invalid && formControl?.touched"
|
||||
aria-describedby="starting-year-header">
|
||||
<div class="invalid-feedback" *ngIf="reviewGroup.dirty || reviewGroup.touched">
|
||||
<div *ngIf="formControl.errors?.min || formControl.errors?.max">
|
||||
Must be between 1 and 12 or blank
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6 col-sm-12" *ngIf="reviewGroup.get('startingYear') as formControl" style="width: 90%">
|
||||
<label for="start-year" class="form-label">Year</label>
|
||||
<input id="start-year" class="form-control" formControlName="startingYear" type="number" inputmode="numeric"
|
||||
[class.is-invalid]="formControl.invalid && formControl.touched"
|
||||
aria-describedby="starting-year-header">
|
||||
<div class="invalid-feedback" *ngIf="reviewGroup.dirty || reviewGroup.touched">
|
||||
<div *ngIf="formControl.errors?.min || formControl.errors?.max">
|
||||
Must be greater than 1000, 0 or blank
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 col-sm-12">
|
||||
<h6 id="ending-year-heading">Ending</h6>
|
||||
<div class="col-md-6 col-sm-12" *ngIf="reviewGroup.get('endingMonth') as formControl" style="width: 90%">
|
||||
<label for="ending-month" class="form-label">Month</label>
|
||||
<input id="ending-month" class="form-control" formControlName="endingMonth" type="number" inputmode="numeric"
|
||||
[class.is-invalid]="formControl?.invalid && formControl?.touched"
|
||||
aria-describedby="ending-year-header">
|
||||
<div class="invalid-feedback" *ngIf="reviewGroup.dirty || reviewGroup.touched">
|
||||
<div *ngIf="formControl.errors?.min || formControl.errors?.max">
|
||||
Must be between 1 and 12 or blank
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6 col-sm-12" *ngIf="reviewGroup.get('endingYear') as formControl" style="width: 90%">
|
||||
<label for="ending-year" class="form-label">Year</label>
|
||||
<input id="ending-year" class="form-control" formControlName="endingYear" type="number" inputmode="numeric"
|
||||
[class.is-invalid]="formControl?.invalid && formControl?.touched"
|
||||
aria-describedby="ending-year-header">
|
||||
<div class="invalid-feedback" *ngIf="reviewGroup.dirty || reviewGroup.touched">
|
||||
<div *ngIf="formControl.errors?.min || formControl.errors?.max">
|
||||
Must be greater than 1000, 0 or blank
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row g-0 mb-3">
|
||||
<div class="col-md-6 col-sm-12">
|
||||
<h6 id="starting-year-header">{{t('starting-title')}}</h6>
|
||||
<div class="col-md-6 col-sm-12" *ngIf="reviewGroup.get('startingMonth') as formControl" style="width: 90%">
|
||||
<label for="start-month" class="form-label">{{t('month-label')}}</label>
|
||||
<input id="start-month" class="form-control" formControlName="startingMonth"
|
||||
type="number" inputmode="numeric" [class.is-invalid]="formControl?.invalid && formControl?.touched"
|
||||
aria-describedby="starting-year-header">
|
||||
<div class="invalid-feedback" *ngIf="reviewGroup.dirty || reviewGroup.touched">
|
||||
<div *ngIf="formControl.errors?.min || formControl.errors?.max">
|
||||
{{t('month-validation')}}
|
||||
</div>
|
||||
|
||||
<div class="row g-0 mb-3">
|
||||
<label for="summary" class="form-label">Summary</label>
|
||||
<textarea id="summary" class="form-control" formControlName="summary" rows="3"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6 col-sm-12" *ngIf="reviewGroup.get('startingYear') as formControl" style="width: 90%">
|
||||
<label for="start-year" class="form-label">{{t('year-label')}}</label>
|
||||
<input id="start-year" class="form-control" formControlName="startingYear" type="number" inputmode="numeric"
|
||||
[class.is-invalid]="formControl.invalid && formControl.touched"
|
||||
aria-describedby="starting-year-header">
|
||||
<div class="invalid-feedback" *ngIf="reviewGroup.dirty || reviewGroup.touched">
|
||||
<div *ngIf="formControl.errors?.min || formControl.errors?.max">
|
||||
{{t('year-validation')}}
|
||||
</div>
|
||||
</form>
|
||||
</ng-template>
|
||||
</li>
|
||||
<li [ngbNavItem]="TabID.CoverImage">
|
||||
<a ngbNavLink>{{TabID.CoverImage}}</a>
|
||||
<ng-template ngbNavContent>
|
||||
<app-cover-image-chooser [(imageUrls)]="imageUrls" (imageSelected)="updateSelectedIndex($event)" (selectedBase64Url)="updateSelectedImage($event)" [showReset]="readingList.coverImageLocked" (resetClicked)="handleReset()"></app-cover-image-chooser>
|
||||
</ng-template>
|
||||
</li>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 col-sm-12">
|
||||
<h6 id="ending-year-heading">{{t('ending-title')}}</h6>
|
||||
<div class="col-md-6 col-sm-12" *ngIf="reviewGroup.get('endingMonth') as formControl" style="width: 90%">
|
||||
<label for="ending-month" class="form-label">{{t('month-label')}}</label>
|
||||
<input id="ending-month" class="form-control" formControlName="endingMonth" type="number" inputmode="numeric"
|
||||
[class.is-invalid]="formControl?.invalid && formControl?.touched"
|
||||
aria-describedby="ending-year-header">
|
||||
<div class="invalid-feedback" *ngIf="reviewGroup.dirty || reviewGroup.touched">
|
||||
<div *ngIf="formControl.errors?.min || formControl.errors?.max">
|
||||
{{t('month-validation')}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6 col-sm-12" *ngIf="reviewGroup.get('endingYear') as formControl" style="width: 90%">
|
||||
<label for="ending-year" class="form-label">{{t('year-label')}}</label>
|
||||
<input id="ending-year" class="form-control" formControlName="endingYear" type="number" inputmode="numeric"
|
||||
[class.is-invalid]="formControl?.invalid && formControl?.touched"
|
||||
aria-describedby="ending-year-header">
|
||||
<div class="invalid-feedback" *ngIf="reviewGroup.dirty || reviewGroup.touched">
|
||||
<div *ngIf="formControl.errors?.min || formControl.errors?.max">
|
||||
{{t('year-validation')}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-0 mb-3">
|
||||
<label for="summary" class="form-label">{{t('summary-label')}}</label>
|
||||
<textarea id="summary" class="form-control" formControlName="summary" rows="3"></textarea>
|
||||
</div>
|
||||
</form>
|
||||
</ng-template>
|
||||
</li>
|
||||
<li [ngbNavItem]="TabID.CoverImage">
|
||||
<a ngbNavLink>{{t(TabID.CoverImage)}}</a>
|
||||
<ng-template ngbNavContent>
|
||||
<app-cover-image-chooser [(imageUrls)]="imageUrls" (imageSelected)="updateSelectedIndex($event)" (selectedBase64Url)="updateSelectedImage($event)" [showReset]="readingList.coverImageLocked" (resetClicked)="handleReset()"></app-cover-image-chooser>
|
||||
</ng-template>
|
||||
</li>
|
||||
</ul>
|
||||
<div [ngbNavOutlet]="nav" class="tab-content {{utilityService.getActiveBreakpoint() === Breakpoint.Mobile ? 'mt-3' : 'ms-4 flex-fill'}}"></div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" (click)="close()">Close</button>
|
||||
<button type="submit" class="btn btn-primary" (click)="save()">Save</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" (click)="close()">{{t('close')}}</button>
|
||||
<button type="submit" class="btn btn-primary" (click)="save()">{{t('save')}}</button>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
</ng-container>
|
||||
|
|
|
|||
|
|
@ -21,10 +21,11 @@ import { UploadService } from 'src/app/_services/upload.service';
|
|||
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
|
||||
import { CoverImageChooserComponent } from '../../../cards/cover-image-chooser/cover-image-chooser.component';
|
||||
import { NgIf, NgTemplateOutlet, AsyncPipe } from '@angular/common';
|
||||
import {translate, TranslocoModule} from "@ngneat/transloco";
|
||||
|
||||
enum TabID {
|
||||
General = 'General',
|
||||
CoverImage = 'Cover Image'
|
||||
General = 'general-tab',
|
||||
CoverImage = 'cover-image-tab'
|
||||
}
|
||||
|
||||
@Component({
|
||||
|
|
@ -33,7 +34,7 @@ enum TabID {
|
|||
styleUrls: ['./edit-reading-list-modal.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: true,
|
||||
imports: [NgbNav, NgbNavItem, NgbNavItemRole, NgbNavLink, NgbNavContent, ReactiveFormsModule, NgIf, NgbTooltip, NgTemplateOutlet, CoverImageChooserComponent, NgbNavOutlet, AsyncPipe]
|
||||
imports: [NgbNav, NgbNavItem, NgbNavItemRole, NgbNavLink, NgbNavContent, ReactiveFormsModule, NgIf, NgbTooltip, NgTemplateOutlet, CoverImageChooserComponent, NgbNavOutlet, AsyncPipe, TranslocoModule]
|
||||
})
|
||||
export class EditReadingListModalComponent implements OnInit {
|
||||
|
||||
|
|
@ -120,7 +121,7 @@ export class EditReadingListModalComponent implements OnInit {
|
|||
this.readingList.coverImageLocked = this.coverImageLocked;
|
||||
this.readingList.promoted = model.promoted;
|
||||
this.ngModal.close(this.readingList);
|
||||
this.toastr.success('Reading List updated');
|
||||
this.toastr.success(translate('toasts.reading-list-updated'));
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,126 +1,129 @@
|
|||
<ng-container *transloco="let t; read: 'import-cbl-modal'">
|
||||
|
||||
<div class="modal-header">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title" id="modal-basic-title">CBL Import</h4>
|
||||
<button type="button" class="btn-close" aria-label="Close" (click)="close()"></button>
|
||||
</div>
|
||||
<div class="modal-body scrollable-modal">
|
||||
</div>
|
||||
<div class="modal-body scrollable-modal">
|
||||
<div class="row g-0" style="min-width: 135px;">
|
||||
<app-step-tracker [steps]="steps" [currentStep]="currentStepIndex"></app-step-tracker>
|
||||
<app-step-tracker [steps]="steps" [currentStep]="currentStepIndex"></app-step-tracker>
|
||||
</div>
|
||||
|
||||
<!-- This is going to need to have a fixed height with a scrollbar-->
|
||||
<div>
|
||||
<div class="row g-0" *ngIf="currentStepIndex === Step.Import">
|
||||
<p>To get started, import a .cbl file. Kavita will perform multiple checks before importing. Some steps will block moving forward due to issues with the file.</p>
|
||||
<form [formGroup]="uploadForm" enctype="multipart/form-data">
|
||||
<file-upload formControlName="files"></file-upload>
|
||||
</form>
|
||||
<div class="row g-0" *ngIf="currentStepIndex === Step.Import">
|
||||
<p>{{t('import-description')}}</p>
|
||||
<form [formGroup]="uploadForm" enctype="multipart/form-data">
|
||||
<file-upload formControlName="files"></file-upload>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<ng-container *ngIf="currentStepIndex === Step.Validate">
|
||||
<p>{{t('validate-description')}}</p>
|
||||
<div class="row g-0">
|
||||
|
||||
<ngb-accordion #a="ngbAccordion">
|
||||
<ngb-panel *ngFor="let fileToProcess of filesToProcess">
|
||||
<ng-container *ngIf="fileToProcess.validateSummary as summary">
|
||||
<ng-template ngbPanelTitle>
|
||||
<ng-container [ngTemplateOutlet]="heading" [ngTemplateOutletContext]="{ summary: summary, filename: fileToProcess.fileName }"></ng-container>
|
||||
</ng-template>
|
||||
|
||||
<ng-template ngbPanelContent>
|
||||
<ng-container *ngIf="summary.results.length > 0; else noValidateIssues">
|
||||
<h5>{{t('validate-warning')}}</h5>
|
||||
<ol class="list-group list-group-numbered list-group-flush" >
|
||||
<li class="list-group-item no-hover" *ngFor="let result of summary.results"
|
||||
[innerHTML]="result | cblConflictReason | safeHtml">
|
||||
</li>
|
||||
</ol>
|
||||
</ng-container>
|
||||
<ng-template #noValidateIssues>
|
||||
<div class="justify-content-center col">
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="flex-shrink-0">
|
||||
<i class="fa-solid fa-circle-check" style="font-size: 24px" aria-hidden="true"></i>
|
||||
</div>
|
||||
<div class="flex-grow-1 ms-3">
|
||||
{{t('validate-no-issue')}}
|
||||
</div>
|
||||
</div>
|
||||
{{t('validate-no-issue-description')}}
|
||||
</div>
|
||||
</ng-template>
|
||||
</ng-template>
|
||||
</ng-container>
|
||||
</ngb-panel>
|
||||
</ngb-accordion>
|
||||
</div>
|
||||
|
||||
<ng-container *ngIf="currentStepIndex === Step.Validate">
|
||||
<p>All files have been validated to see if there are any operations to do on the list. Any lists have have failed will not move to the next step. Fix the CBL files and retry.</p>
|
||||
<div class="row g-0">
|
||||
</ng-container>
|
||||
|
||||
<ngb-accordion #a="ngbAccordion">
|
||||
<ngb-panel *ngFor="let fileToProcess of filesToProcess">
|
||||
<ng-container *ngIf="fileToProcess.validateSummary as summary">
|
||||
<ng-template ngbPanelTitle>
|
||||
<ng-container [ngTemplateOutlet]="heading" [ngTemplateOutletContext]="{ summary: summary, filename: fileToProcess.fileName }"></ng-container>
|
||||
</ng-template>
|
||||
<ng-container *ngIf="currentStepIndex === Step.DryRun">
|
||||
<div class="row g-0">
|
||||
<p>{{t('dry-run-description')}}</p>
|
||||
|
||||
<ng-template ngbPanelContent>
|
||||
<ng-container *ngIf="summary.results.length > 0; else noValidateIssues">
|
||||
<h5>There are issues with the CBL that will prevent an import. Correct these issues then try again.</h5>
|
||||
<ol class="list-group list-group-numbered list-group-flush" >
|
||||
<li class="list-group-item no-hover" *ngFor="let result of summary.results"
|
||||
[innerHTML]="result | cblConflictReason | safeHtml">
|
||||
</li>
|
||||
</ol>
|
||||
</ng-container>
|
||||
<ng-template #noValidateIssues>
|
||||
<div class="justify-content-center col">
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="flex-shrink-0">
|
||||
<i class="fa-solid fa-circle-check" style="font-size: 24px" aria-hidden="true"></i>
|
||||
</div>
|
||||
<div class="flex-grow-1 ms-3">
|
||||
Looks good
|
||||
</div>
|
||||
</div>
|
||||
No issues found with CBL, press next.
|
||||
</div>
|
||||
</ng-template>
|
||||
</ng-template>
|
||||
</ng-container>
|
||||
</ngb-panel>
|
||||
</ngb-accordion>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="currentStepIndex === Step.DryRun">
|
||||
<div class="row g-0">
|
||||
<p>This is a dry run and shows what will happen if you press Next and perform the import. All Failures will not be imported.</p>
|
||||
<ngb-accordion #a="ngbAccordion">
|
||||
<ngb-panel *ngFor="let fileToProcess of filesToProcess">
|
||||
<ng-container *ngIf="fileToProcess.dryRunSummary as summary">
|
||||
<ng-template ngbPanelTitle>
|
||||
<ng-container [ngTemplateOutlet]="heading" [ngTemplateOutletContext]="{ summary: summary, filename: fileToProcess.fileName }"></ng-container>
|
||||
</ng-template>
|
||||
|
||||
<ngb-accordion #a="ngbAccordion">
|
||||
<ngb-panel *ngFor="let fileToProcess of filesToProcess">
|
||||
<ng-container *ngIf="fileToProcess.dryRunSummary as summary">
|
||||
<ng-template ngbPanelTitle>
|
||||
<ng-container [ngTemplateOutlet]="heading" [ngTemplateOutletContext]="{ summary: summary, filename: fileToProcess.fileName }"></ng-container>
|
||||
</ng-template>
|
||||
<ng-template ngbPanelContent>
|
||||
<ng-container [ngTemplateOutlet]="resultsList" [ngTemplateOutletContext]="{ summary: summary }"></ng-container>
|
||||
</ng-template>
|
||||
</ng-container>
|
||||
</ngb-panel>
|
||||
</ngb-accordion>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<ng-template ngbPanelContent>
|
||||
<ng-container [ngTemplateOutlet]="resultsList" [ngTemplateOutletContext]="{ summary: summary }"></ng-container>
|
||||
</ng-template>
|
||||
</ng-container>
|
||||
</ngb-panel>
|
||||
</ngb-accordion>
|
||||
</div>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="currentStepIndex === Step.Finalize">
|
||||
<div class="row g-0">
|
||||
<ngb-accordion #a="ngbAccordion">
|
||||
<ngb-panel *ngFor="let fileToProcess of filesToProcess">
|
||||
<ng-container *ngIf="fileToProcess.finalizeSummary as summary">
|
||||
<ng-template ngbPanelTitle>
|
||||
<ng-container [ngTemplateOutlet]="heading" [ngTemplateOutletContext]="{ summary: summary, filename: fileToProcess.fileName }"></ng-container>
|
||||
</ng-template>
|
||||
|
||||
<ng-container *ngIf="currentStepIndex === Step.Finalize">
|
||||
<div class="row g-0">
|
||||
<ngb-accordion #a="ngbAccordion">
|
||||
<ngb-panel *ngFor="let fileToProcess of filesToProcess">
|
||||
<ng-container *ngIf="fileToProcess.finalizeSummary as summary">
|
||||
<ng-template ngbPanelTitle>
|
||||
<ng-container [ngTemplateOutlet]="heading" [ngTemplateOutletContext]="{ summary: summary, filename: fileToProcess.fileName }"></ng-container>
|
||||
</ng-template>
|
||||
|
||||
<ng-template ngbPanelContent>
|
||||
<ng-container [ngTemplateOutlet]="resultsList" [ngTemplateOutletContext]="{ summary: summary }"></ng-container>
|
||||
</ng-template>
|
||||
</ng-container>
|
||||
</ngb-panel>
|
||||
</ngb-accordion>
|
||||
</div>
|
||||
</ng-container>
|
||||
<ng-template ngbPanelContent>
|
||||
<ng-container [ngTemplateOutlet]="resultsList" [ngTemplateOutletContext]="{ summary: summary }"></ng-container>
|
||||
</ng-template>
|
||||
</ng-container>
|
||||
</ngb-panel>
|
||||
</ngb-accordion>
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
|
||||
<ng-template #resultsList let-summary="summary">
|
||||
<ul class="list-group list-group-flush">
|
||||
<li class="list-group-item no-hover" *ngFor="let result of summary.results"
|
||||
innerHTML="{{result.order + 1}}. {{result | cblConflictReason | safeHtml}}"></li>
|
||||
</ul>
|
||||
<ul class="list-group list-group-flush">
|
||||
<li class="list-group-item no-hover" *ngFor="let result of summary.results"
|
||||
innerHTML="{{result.order + 1}}. {{result | cblConflictReason | safeHtml}}"></li>
|
||||
</ul>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #heading let-filename="filename" let-summary="summary">
|
||||
<ng-container *ngIf="summary.success | cblImportResult as success">
|
||||
<ng-container [ngSwitch]="summary.success">
|
||||
<span *ngSwitchCase="CblImportResult.Success" class="badge bg-primary me-1">{{success}}</span>
|
||||
<span *ngSwitchCase="CblImportResult.Fail" class="badge bg-danger me-1">{{success}}</span>
|
||||
<span *ngSwitchCase="CblImportResult.Partial" class="badge bg-warning me-1">{{success}}</span>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
<span>{{filename}}<span *ngIf="summary.cblName">: ({{summary.cblName}})</span></span>
|
||||
<ng-container *ngIf="summary.success | cblImportResult as success">
|
||||
<ng-container [ngSwitch]="summary.success">
|
||||
<span *ngSwitchCase="CblImportResult.Success" class="badge bg-primary me-1">{{success}}</span>
|
||||
<span *ngSwitchCase="CblImportResult.Fail" class="badge bg-danger me-1">{{success}}</span>
|
||||
<span *ngSwitchCase="CblImportResult.Partial" class="badge bg-warning me-1">{{success}}</span>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
<span>{{filename}}<span *ngIf="summary.cblName">: ({{summary.cblName}})</span></span>
|
||||
</ng-template>
|
||||
|
||||
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<a class="btn btn-icon" href="https://wiki.kavitareader.com/en/guides/get-started-using-your-library/reading-lists#creating-a-reading-list-via-cbl" target="_blank" rel="noopener noreferrer">Help</a>
|
||||
<button type="button" class="btn btn-secondary" (click)="close()">Close</button>
|
||||
<button type="button" class="btn btn-primary" (click)="prevStep()" [disabled]="!canMoveToPrevStep()">Prev</button>
|
||||
<button type="button" class="btn btn-primary" (click)="nextStep()" [disabled]="!canMoveToNextStep()">{{NextButtonLabel}}</button>
|
||||
</div>
|
||||
<button type="button" class="btn btn-secondary" (click)="close()">{{t('close')}}</button>
|
||||
<button type="button" class="btn btn-primary" (click)="prevStep()" [disabled]="!canMoveToPrevStep()">{{t('prev')}}</button>
|
||||
<button type="button" class="btn btn-primary" (click)="nextStep()" [disabled]="!canMoveToNextStep()">{{t(NextButtonLabel)}}</button>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
</ng-container>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, ViewChild } from '@angular/core';
|
||||
import {ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, inject, ViewChild} from '@angular/core';
|
||||
import {FormControl, FormGroup, ReactiveFormsModule} from '@angular/forms';
|
||||
import {FileUploadModule, FileUploadValidators} from '@iplab/ngx-file-upload';
|
||||
import {
|
||||
|
|
@ -21,6 +21,7 @@ import {CommonModule} from "@angular/common";
|
|||
import {SafeHtmlPipe} from "../../../pipe/safe-html.pipe";
|
||||
import {CblConflictReasonPipe} from "../../_pipes/cbl-conflict-reason.pipe";
|
||||
import {CblImportResultPipe} from "../../_pipes/cbl-import-result.pipe";
|
||||
import {TranslocoModule, TranslocoService} from "@ngneat/transloco";
|
||||
|
||||
interface FileStep {
|
||||
fileName: string;
|
||||
|
|
@ -43,7 +44,7 @@ enum Step {
|
|||
FileUploadModule,
|
||||
NgbAccordionModule,
|
||||
SafeHtmlPipe,
|
||||
CblConflictReasonPipe, ReactiveFormsModule, StepTrackerComponent, CblImportResultPipe, NgbAccordionToggle],
|
||||
CblConflictReasonPipe, ReactiveFormsModule, StepTrackerComponent, CblImportResultPipe, NgbAccordionToggle, TranslocoModule],
|
||||
templateUrl: './import-cbl-modal.component.html',
|
||||
styleUrls: ['./import-cbl-modal.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
|
|
@ -52,6 +53,8 @@ export class ImportCblModalComponent {
|
|||
|
||||
@ViewChild('fileUpload') fileUpload!: ElementRef<HTMLInputElement>;
|
||||
|
||||
translocoService = inject(TranslocoService);
|
||||
|
||||
fileUploadControl = new FormControl<undefined | Array<File>>(undefined, [
|
||||
FileUploadValidators.accept(['.cbl']),
|
||||
]);
|
||||
|
|
@ -63,16 +66,17 @@ export class ImportCblModalComponent {
|
|||
isLoading: boolean = false;
|
||||
|
||||
steps: Array<TimelineStep> = [
|
||||
{title: 'Import CBLs', index: Step.Import, active: true, icon: 'fa-solid fa-file-arrow-up'},
|
||||
{title: 'Validate CBL', index: Step.Validate, active: false, icon: 'fa-solid fa-spell-check'},
|
||||
{title: 'Dry Run', index: Step.DryRun, active: false, icon: 'fa-solid fa-gears'},
|
||||
{title: 'Final Import', index: Step.Finalize, active: false, icon: 'fa-solid fa-floppy-disk'},
|
||||
{title: this.translocoService.translate('import-cbl-modal.import-step'), index: Step.Import, active: true, icon: 'fa-solid fa-file-arrow-up'},
|
||||
{title: this.translocoService.translate('import-cbl-modal.validate-cbl-step'), index: Step.Validate, active: false, icon: 'fa-solid fa-spell-check'},
|
||||
{title: this.translocoService.translate('import-cbl-modal.dry-run-step'), index: Step.DryRun, active: false, icon: 'fa-solid fa-gears'},
|
||||
{title: this.translocoService.translate('import-cbl-final-import.import-step'), index: Step.Finalize, active: false, icon: 'fa-solid fa-floppy-disk'},
|
||||
];
|
||||
currentStepIndex = this.steps[0].index;
|
||||
|
||||
filesToProcess: Array<FileStep> = [];
|
||||
failedFiles: Array<FileStep> = [];
|
||||
|
||||
|
||||
get Breakpoint() { return Breakpoint; }
|
||||
get Step() { return Step; }
|
||||
get CblImportResult() { return CblImportResult; }
|
||||
|
|
@ -80,11 +84,11 @@ export class ImportCblModalComponent {
|
|||
get NextButtonLabel() {
|
||||
switch(this.currentStepIndex) {
|
||||
case Step.DryRun:
|
||||
return 'Import';
|
||||
return 'import';
|
||||
case Step.Finalize:
|
||||
return 'Restart'
|
||||
return 'restart'
|
||||
default:
|
||||
return 'Next';
|
||||
return 'next';
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -104,7 +108,7 @@ export class ImportCblModalComponent {
|
|||
case Step.Import:
|
||||
const files = this.uploadForm.get('files')?.value;
|
||||
if (!files) {
|
||||
this.toastr.error('You need to select files to move forward');
|
||||
this.toastr.error(this.translocoService.translate('toasts.select-files-warning'));
|
||||
return;
|
||||
}
|
||||
// Load each file into filesToProcess and group their data
|
||||
|
|
@ -237,7 +241,7 @@ export class ImportCblModalComponent {
|
|||
|
||||
this.isLoading = false;
|
||||
this.currentStepIndex++;
|
||||
this.toastr.success('Reading List imported');
|
||||
this.toastr.success(this.translocoService.translate('toasts.reading-list-imported'));
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { Pipe, PipeTransform } from '@angular/core';
|
||||
import {inject, Pipe, PipeTransform} from '@angular/core';
|
||||
import { CblBookResult } from 'src/app/_models/reading-list/cbl/cbl-book-result';
|
||||
import { CblImportReason } from 'src/app/_models/reading-list/cbl/cbl-import-reason.enum';
|
||||
import {TranslocoService} from "@ngneat/transloco";
|
||||
|
||||
const failIcon = '<i aria-hidden="true" class="reading-list-fail--item fa-solid fa-circle-xmark me-1"></i>';
|
||||
const successIcon = '<i aria-hidden="true" class="reading-list-success--item fa-solid fa-circle-check me-1"></i>';
|
||||
|
|
@ -11,28 +12,30 @@ const successIcon = '<i aria-hidden="true" class="reading-list-success--item fa-
|
|||
})
|
||||
export class CblConflictReasonPipe implements PipeTransform {
|
||||
|
||||
translocoService = inject(TranslocoService);
|
||||
|
||||
transform(result: CblBookResult): string {
|
||||
switch (result.reason) {
|
||||
case CblImportReason.AllSeriesMissing:
|
||||
return failIcon + 'Your account is missing access to all series in the list or Kavita does not have anything present in the list.';
|
||||
return failIcon + this.translocoService.translate('cbl-conflict-reason-pipe.all-series-missing');
|
||||
case CblImportReason.ChapterMissing:
|
||||
return failIcon + result.series + ': ' + 'Chapter ' + result.number + ' is missing from Kavita. This item will be skipped.';
|
||||
return failIcon + this.translocoService.translate('cbl-conflict-reason-pipe.chapter-missing', {series: result.series, chapter: result.number});
|
||||
case CblImportReason.EmptyFile:
|
||||
return failIcon + 'The cbl file is empty, nothing to be done.';
|
||||
return failIcon + this.translocoService.translate('cbl-conflict-reason-pipe.empty-file');
|
||||
case CblImportReason.NameConflict:
|
||||
return failIcon + 'A reading list (' + result.readingListName + ') already exists on your account that matches the cbl file.';
|
||||
return failIcon + this.translocoService.translate('cbl-conflict-reason-pipe.chapter-missing', {readingListName: result.readingListName});
|
||||
case CblImportReason.SeriesCollision:
|
||||
return failIcon + 'The series, ' + `<a href="/library/${result.libraryId}/series/${result.seriesId}" target="_blank">${result.series}</a>` + ', collides with another series of the same name in another library.';
|
||||
return failIcon + this.translocoService.translate('cbl-conflict-reason-pipe.series-collision', {seriesLink: `<a href="/library/${result.libraryId}/series/${result.seriesId}" target="_blank">${result.series}</a>`});
|
||||
case CblImportReason.SeriesMissing:
|
||||
return failIcon + 'The series, ' + result.series + ', is missing from Kavita or your account does not have permission. All items with this series will be skipped from import.';
|
||||
return failIcon + this.translocoService.translate('cbl-conflict-reason-pipe.series-missing', {series: result.series});
|
||||
case CblImportReason.VolumeMissing:
|
||||
return failIcon + result.series + ': ' + 'Volume ' + result.volume + ' is missing from Kavita. All items with this volume number will be skipped.';
|
||||
return failIcon + this.translocoService.translate('cbl-conflict-reason-pipe.volume-missing', {series: result.series, volume: result.volume});
|
||||
case CblImportReason.AllChapterMissing:
|
||||
return failIcon + 'All chapters cannot be matched to Chapters in Kavita.';
|
||||
return failIcon + this.translocoService.translate('cbl-conflict-reason-pipe.all-chapter-missing');
|
||||
case CblImportReason.Success:
|
||||
return successIcon + result.series + ' volume ' + result.volume + ' number ' + result.number + ' mapped successfully.';
|
||||
return successIcon + this.translocoService.translate('cbl-conflict-reason-pipe.volume-missing', {series: result.series, volume: result.volume, chapter: result.number});
|
||||
case CblImportReason.InvalidFile:
|
||||
return failIcon + 'The file is corrupted or not matching the expected tags/spec.';
|
||||
return failIcon + this.translocoService.translate('cbl-conflict-reason-pipe.invalid-file');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { Pipe, PipeTransform } from '@angular/core';
|
||||
import {inject, Pipe, PipeTransform} from '@angular/core';
|
||||
import { CblImportResult } from 'src/app/_models/reading-list/cbl/cbl-import-result.enum';
|
||||
import {TranslocoService} from "@ngneat/transloco";
|
||||
|
||||
@Pipe({
|
||||
name: 'cblImportResult',
|
||||
|
|
@ -7,14 +8,16 @@ import { CblImportResult } from 'src/app/_models/reading-list/cbl/cbl-import-res
|
|||
})
|
||||
export class CblImportResultPipe implements PipeTransform {
|
||||
|
||||
translocoService = inject(TranslocoService);
|
||||
|
||||
transform(result: CblImportResult): string {
|
||||
switch (result) {
|
||||
case CblImportResult.Success:
|
||||
return 'Success';
|
||||
return this.translocoService.translate('cbl-import-result-pipe.success');
|
||||
case CblImportResult.Partial:
|
||||
return 'Partial';
|
||||
return this.translocoService.translate('cbl-import-result-pipe.partial');
|
||||
case CblImportResult.Fail:
|
||||
return 'Failure';
|
||||
return this.translocoService.translate('cbl-import-result-pipe.failure');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue