Word Count (#1286)
* Adding some code for Robbie * See more on series detail metadata area is now at the bottom on the section * Cleaned up subtitle headings to use a single class for offset with actionables * Added some markup for the new design, waiting for Robbie to finish it off * styling age-rating badge * Started hooking up basic analyze file service and hooks in the UI. Basic code to implement the count is implemented and in benchmarks. * Hooked up analyze ui to backend * Refactored Series Detail metadata area to use a new icon/title design * Cleaned up the new design * Pushing for robbie to do css * Massive performance improvement to scan series where we only need to scan folders reported that have series in them, rather than the whole library. * Removed theme page as we no longer need it. Added WordCount to DTOs so the UI can show them. Added new pipe to format numbers in compact mode. * Hooked up actual reading time based on user's words per hour * Refactor some magic numbers to consts * Hooked in progress reporting for series word count * Hooked up analyze files * Re-implemented time to read on comics * Removed the word Last Read * Show proper language name instead of iso tag on series detail page. Added some error handling on word count code. * Reworked error handling * Fixed some security vulnerabilities in npm. * Handle a case where there are no text nodes and instead of returning an empty list, htmlagilitypack returns null. * Tweaked the styles a bit on the icon-and-title * Code cleanup Co-authored-by: Robbie Davis <robbie@therobbiedavis.com>
This commit is contained in:
parent
0a70ac35dc
commit
c1490d6e86
48 changed files with 2354 additions and 408 deletions
|
|
@ -48,4 +48,8 @@ export interface Series {
|
|||
* DateTime representing last time a chapter was added to the Series
|
||||
*/
|
||||
lastChapterAdded: string;
|
||||
/**
|
||||
* Number of words in the series
|
||||
*/
|
||||
wordCount: number;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,20 +9,53 @@ import { Volume } from '../_models/volume';
|
|||
import { AccountService } from './account.service';
|
||||
|
||||
export enum Action {
|
||||
/**
|
||||
* Mark entity as read
|
||||
*/
|
||||
MarkAsRead = 0,
|
||||
/**
|
||||
* Mark entity as unread
|
||||
*/
|
||||
MarkAsUnread = 1,
|
||||
/**
|
||||
* Invoke a Scan Library
|
||||
*/
|
||||
ScanLibrary = 2,
|
||||
/**
|
||||
* Delete the entity
|
||||
*/
|
||||
Delete = 3,
|
||||
/**
|
||||
* Open edit modal
|
||||
*/
|
||||
Edit = 4,
|
||||
/**
|
||||
* Open details modal
|
||||
*/
|
||||
Info = 5,
|
||||
/**
|
||||
* Invoke a refresh covers
|
||||
*/
|
||||
RefreshMetadata = 6,
|
||||
/**
|
||||
* Download the entity
|
||||
*/
|
||||
Download = 7,
|
||||
/**
|
||||
* @deprecated This is no longer supported. Use the dedicated page instead
|
||||
* Invoke an Analyze Files which calculates word count
|
||||
*/
|
||||
AnalyzeFiles = 8,
|
||||
/**
|
||||
* Read in incognito mode aka no progress tracking
|
||||
*/
|
||||
Bookmarks = 8,
|
||||
IncognitoRead = 9,
|
||||
/**
|
||||
* Add to reading list
|
||||
*/
|
||||
AddToReadingList = 10,
|
||||
/**
|
||||
* Add to collection
|
||||
*/
|
||||
AddToCollection = 11,
|
||||
/**
|
||||
* Essentially a download, but handled differently. Needed so card bubbles it up for handling
|
||||
|
|
@ -31,7 +64,7 @@ export enum Action {
|
|||
/**
|
||||
* Open Series detail page for said series
|
||||
*/
|
||||
ViewSeries = 13
|
||||
ViewSeries = 13,
|
||||
}
|
||||
|
||||
export interface ActionItem<T> {
|
||||
|
|
@ -97,6 +130,13 @@ export class ActionFactoryService {
|
|||
requiresAdmin: true
|
||||
});
|
||||
|
||||
this.seriesActions.push({
|
||||
action: Action.AnalyzeFiles,
|
||||
title: 'Analyze Files',
|
||||
callback: this.dummyCallback,
|
||||
requiresAdmin: true
|
||||
});
|
||||
|
||||
this.seriesActions.push({
|
||||
action: Action.Delete,
|
||||
title: 'Delete',
|
||||
|
|
@ -131,6 +171,13 @@ export class ActionFactoryService {
|
|||
callback: this.dummyCallback,
|
||||
requiresAdmin: true
|
||||
});
|
||||
|
||||
this.libraryActions.push({
|
||||
action: Action.AnalyzeFiles,
|
||||
title: 'Analyze Files',
|
||||
callback: this.dummyCallback,
|
||||
requiresAdmin: true
|
||||
});
|
||||
|
||||
this.chapterActions.push({
|
||||
action: Action.Edit,
|
||||
|
|
@ -200,11 +247,6 @@ export class ActionFactoryService {
|
|||
return actions;
|
||||
}
|
||||
|
||||
filterBookmarksForFormat(action: ActionItem<Series>, series: Series) {
|
||||
if (action.action === Action.Bookmarks && series?.format === MangaFormat.EPUB) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
dummyCallback(action: Action, data: any) {}
|
||||
|
||||
_resetActions() {
|
||||
|
|
|
|||
|
|
@ -64,6 +64,7 @@ export class ActionService implements OnDestroy {
|
|||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Request a refresh of Metadata for a given Library
|
||||
* @param library Partial Library, must have id and name populated
|
||||
|
|
@ -90,6 +91,32 @@ export class ActionService implements OnDestroy {
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Request an analysis of files for a given Library (currently just word count)
|
||||
* @param library Partial Library, must have id and name populated
|
||||
* @param callback Optional callback to perform actions after API completes
|
||||
* @returns
|
||||
*/
|
||||
async analyzeFiles(library: Partial<Library>, callback?: LibraryActionCallback) {
|
||||
if (!library.hasOwnProperty('id') || library.id === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!await this.confirmService.alert('This is a long running process. Please give it the time to complete before invoking again.')) {
|
||||
if (callback) {
|
||||
callback(library);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
this.libraryService.analyze(library?.id).pipe(take(1)).subscribe((res: any) => {
|
||||
this.toastr.info('Library file analysis queued for ' + library.name);
|
||||
if (callback) {
|
||||
callback(library);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark a series as read; updates the series pagesRead
|
||||
* @param series Series, must have id and name populated
|
||||
|
|
@ -121,7 +148,7 @@ export class ActionService implements OnDestroy {
|
|||
}
|
||||
|
||||
/**
|
||||
* Start a file scan for a Series (currently just does the library not the series directly)
|
||||
* Start a file scan for a Series
|
||||
* @param series Series, must have libraryId and name populated
|
||||
* @param callback Optional callback to perform actions after API completes
|
||||
*/
|
||||
|
|
@ -134,6 +161,20 @@ export class ActionService implements OnDestroy {
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Start a file scan for analyze files for a Series
|
||||
* @param series Series, must have libraryId and name populated
|
||||
* @param callback Optional callback to perform actions after API completes
|
||||
*/
|
||||
analyzeFilesForSeries(series: Series, callback?: SeriesActionCallback) {
|
||||
this.seriesService.analyzeFiles(series.libraryId, series.id).pipe(take(1)).subscribe((res: any) => {
|
||||
this.toastr.info('Scan queued for ' + series.name);
|
||||
if (callback) {
|
||||
callback(series);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Start a metadata refresh for a Series
|
||||
* @param series Series, must have libraryId, id and name populated
|
||||
|
|
|
|||
|
|
@ -74,6 +74,10 @@ export class LibraryService {
|
|||
return this.httpClient.post(this.baseUrl + 'library/scan?libraryId=' + libraryId, {});
|
||||
}
|
||||
|
||||
analyze(libraryId: number) {
|
||||
return this.httpClient.post(this.baseUrl + 'library/analyze?libraryId=' + libraryId, {});
|
||||
}
|
||||
|
||||
refreshMetadata(libraryId: number) {
|
||||
return this.httpClient.post(this.baseUrl + 'library/refresh-metadata?libraryId=' + libraryId, {});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ export class MetadataService {
|
|||
baseUrl = environment.apiUrl;
|
||||
|
||||
private ageRatingTypes: {[key: number]: string} | undefined = undefined;
|
||||
private validLanguages: Array<Language> = [];
|
||||
|
||||
constructor(private httpClient: HttpClient, private utilityService: UtilityService) { }
|
||||
|
||||
|
|
@ -81,7 +82,12 @@ export class MetadataService {
|
|||
* All the potential language tags there can be
|
||||
*/
|
||||
getAllValidLanguages() {
|
||||
return this.httpClient.get<Array<Language>>(this.baseUrl + 'metadata/all-languages');
|
||||
if (this.validLanguages != undefined && this.validLanguages.length > 0) {
|
||||
return of(this.validLanguages);
|
||||
}
|
||||
return this.httpClient.get<Array<Language>>(this.baseUrl + 'metadata/all-languages').pipe(map(l => this.validLanguages = l));
|
||||
|
||||
//return this.httpClient.get<Array<Language>>(this.baseUrl + 'metadata/all-languages').pipe();
|
||||
}
|
||||
|
||||
getAllPeople(libraries?: Array<number>) {
|
||||
|
|
|
|||
|
|
@ -145,6 +145,10 @@ export class SeriesService {
|
|||
return this.httpClient.post(this.baseUrl + 'series/scan', {libraryId: libraryId, seriesId: seriesId});
|
||||
}
|
||||
|
||||
analyzeFiles(libraryId: number, seriesId: number) {
|
||||
return this.httpClient.post(this.baseUrl + 'series/analyze', {libraryId: libraryId, seriesId: seriesId});
|
||||
}
|
||||
|
||||
getMetadata(seriesId: number) {
|
||||
return this.httpClient.get<SeriesMetadata>(this.baseUrl + 'series/metadata?seriesId=' + seriesId).pipe(map(items => {
|
||||
items?.collectionTags.forEach(tag => tag.coverImage = this.imageService.getCollectionCoverImage(tag.id));
|
||||
|
|
|
|||
|
|
@ -4,8 +4,6 @@ import { AuthGuard } from './_guards/auth.guard';
|
|||
import { LibraryAccessGuard } from './_guards/library-access.guard';
|
||||
import { AdminGuard } from './_guards/admin.guard';
|
||||
|
||||
// TODO: Once we modularize the components, use this and measure performance impact: https://angular.io/guide/lazy-loading-ngmodules#preloading-modules
|
||||
// TODO: Use Prefetching of LazyLoaded Modules
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: 'admin',
|
||||
|
|
@ -72,13 +70,9 @@ const routes: Routes = [
|
|||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
path: 'theme',
|
||||
loadChildren: () => import('../app/dev-only/dev-only.module').then(m => m.DevOnlyModule)
|
||||
},
|
||||
{path: 'login', loadChildren: () => import('../app/registration/registration.module').then(m => m.RegistrationModule)},
|
||||
{path: '**', pathMatch: 'full', redirectTo: 'libraries'},
|
||||
{path: '', pathMatch: 'full', redirectTo: 'libraries'},
|
||||
{path: '', pathMatch: 'full', redirectTo: 'login'},
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
|
|
|
|||
|
|
@ -71,7 +71,7 @@ export class SeriesCardComponent implements OnInit, OnChanges, OnDestroy {
|
|||
|
||||
ngOnChanges(changes: any) {
|
||||
if (this.data) {
|
||||
this.actions = this.actionFactoryService.getSeriesActions((action: Action, series: Series) => this.handleSeriesActionCallback(action, series)).filter(action => this.actionFactoryService.filterBookmarksForFormat(action, this.data));
|
||||
this.actions = this.actionFactoryService.getSeriesActions((action: Action, series: Series) => this.handleSeriesActionCallback(action, series));
|
||||
this.imageUrl = this.imageService.randomize(this.imageService.getSeriesCoverImage(this.data.id));
|
||||
}
|
||||
}
|
||||
|
|
@ -102,10 +102,13 @@ export class SeriesCardComponent implements OnInit, OnChanges, OnDestroy {
|
|||
this.openEditModal(series);
|
||||
break;
|
||||
case(Action.AddToReadingList):
|
||||
this.actionService.addSeriesToReadingList(series, (series) => {/* No Operation */ });
|
||||
this.actionService.addSeriesToReadingList(series);
|
||||
break;
|
||||
case(Action.AddToCollection):
|
||||
this.actionService.addMultipleSeriesToCollectionTag([series], () => {/* No Operation */ });
|
||||
this.actionService.addMultipleSeriesToCollectionTag([series]);
|
||||
break;
|
||||
case (Action.AnalyzeFiles):
|
||||
this.actionService.analyzeFilesForSeries(series);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
|
|
|
|||
|
|
@ -1,18 +0,0 @@
|
|||
import { NgModule } from '@angular/core';
|
||||
import { Routes, RouterModule } from '@angular/router';
|
||||
import { ThemeTestComponent } from './theme-test/theme-test.component';
|
||||
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: ThemeTestComponent,
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forChild(routes), ],
|
||||
exports: [RouterModule]
|
||||
})
|
||||
export class DevOnlyRoutingModule { }
|
||||
|
|
@ -1,37 +0,0 @@
|
|||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { NgbAccordionModule, NgbNavModule } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { CardsModule } from '../cards/cards.module';
|
||||
import { TypeaheadModule } from '../typeahead/typeahead.module';
|
||||
import { ThemeTestComponent } from './theme-test/theme-test.component';
|
||||
import { SharedModule } from '../shared/shared.module';
|
||||
import { PipeModule } from '../pipe/pipe.module';
|
||||
import { DevOnlyRoutingModule } from './dev-only-routing.module';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
|
||||
/**
|
||||
* This module contains components that aren't meant to ship with main code. They are there to test things out. This module may be deleted in future updates.
|
||||
*/
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
ThemeTestComponent
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
|
||||
|
||||
TypeaheadModule,
|
||||
CardsModule,
|
||||
NgbAccordionModule,
|
||||
NgbNavModule,
|
||||
|
||||
|
||||
SharedModule,
|
||||
PipeModule,
|
||||
|
||||
DevOnlyRoutingModule
|
||||
]
|
||||
})
|
||||
export class DevOnlyModule { }
|
||||
|
|
@ -1,188 +0,0 @@
|
|||
<h1>Themes</h1>
|
||||
<button class="btn btn-primary" (click)="themeService?.setTheme('dark')">Dark</button>
|
||||
<button class="btn btn-primary" (click)="themeService?.setTheme('light')">Light</button>
|
||||
<button class="btn btn-primary" (click)="themeService?.setTheme('E-Ink')">E-ink</button>
|
||||
<button class="btn btn-primary" (click)="themeService?.setTheme('custom')">Custom</button>
|
||||
|
||||
|
||||
|
||||
<h2>Buttons</h2>
|
||||
<button class="btn btn-primary">Primary</button>
|
||||
<button class="btn btn-secondary">secondary</button>
|
||||
<button class="btn btn-secondary alt">secondary alt</button>
|
||||
<button class="btn btn-outline-primary">outline primary</button>
|
||||
<button class="btn btn-outline-secondary">outline secondary</button>
|
||||
<button class="btn btn-link">btn link</button>
|
||||
<button class="btn btn-icon">
|
||||
<i class="fa fa-angle-left"></i> Icon
|
||||
</button>
|
||||
|
||||
<h2>Toastr</h2>
|
||||
|
||||
<button class="btn btn-primary" (click)="toastr.success('Test')">Success</button>
|
||||
<button class="btn btn-danger" (click)="toastr.error('Test')">Error</button>
|
||||
<button class="btn btn-secondary" (click)="toastr.warning('Test')">Warning</button>
|
||||
<button class="btn btn-link" (click)="toastr.info('Test')">Info</button>
|
||||
|
||||
<h2>Inputs</h2>
|
||||
<p>Inputs should always have class="form-control" on them</p>
|
||||
<label>Normal</label>
|
||||
<input type="text" class="form-control">
|
||||
<label>Readonly</label>
|
||||
<input type="text" readonly class="form-control">
|
||||
<label>Placeholder</label>
|
||||
<input type="text" placeholder="Hello, I'm a placeholder" class="form-control">
|
||||
<label>Disabled</label>
|
||||
<input type="text" placeholder="Hello, I'm a placeholder" [disabled]="true" class="form-control">
|
||||
|
||||
<h2>Checkbox</h2>
|
||||
<div class="mb-3">
|
||||
<label for="stat-collection" class="form-label" aria-describedby="collection-info">Allow Anonymous Usage Collection</label>
|
||||
<div class="form-check">
|
||||
<input id="stat-collection" type="checkbox" aria-label="Stat Collection" class="form-check-input">
|
||||
<label for="stat-collection" class="form-check-label">Normal Checkbox</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="stat-collection" class="form-label" aria-describedby="collection-info">Allow Anonymous Usage Collection</label>
|
||||
<div class="form-check">
|
||||
<input id="stat-collection" type="checkbox" aria-label="Stat Collection" class="form-check-input" [disabled]="true">
|
||||
<label for="stat-collection" class="form-check-label">Disabled Checkbox</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2>Radio</h2>
|
||||
<p>Labels should have form-check-label on them and inputs form-check-input</p>
|
||||
<div class="mb-3">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="radio" id="site-dark-mode" [value]="true" aria-labelledby="site-dark-mode-label">
|
||||
<label class="form-check-label" for="site-dark-mode">True</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="radio" id="site-not-dark-mode2" [value]="false" aria-labelledby="site-dark-mode-label">
|
||||
<label class="form-check-label" for="site-not-dark-mode2">False</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="radio" id="site-not-dark-mode3" [disabled]="true" [value]="false" aria-labelledby="site-dark-mode-label">
|
||||
<label class="form-check-label" for="site-not-dark-mode3">Disabled</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="radio" id="site-not-dark-mode4" readonly [value]="false" aria-labelledby="site-dark-mode-label">
|
||||
<label class="form-check-label" for="site-not-dark-mode4">Readonly</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<h2>Nav tabs</h2>
|
||||
<h3>Tabs</h3>
|
||||
<ul ngbNav #nav="ngbNav" [(activeId)]="active" class="nav-tabs nav-pills">
|
||||
<li *ngFor="let tab of tabs" [ngbNavItem]="tab">
|
||||
<a ngbNavLink routerLink=".">{{ tab.title | sentenceCase }}</a>
|
||||
<ng-template ngbNavContent>
|
||||
<ng-container>
|
||||
Tab 1
|
||||
</ng-container>
|
||||
<ng-container>
|
||||
Tab 2
|
||||
</ng-container>
|
||||
</ng-template>
|
||||
</li>
|
||||
</ul>
|
||||
<div [ngbNavOutlet]="nav" class="mt-3"></div>
|
||||
|
||||
<h3>Tabs</h3>
|
||||
<nav role="navigation">
|
||||
<ul ngbNav #nav="ngbNav" [(activeId)]="active" class="nav nav-pills justify-content-center mt-3" role="tab">
|
||||
<li *ngFor="let tab of tabs" [ngbNavItem]="tab" class="nav-item">
|
||||
<a ngbNavLink routerLink="." [fragment]="tab.fragment">{{ tab.title | titlecase }}</a>
|
||||
<ng-template ngbNavContent>
|
||||
<ng-container>
|
||||
Tab 1
|
||||
</ng-container>
|
||||
<ng-container>
|
||||
Tab 2
|
||||
</ng-container>
|
||||
</ng-template>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
<div [ngbNavOutlet]="nav" class="mt-3"></div>
|
||||
|
||||
<h2>Tag Badge</h2>
|
||||
<div class="g-2">
|
||||
<app-tag-badge [selectionMode]="TagBadgeCursor.Selectable">Selectable</app-tag-badge>
|
||||
<app-tag-badge [selectionMode]="TagBadgeCursor.Clickable">Clickable</app-tag-badge>
|
||||
<app-tag-badge [selectionMode]="TagBadgeCursor.NotAllowed">Non Allowed</app-tag-badge>
|
||||
</div>
|
||||
|
||||
<h2>Person Badge with Expander</h2>
|
||||
<div class="g-2">
|
||||
<app-person-badge></app-person-badge>
|
||||
<app-badge-expander [items]="people" [itemsTillExpander]="1">
|
||||
<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>
|
||||
|
||||
<h2>Switch</h2>
|
||||
<form>
|
||||
<div class="mb-3">
|
||||
<label id="auto-close-label" class="form-label"></label>
|
||||
<div class="mb-3">
|
||||
<div class="form-check form-switch">
|
||||
<input type="checkbox" id="auto-close" class="form-check-input" aria-labelledby="auto-close-label">
|
||||
<label class="form-check-label" for="auto-close">Auto Close Menu</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<h2>Dropdown/List Group</h2>
|
||||
<div class="dropdown" >
|
||||
<ul class="list-group" role="listbox" id="dropdown">
|
||||
<li class="list-group-item">Item 1</li>
|
||||
<li class="list-group-item">Item 2</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<h2>Accordion</h2>
|
||||
<ngb-accordion [closeOthers]="true" activeIds="reading-panel" #acc="ngbAccordion">
|
||||
<ngb-panel id="reading-panel" title="Reading">
|
||||
<ng-template ngbPanelHeader>
|
||||
<h2 class="accordion-header">
|
||||
<button class="accordion-button" ngbPanelToggle type="button" [attr.aria-expanded]="acc.isExpanded('reading-panel')" aria-controls="collapseOne">
|
||||
Reading
|
||||
</button>
|
||||
</h2>
|
||||
</ng-template>
|
||||
<ng-template ngbPanelContent>
|
||||
<p>This is the body of the accordion...........This is the body of the accordion asdfasdf asThis is the body of the accordion asdfasdf asThis is the body of the accordion asdfasdf asThis is the body of the accordion asdfasdf asThis is the body of the accordion asdfasdf as</p>
|
||||
</ng-template>
|
||||
</ngb-panel>
|
||||
|
||||
<ngb-panel id="reading-panel2">
|
||||
<ng-template ngbPanelHeader>
|
||||
<h2 class="accordion-header">
|
||||
<button class="accordion-button" ngbPanelToggle type="button" [attr.aria-expanded]="acc.isExpanded('reading-panel')" aria-controls="collapseOne">
|
||||
Header 2
|
||||
</button>
|
||||
</h2>
|
||||
</ng-template>
|
||||
<ng-template ngbPanelContent>
|
||||
<p>This is the body of the accordion asdfasdf as
|
||||
dfas
|
||||
f asdfasdfasdf asdfasdfaaff asdf
|
||||
as fd
|
||||
asfasf asdfasdfafd
|
||||
</p>
|
||||
</ng-template>
|
||||
</ngb-panel>
|
||||
</ngb-accordion>
|
||||
|
||||
|
||||
<h2>Cards</h2>
|
||||
<app-card-item [entity]="seriesNotRead"></app-card-item>
|
||||
<app-card-item [entity]="seriesNotRead" [count]="10"></app-card-item>
|
||||
<app-card-item [entity]="seriesWithProgress"></app-card-item>
|
||||
|
|
@ -1,84 +0,0 @@
|
|||
import { Component, OnInit } from '@angular/core';
|
||||
import { ToastrService } from 'ngx-toastr';
|
||||
import { TagBadgeCursor } from '../../shared/tag-badge/tag-badge.component';
|
||||
import { ThemeService } from '../../_services/theme.service';
|
||||
import { MangaFormat } from '../../_models/manga-format';
|
||||
import { Person, PersonRole } from '../../_models/person';
|
||||
import { Series } from '../../_models/series';
|
||||
import { NavService } from '../../_services/nav.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-theme-test',
|
||||
templateUrl: './theme-test.component.html',
|
||||
styleUrls: ['./theme-test.component.scss']
|
||||
})
|
||||
export class ThemeTestComponent implements OnInit {
|
||||
|
||||
tabs: Array<{title: string, fragment: string}> = [
|
||||
{title: 'General', fragment: ''},
|
||||
{title: 'Users', fragment: 'users'},
|
||||
{title: 'Libraries', fragment: 'libraries'},
|
||||
{title: 'System', fragment: 'system'},
|
||||
{title: 'Changelog', fragment: 'changelog'},
|
||||
];
|
||||
active = this.tabs[0];
|
||||
|
||||
people: Array<Person> = [
|
||||
{id: 1, name: 'Joe', role: PersonRole.Artist},
|
||||
{id: 2, name: 'Joe 2', role: PersonRole.Artist},
|
||||
];
|
||||
|
||||
seriesNotRead: Series = {
|
||||
id: 1,
|
||||
name: 'Test Series',
|
||||
pages: 0,
|
||||
pagesRead: 10,
|
||||
format: MangaFormat.ARCHIVE,
|
||||
libraryId: 1,
|
||||
coverImageLocked: false,
|
||||
created: '',
|
||||
latestReadDate: '',
|
||||
localizedName: '',
|
||||
originalName: '',
|
||||
sortName: '',
|
||||
userRating: 0,
|
||||
userReview: '',
|
||||
volumes: [],
|
||||
localizedNameLocked: false,
|
||||
nameLocked: false,
|
||||
sortNameLocked: false,
|
||||
lastChapterAdded: '',
|
||||
}
|
||||
|
||||
seriesWithProgress: Series = {
|
||||
id: 1,
|
||||
name: 'Test Series',
|
||||
pages: 5,
|
||||
pagesRead: 10,
|
||||
format: MangaFormat.ARCHIVE,
|
||||
libraryId: 1,
|
||||
coverImageLocked: false,
|
||||
created: '',
|
||||
latestReadDate: '',
|
||||
localizedName: '',
|
||||
originalName: '',
|
||||
sortName: '',
|
||||
userRating: 0,
|
||||
userReview: '',
|
||||
volumes: [],
|
||||
localizedNameLocked: false,
|
||||
nameLocked: false,
|
||||
sortNameLocked: false,
|
||||
lastChapterAdded: '',
|
||||
}
|
||||
|
||||
get TagBadgeCursor(): typeof TagBadgeCursor {
|
||||
return TagBadgeCursor;
|
||||
}
|
||||
|
||||
constructor(public toastr: ToastrService, public navService: NavService, public themeService: ThemeService) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -3,7 +3,7 @@
|
|||
<app-card-actionables [actions]="actions" (actionHandler)="performAction($event)"></app-card-actionables>
|
||||
{{libraryName}}
|
||||
</h2>
|
||||
<h6 subtitle style="margin-left:40px;" *ngIf="active.fragment === ''">{{pagination?.totalItems}} Series</h6>
|
||||
<h6 subtitle class="subtitle-with-actionables" *ngIf="active.fragment === ''">{{pagination?.totalItems}} Series</h6>
|
||||
<div main>
|
||||
<ul ngbNav #nav="ngbNav" [(activeId)]="active" class="nav nav-pills" style="flex-wrap: nowrap;">
|
||||
<li *ngFor="let tab of tabs" [ngbNavItem]="tab">
|
||||
|
|
|
|||
18
UI/Web/src/app/pipe/compact-number.pipe.ts
Normal file
18
UI/Web/src/app/pipe/compact-number.pipe.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import { Pipe, PipeTransform } from '@angular/core';
|
||||
|
||||
|
||||
const formatter = new Intl.NumberFormat('en-GB', {
|
||||
//@ts-ignore
|
||||
notation: 'compact' // https://github.com/microsoft/TypeScript/issues/36533
|
||||
});
|
||||
|
||||
@Pipe({
|
||||
name: 'compactNumber'
|
||||
})
|
||||
export class CompactNumberPipe implements PipeTransform {
|
||||
|
||||
transform(value: number): string {
|
||||
return formatter.format(value);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -5,8 +5,8 @@ import { Pipe, PipeTransform } from '@angular/core';
|
|||
})
|
||||
export class DefaultValuePipe implements PipeTransform {
|
||||
|
||||
transform(value: any): string {
|
||||
if (value === null || value === undefined || value === '' || value === Infinity || value === NaN || value === {}) return '—';
|
||||
transform(value: any, replacementString = '—'): string {
|
||||
if (value === null || value === undefined || value === '' || value === Infinity || value === NaN || value === {}) return replacementString;
|
||||
return value;
|
||||
}
|
||||
|
||||
|
|
|
|||
19
UI/Web/src/app/pipe/language-name.pipe.ts
Normal file
19
UI/Web/src/app/pipe/language-name.pipe.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import { Pipe, PipeTransform } from '@angular/core';
|
||||
import { map, Observable } from 'rxjs';
|
||||
import { MetadataService } from '../_services/metadata.service';
|
||||
|
||||
@Pipe({
|
||||
name: 'languageName'
|
||||
})
|
||||
export class LanguageNamePipe implements PipeTransform {
|
||||
|
||||
constructor(private metadataService: MetadataService) {
|
||||
}
|
||||
|
||||
transform(isoCode: string): Observable<string> {
|
||||
return this.metadataService.getAllValidLanguages().pipe(map(lang => {
|
||||
return lang.filter(l => l.isoCode === isoCode)[0].title;
|
||||
}));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -7,6 +7,8 @@ import { PersonRolePipe } from './person-role.pipe';
|
|||
import { SafeHtmlPipe } from './safe-html.pipe';
|
||||
import { RelationshipPipe } from './relationship.pipe';
|
||||
import { DefaultValuePipe } from './default-value.pipe';
|
||||
import { CompactNumberPipe } from './compact-number.pipe';
|
||||
import { LanguageNamePipe } from './language-name.pipe';
|
||||
|
||||
|
||||
|
||||
|
|
@ -18,7 +20,9 @@ import { DefaultValuePipe } from './default-value.pipe';
|
|||
SentenceCasePipe,
|
||||
SafeHtmlPipe,
|
||||
RelationshipPipe,
|
||||
DefaultValuePipe
|
||||
DefaultValuePipe,
|
||||
CompactNumberPipe,
|
||||
LanguageNamePipe
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
|
|
@ -30,7 +34,9 @@ import { DefaultValuePipe } from './default-value.pipe';
|
|||
SentenceCasePipe,
|
||||
SafeHtmlPipe,
|
||||
RelationshipPipe,
|
||||
DefaultValuePipe
|
||||
DefaultValuePipe,
|
||||
CompactNumberPipe,
|
||||
LanguageNamePipe
|
||||
]
|
||||
})
|
||||
export class PipeModule { }
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
</span>
|
||||
{{readingList?.title}} <span *ngIf="readingList?.promoted">(<i class="fa fa-angle-double-up" aria-hidden="true"></i>)</span>
|
||||
</h2>
|
||||
<h6 subtitle style="margin-left: 40px">{{items.length}} Items</h6>
|
||||
<h6 subtitle class="subtitle-with-actionables">{{items.length}} Items</h6>
|
||||
</app-side-nav-companion-bar>
|
||||
<div class="container-fluid mt-2" *ngIf="readingList">
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
</h2>
|
||||
</ng-container>
|
||||
<ng-container subtitle *ngIf="series?.localizedName !== series?.name">
|
||||
<h6 style="margin-left:40px;" title="Localized Name">{{series?.localizedName}}</h6>
|
||||
<h6 class="subtitle-with-actionables" title="Localized Name">{{series?.localizedName}}</h6>
|
||||
</ng-container>
|
||||
</app-side-nav-companion-bar>
|
||||
<div class="container-fluid pt-2" *ngIf="series !== undefined">
|
||||
|
|
@ -62,6 +62,26 @@
|
|||
<app-series-metadata-detail [seriesMetadata]="seriesMetadata" [readingLists]="readingLists" [series]="series"></app-series-metadata-detail>
|
||||
</div>
|
||||
|
||||
<!-- <ng-container>
|
||||
<div class="row g-0">
|
||||
<div class="col-2">
|
||||
<i class="fa-regular fa-file-lines" aria-hidden="true"></i>
|
||||
{{series.pages}} Pages
|
||||
</div>
|
||||
|
|
||||
<div class="col-2">
|
||||
<i class="fa-regular fa-clock" aria-hidden="true"></i>
|
||||
1-2 Hours to Read
|
||||
</div>
|
||||
<ng-container *ngIf="utilityService.mangaFormat(series.format) === 'EPUB'">
|
||||
|
|
||||
<div class="col-2">
|
||||
<i class="fa-regular fa-book-open" aria-hidden="true"></i>
|
||||
10K Total Words
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
</ng-container> -->
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -290,6 +290,9 @@ export class SeriesDetailComponent implements OnInit, OnDestroy {
|
|||
case(Action.AddToCollection):
|
||||
this.actionService.addMultipleSeriesToCollectionTag([series], () => this.actionInProgress = false);
|
||||
break;
|
||||
case (Action.AnalyzeFiles):
|
||||
this.actionService.analyzeFilesForSeries(series, () => this.actionInProgress = false);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
|
@ -372,12 +375,10 @@ export class SeriesDetailComponent implements OnInit, OnDestroy {
|
|||
this.titleService.setTitle('Kavita - ' + this.series.name + ' Details');
|
||||
|
||||
this.seriesActions = this.actionFactoryService.getSeriesActions(this.handleSeriesActionCallback.bind(this))
|
||||
.filter(action => action.action !== Action.Edit)
|
||||
.filter(action => this.actionFactoryService.filterBookmarksForFormat(action, this.series));
|
||||
.filter(action => action.action !== Action.Edit);
|
||||
this.volumeActions = this.actionFactoryService.getVolumeActions(this.handleVolumeActionCallback.bind(this));
|
||||
this.chapterActions = this.actionFactoryService.getChapterActions(this.handleChapterActionCallback.bind(this));
|
||||
|
||||
// TODO: Move this to a forkJoin?
|
||||
this.seriesService.getRelatedForSeries(this.seriesId).subscribe((relations: RelatedSeries) => {
|
||||
this.relations = [
|
||||
...relations.prequels.map(item => this.createRelatedSeries(item, RelationKind.Prequel)),
|
||||
|
|
|
|||
|
|
@ -3,22 +3,85 @@
|
|||
</div>
|
||||
|
||||
<!-- This first row will have random information about the series-->
|
||||
<div class="row g-0 mb-2">
|
||||
<app-tag-badge title="Age Rating" *ngIf="seriesMetadata.ageRating" a11y-click="13,32" class="clickable col-auto" (click)="goTo(FilterQueryParam.AgeRating, seriesMetadata.ageRating)" [selectionMode]="TagBadgeCursor.Clickable">{{metadataService.getAgeRating(this.seriesMetadata.ageRating) | async}}</app-tag-badge>
|
||||
<div class="row g-0 mb-4 mt-3">
|
||||
<ng-container *ngIf="seriesMetadata.ageRating">
|
||||
<div class="col-auto">
|
||||
<app-icon-and-title [clickable]="true" fontClasses="fas fa-eye" (click)="goTo(FilterQueryParam.AgeRating, seriesMetadata.ageRating)" title="Age Rating">
|
||||
{{metadataService.getAgeRating(this.seriesMetadata.ageRating) | async}}
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
<div class="vr m-2"></div>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="series">
|
||||
<app-tag-badge *ngIf="seriesMetadata.releaseYear > 0" title="Release date" class="col-auto">{{seriesMetadata.releaseYear}}</app-tag-badge>
|
||||
<app-tag-badge *ngIf="seriesMetadata.language !== null && seriesMetadata.language !== ''" title="Language" a11y-click="13,32" class="col-auto" (click)="goTo(FilterQueryParam.Languages, seriesMetadata.language)" [selectionMode]="TagBadgeCursor.Clickable">{{seriesMetadata.language}}</app-tag-badge>
|
||||
|
||||
<app-tag-badge title="Publication Status ({{seriesMetadata.maxCount}} / {{seriesMetadata.totalCount}})" [fillStyle]="seriesMetadata.maxCount != 0 && seriesMetadata.totalCount != 0 && seriesMetadata.maxCount >= seriesMetadata.totalCount ? 'filled' : 'outline'" a11y-click="13,32" class="col-auto"
|
||||
(click)="goTo(FilterQueryParam.PublicationStatus, seriesMetadata.publicationStatus)"
|
||||
[selectionMode]="TagBadgeCursor.Clickable">{{seriesMetadata.publicationStatus | publicationStatus}}</app-tag-badge>
|
||||
<ng-container *ngIf="seriesMetadata.releaseYear > 0">
|
||||
<div class="col-auto mb-2">
|
||||
<app-icon-and-title [clickable]="false" fontClasses="fa-regular fa-calendar" title="Release Year">
|
||||
{{seriesMetadata.releaseYear}}
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
<div class="vr m-2"></div>
|
||||
</ng-container>
|
||||
|
||||
<app-tag-badge a11y-click="13,32" class="col-auto" (click)="goTo(FilterQueryParam.Format, series.format)" [selectionMode]="TagBadgeCursor.Clickable">
|
||||
<app-series-format [format]="series.format">{{utilityService.mangaFormat(series.format)}}</app-series-format>
|
||||
</app-tag-badge>
|
||||
<app-tag-badge title="Last Read" class="col-auto" *ngIf="series.latestReadDate && series.latestReadDate !== '' && (series.latestReadDate | date: 'shortDate') !== '1/1/01'" [selectionMode]="TagBadgeCursor.Selectable">
|
||||
Last Read: {{series.latestReadDate | date:'shortDate'}}
|
||||
</app-tag-badge>
|
||||
<ng-container *ngIf="seriesMetadata.language !== null">
|
||||
<div class="col-auto mb-2">
|
||||
<app-icon-and-title [clickable]="true" fontClasses="fas fa-language" (click)="goTo(FilterQueryParam.Languages, seriesMetadata.language)" title="Language">
|
||||
{{seriesMetadata.language | defaultValue:'en' | languageName | async}}
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
<div class="vr m-2"></div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container>
|
||||
<div class="col-auto mb-2">
|
||||
<app-icon-and-title [clickable]="true" fontClasses="fa-solid fa-hourglass-empty" (click)="goTo(FilterQueryParam.PublicationStatus, seriesMetadata.publicationStatus)" title="Publication Status ({{seriesMetadata.maxCount}} / {{seriesMetadata.totalCount}})">
|
||||
{{seriesMetadata.publicationStatus | publicationStatus}}
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
<div class="vr m-2 mb-2"></div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container>
|
||||
<div class="col-auto mb-2">
|
||||
<app-icon-and-title [clickable]="true" [fontClasses]="'fa ' + utilityService.mangaFormatIcon(series.format)" (click)="goTo(FilterQueryParam.Format, series.format)" title="Format">
|
||||
{{utilityService.mangaFormat(series.format)}}
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
<div class="vr m-2"></div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="series.latestReadDate && series.latestReadDate !== '' && (series.latestReadDate | date: 'shortDate') !== '1/1/01'">
|
||||
<div class="col-auto mb-2">
|
||||
<app-icon-and-title [clickable]="false" fontClasses="fa-regular fa-clock" title="Last Read">
|
||||
{{series.latestReadDate | date:'shortDate'}}
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
<div class="vr m-2"></div>
|
||||
</ng-container>
|
||||
|
||||
|
||||
<div class="col-auto mb-2">
|
||||
<app-icon-and-title [clickable]="false" fontClasses="fa-regular fa-file-lines">
|
||||
{{series.pages}} Pages
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
<div class="vr m-2"></div>
|
||||
|
||||
<ng-container *ngIf="series.format === MangaFormat.EPUB && series.wordCount > 0 || series.format !== MangaFormat.EPUB">
|
||||
<div class="col-auto mb-2">
|
||||
<app-icon-and-title [clickable]="false" fontClasses="fa-regular fa-clock">
|
||||
{{minHoursToRead}}{{maxHoursToRead !== minHoursToRead ? ('-' + maxHoursToRead) : ''}} Hours
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="series.format === MangaFormat.EPUB && series.wordCount > 0">
|
||||
<div class="vr m-2"></div>
|
||||
<div class="col-auto mb-2">
|
||||
<app-icon-and-title [clickable]="false" fontClasses="fa-solid fa-book-open">
|
||||
{{series.wordCount | compactNumber}} Words
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
</div>
|
||||
|
||||
|
|
@ -92,11 +155,6 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-0">
|
||||
<hr class="col mt-3" *ngIf="hasExtendedProperites" >
|
||||
<a [class.hidden]="hasExtendedProperites" *ngIf="hasExtendedProperites" class="col col-md-auto align-self-end read-more-link" (click)="toggleView()"> <i aria-hidden="true" class="fa fa-caret-{{isCollapsed ? 'down' : 'up'}}" aria-controls="extended-series-metadata"></i> See {{isCollapsed ? 'More' : 'Less'}}</a>
|
||||
</div>
|
||||
|
||||
<div #collapse="ngbCollapse" [(ngbCollapse)]="isCollapsed" id="extended-series-metadata">
|
||||
<div class="row g-0 mt-1" *ngIf="seriesMetadata.coverArtists && seriesMetadata.coverArtists.length > 0">
|
||||
<div class="col-md-4">
|
||||
|
|
@ -213,4 +271,13 @@
|
|||
</app-badge-expander>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-0">
|
||||
<hr class="col mt-3" *ngIf="hasExtendedProperites" >
|
||||
<a [class.hidden]="hasExtendedProperites" *ngIf="hasExtendedProperites"
|
||||
class="col col-md-auto align-self-end read-more-link" (click)="toggleView()">
|
||||
<i aria-hidden="true" class="fa fa-caret-{{isCollapsed ? 'down' : 'up'}} me-1" aria-controls="extended-series-metadata"></i>
|
||||
See {{isCollapsed ? 'More' : 'Less'}}
|
||||
</a>
|
||||
</div>
|
||||
|
|
@ -9,6 +9,12 @@ import { Series } from '../../_models/series';
|
|||
import { SeriesMetadata } from '../../_models/series-metadata';
|
||||
import { MetadataService } from '../../_services/metadata.service';
|
||||
|
||||
const MAX_WORDS_PER_HOUR = 30_000;
|
||||
const MIN_WORDS_PER_HOUR = 10_260;
|
||||
const MAX_PAGES_PER_MINUTE = 2.75;
|
||||
const MIN_PAGES_PER_MINUTE = 3.33;
|
||||
|
||||
|
||||
@Component({
|
||||
selector: 'app-series-metadata-detail',
|
||||
templateUrl: './series-metadata-detail.component.html',
|
||||
|
|
@ -26,6 +32,9 @@ export class SeriesMetadataDetailComponent implements OnInit, OnChanges {
|
|||
isCollapsed: boolean = true;
|
||||
hasExtendedProperites: boolean = false;
|
||||
|
||||
minHoursToRead: number = 1;
|
||||
maxHoursToRead: number = 1;
|
||||
|
||||
/**
|
||||
* Html representation of Series Summary
|
||||
*/
|
||||
|
|
@ -58,8 +67,19 @@ export class SeriesMetadataDetailComponent implements OnInit, OnChanges {
|
|||
|
||||
if (this.seriesMetadata !== null) {
|
||||
this.seriesSummary = (this.seriesMetadata.summary === null ? '' : this.seriesMetadata.summary).replace(/\n/g, '<br>');
|
||||
|
||||
|
||||
|
||||
}
|
||||
if (this.series !== null && this.series.wordCount > 0) {
|
||||
if (this.series.format === MangaFormat.EPUB) {
|
||||
this.minHoursToRead = parseInt(Math.round(this.series.wordCount / MAX_WORDS_PER_HOUR) + '', 10);
|
||||
this.maxHoursToRead = parseInt(Math.round(this.series.wordCount / MIN_WORDS_PER_HOUR) + '', 10);
|
||||
} else if (this.series.format === MangaFormat.IMAGE || this.series.format === MangaFormat.ARCHIVE) {
|
||||
this.minHoursToRead = parseInt(Math.round((this.series.wordCount * MAX_PAGES_PER_MINUTE) / 60) + '', 10);
|
||||
this.maxHoursToRead = parseInt(Math.round((this.series.wordCount * MIN_PAGES_PER_MINUTE) / 60) + '', 10);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,8 @@
|
|||
<div class="d-flex justify-content-center align-self-center align-items-center icon-and-title" [ngClass]="{'clickable': clickable}" [attr.role]="clickable ? 'button' : ''"
|
||||
(click)="handleClick($event)">
|
||||
<i class="{{fontClasses}} mx-auto icon" aria-hidden="true" [title]="title"></i>
|
||||
|
||||
<div style="padding-top: 5px">
|
||||
<ng-content></ng-content>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
.icon-and-title {
|
||||
flex-direction: column;
|
||||
min-width: 60px;
|
||||
}
|
||||
|
||||
.icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-icon-and-title',
|
||||
templateUrl: './icon-and-title.component.html',
|
||||
styleUrls: ['./icon-and-title.component.scss']
|
||||
})
|
||||
export class IconAndTitleComponent implements OnInit {
|
||||
/**
|
||||
* If the component is clickable and should emit click events
|
||||
*/
|
||||
@Input() clickable: boolean = true;
|
||||
@Input() title: string = '';
|
||||
/**
|
||||
* Font classes used to display font
|
||||
*/
|
||||
@Input() fontClasses: string = '';
|
||||
|
||||
@Output() click: EventEmitter<MouseEvent> = new EventEmitter<MouseEvent>();
|
||||
|
||||
|
||||
|
||||
constructor() { }
|
||||
|
||||
ngOnInit(): void {
|
||||
}
|
||||
|
||||
handleClick(event: MouseEvent) {
|
||||
if (this.clickable) this.click.emit(event);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -17,6 +17,7 @@ import { PersonBadgeComponent } from './person-badge/person-badge.component';
|
|||
import { BadgeExpanderComponent } from './badge-expander/badge-expander.component';
|
||||
import { ImageComponent } from './image/image.component';
|
||||
import { PipeModule } from '../pipe/pipe.module';
|
||||
import { IconAndTitleComponent } from './icon-and-title/icon-and-title.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
|
|
@ -32,6 +33,7 @@ import { PipeModule } from '../pipe/pipe.module';
|
|||
PersonBadgeComponent,
|
||||
BadgeExpanderComponent,
|
||||
ImageComponent,
|
||||
IconAndTitleComponent,
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
|
|
@ -55,6 +57,8 @@ import { PipeModule } from '../pipe/pipe.module';
|
|||
|
||||
PersonBadgeComponent, // Used Series Detail
|
||||
BadgeExpanderComponent, // Used Series Detail/Metadata
|
||||
|
||||
IconAndTitleComponent // Used in Series Detail/Metadata
|
||||
|
||||
],
|
||||
})
|
||||
|
|
|
|||
|
|
@ -84,6 +84,9 @@ export class SideNavComponent implements OnInit, OnDestroy {
|
|||
case(Action.RefreshMetadata):
|
||||
this.actionService.refreshMetadata(library);
|
||||
break;
|
||||
case (Action.AnalyzeFiles):
|
||||
this.actionService.analyzeFiles(library);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,3 +25,7 @@ hr {
|
|||
.text-muted {
|
||||
color: var(--text-muted-color) !important;
|
||||
}
|
||||
|
||||
.subtitle-with-actionables {
|
||||
margin-left: 32px;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue