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,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();
|
||||
});
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue