Linked Series (#1230)
* Implemented the ability to link different series together through Edit Series. CSS pending. * Fixed up the css for related cards to show the relation * Working on making all tabs in edit seris modal save in one go. Taking a break. * Some fixes for Robbie to help with styling on * Linked series pill, center library * Centering library detail and related pill spacing - Library detail cards are now centered if total number of items is > 6 or if mobile. - Added ability to determine if mobile (viewport width <= 480px - Fixed related card spacing - Fixed related card pill spacing * Updating relation form spacing * Fixed a bug in card detail layout when there is no pagination, we create one in a way that all items render at once. * Only auto-close side nav on phones, not tablets * Fixed a bug where we had flipped state on sideNavCollapsed$ * Cleaned up some misleading comments * Implemented RBS back in and now if you have a relationship besides prequel/sequel, the target series will show a link back to it's parent. * Added Parentto pipe * Missed a relationship type Co-authored-by: Robbie Davis <robbie@therobbiedavis.com>
This commit is contained in:
parent
7253765f1d
commit
4206ae3e22
47 changed files with 2571 additions and 195 deletions
|
|
@ -0,0 +1,36 @@
|
|||
<div class="container-fluid">
|
||||
|
||||
<p>
|
||||
Not sure what relationship to add? See our <a href="https://wiki.kavitareader.com/en/guides/get-started-using-your-library/series-relationships" target="_blank" referrerpolicy="no-refer">wiki for hints</a>.
|
||||
</p>
|
||||
|
||||
<div class="row g-0" *ngIf="relations.length > 0">
|
||||
<label class="form-label col-md-7">Target Series</label>
|
||||
<label class="form-label col-md-5">Relationship</label>
|
||||
</div>
|
||||
|
||||
<form>
|
||||
<div class="row g-0 mb-3" *ngFor="let relation of relations; let idx = index; let isLast = last;">
|
||||
<div class="col-sm-12 col-md-7">
|
||||
<app-typeahead (selectedData)="updateSeries($event, relation)" [settings]="relation.typeaheadSettings">
|
||||
<ng-template #badgeItem let-item let-position="idx">
|
||||
{{item.name}} ({{libraryNames[item.libraryId]}})
|
||||
</ng-template>
|
||||
<ng-template #optionItem let-item let-position="idx">
|
||||
{{item.name}} ({{libraryNames[item.libraryId]}})
|
||||
</ng-template>
|
||||
</app-typeahead>
|
||||
</div>
|
||||
<div class="col-sm-auto col-md-3">
|
||||
<select class="form-select" [formControl]="relation.formControl">
|
||||
<option *ngFor="let opt of relationOptions" [value]="opt.value">{{opt.text}}</option>
|
||||
</select>
|
||||
</div>
|
||||
<button class="col-sm-auto col-md-2 btn btn-outline-secondary" (click)="removeRelation(idx)">Remove</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="row g-0 mt-3 mb-3">
|
||||
<button class="btn btn-outline-secondary col-md-12" (click)="addNewRelation()">Add Relationship</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -0,0 +1,150 @@
|
|||
import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core';
|
||||
import { FormControl } from '@angular/forms';
|
||||
import { map, Subject, Observable, of, firstValueFrom, takeUntil, ReplaySubject } from 'rxjs';
|
||||
import { UtilityService } from 'src/app/shared/_services/utility.service';
|
||||
import { TypeaheadSettings } from 'src/app/typeahead/typeahead-settings';
|
||||
import { SearchResult } from 'src/app/_models/search-result';
|
||||
import { Series } from 'src/app/_models/series';
|
||||
import { RelationKind, RelationKinds } from 'src/app/_models/series-detail/relation-kind';
|
||||
import { ImageService } from 'src/app/_services/image.service';
|
||||
import { LibraryService } from 'src/app/_services/library.service';
|
||||
import { SeriesService } from 'src/app/_services/series.service';
|
||||
|
||||
interface RelationControl {
|
||||
series: {id: number, name: string} | undefined; // Will add type as well
|
||||
typeaheadSettings: TypeaheadSettings<SearchResult>;
|
||||
formControl: FormControl;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-edit-series-relation',
|
||||
templateUrl: './edit-series-relation.component.html',
|
||||
styleUrls: ['./edit-series-relation.component.scss']
|
||||
})
|
||||
export class EditSeriesRelationComponent implements OnInit, OnDestroy {
|
||||
|
||||
@Input() series!: Series;
|
||||
/**
|
||||
* This will tell the component to save based on it's internal state
|
||||
*/
|
||||
@Input() save: EventEmitter<void> = new EventEmitter();
|
||||
|
||||
@Output() saveApi = new ReplaySubject(1);
|
||||
relationOptions = RelationKinds;
|
||||
|
||||
relations: Array<RelationControl> = [];
|
||||
seriesSettings: TypeaheadSettings<SearchResult> = new TypeaheadSettings();
|
||||
libraryNames: {[key:number]: string} = {};
|
||||
|
||||
|
||||
|
||||
private onDestroy: Subject<void> = new Subject<void>();
|
||||
|
||||
constructor(private seriesService: SeriesService, private utilityService: UtilityService,
|
||||
public imageService: ImageService, private libraryService: LibraryService) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.seriesService.getRelatedForSeries(this.series.id).subscribe(async relations => {
|
||||
this.setupRelationRows(relations.prequels, RelationKind.Prequel);
|
||||
this.setupRelationRows(relations.sequels, RelationKind.Sequel);
|
||||
this.setupRelationRows(relations.sideStories, RelationKind.SideStory);
|
||||
this.setupRelationRows(relations.spinOffs, RelationKind.SpinOff);
|
||||
this.setupRelationRows(relations.adaptations, RelationKind.Adaptation);
|
||||
this.setupRelationRows(relations.others, RelationKind.Other);
|
||||
this.setupRelationRows(relations.characters, RelationKind.Character);
|
||||
this.setupRelationRows(relations.alternativeSettings, RelationKind.AlternativeSetting);
|
||||
this.setupRelationRows(relations.alternativeVersions, RelationKind.AlternativeVersion);
|
||||
this.setupRelationRows(relations.doujinshis, RelationKind.Doujinshi);
|
||||
});
|
||||
|
||||
this.libraryService.getLibraryNames().subscribe(names => {
|
||||
this.libraryNames = names;
|
||||
});
|
||||
|
||||
this.save.pipe(takeUntil(this.onDestroy)).subscribe(() => this.saveState());
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.onDestroy.next();
|
||||
this.onDestroy.complete();
|
||||
}
|
||||
|
||||
setupRelationRows(relations: Array<Series>, kind: RelationKind) {
|
||||
relations.map(async item => {
|
||||
const settings = await firstValueFrom(this.createSeriesTypeahead(item, kind));
|
||||
return {series: item, typeaheadSettings: settings, formControl: new FormControl(kind, [])}
|
||||
}).forEach(async p => {
|
||||
this.relations.push(await p);
|
||||
});
|
||||
}
|
||||
|
||||
async addNewRelation() {
|
||||
this.relations.push({series: undefined, formControl: new FormControl(RelationKind.Adaptation, []), typeaheadSettings: await firstValueFrom(this.createSeriesTypeahead(undefined, RelationKind.Adaptation))});
|
||||
}
|
||||
|
||||
removeRelation(index: number) {
|
||||
this.relations.splice(index, 1);
|
||||
}
|
||||
|
||||
|
||||
updateSeries(event: Array<SearchResult | undefined>, relation: RelationControl) {
|
||||
if (event[0] === undefined) {
|
||||
relation.series = undefined;
|
||||
return;
|
||||
}
|
||||
relation.series = {id: event[0].seriesId, name: event[0].name};
|
||||
}
|
||||
|
||||
createSeriesTypeahead(series: Series | undefined, relationship: RelationKind): Observable<TypeaheadSettings<SearchResult>> {
|
||||
const seriesSettings = new TypeaheadSettings<SearchResult>();
|
||||
seriesSettings.minCharacters = 0;
|
||||
seriesSettings.multiple = false;
|
||||
seriesSettings.id = 'format';
|
||||
seriesSettings.unique = true;
|
||||
seriesSettings.addIfNonExisting = false;
|
||||
seriesSettings.fetchFn = (searchFilter: string) => this.libraryService.search(searchFilter).pipe(
|
||||
map(group => group.series),
|
||||
map(items => seriesSettings.compareFn(items, searchFilter)),
|
||||
map(series => series.filter(s => s.seriesId !== this.series.id)),
|
||||
);
|
||||
|
||||
seriesSettings.compareFn = (options: SearchResult[], filter: string) => {
|
||||
return options.filter(m => this.utilityService.filter(m.name, filter));
|
||||
}
|
||||
|
||||
seriesSettings.selectionCompareFn = (a: SearchResult, b: SearchResult) => {
|
||||
return a.seriesId == b.seriesId;
|
||||
}
|
||||
|
||||
if (series !== undefined) {
|
||||
return this.libraryService.search(series.name).pipe(
|
||||
map(group => group.series), map(results => {
|
||||
seriesSettings.savedData = results.filter(s => s.seriesId === series.id);
|
||||
return seriesSettings;
|
||||
}));
|
||||
}
|
||||
|
||||
return of(seriesSettings);
|
||||
}
|
||||
|
||||
saveState() {
|
||||
const adaptations = this.relations.filter(item => (parseInt(item.formControl.value, 10) as RelationKind) === RelationKind.Adaptation && item.series !== undefined).map(item => item.series!.id);
|
||||
const characters = this.relations.filter(item => (parseInt(item.formControl.value, 10) as RelationKind) === RelationKind.Character && item.series !== undefined).map(item => item.series!.id);
|
||||
const contains = this.relations.filter(item => (parseInt(item.formControl.value, 10) as RelationKind) === RelationKind.Contains && item.series !== undefined).map(item => item.series!.id);
|
||||
const others = this.relations.filter(item => (parseInt(item.formControl.value, 10) as RelationKind) === RelationKind.Other && item.series !== undefined).map(item => item.series!.id);
|
||||
const prequels = this.relations.filter(item => (parseInt(item.formControl.value, 10) as RelationKind) === RelationKind.Prequel && item.series !== undefined).map(item => item.series!.id);
|
||||
const sequels = this.relations.filter(item => (parseInt(item.formControl.value, 10) as RelationKind) === RelationKind.Sequel && item.series !== undefined).map(item => item.series!.id);
|
||||
const sideStories = this.relations.filter(item => (parseInt(item.formControl.value, 10) as RelationKind) === RelationKind.SideStory && item.series !== undefined).map(item => item.series!.id);
|
||||
const spinOffs = this.relations.filter(item => (parseInt(item.formControl.value, 10) as RelationKind) === RelationKind.SpinOff && item.series !== undefined).map(item => item.series!.id);
|
||||
const alternativeSettings = this.relations.filter(item => (parseInt(item.formControl.value, 10) as RelationKind) === RelationKind.AlternativeSetting && item.series !== undefined).map(item => item.series!.id);
|
||||
const alternativeVersions = this.relations.filter(item => (parseInt(item.formControl.value, 10) as RelationKind) === RelationKind.AlternativeVersion && item.series !== undefined).map(item => item.series!.id);
|
||||
const doujinshis = this.relations.filter(item => (parseInt(item.formControl.value, 10) as RelationKind) === RelationKind.Doujinshi && item.series !== undefined).map(item => item.series!.id);
|
||||
|
||||
// TODO: We can actually emit this onto an observable and in main parent, use mergeMap into the forkJoin
|
||||
|
||||
//this.saveApi.next(this.seriesService.updateRelationships(this.series.id, adaptations, characters, contains, others, prequels, sequels, sideStories, spinOffs, alternativeSettings, alternativeVersions, doujinshis));
|
||||
this.seriesService.updateRelationships(this.series.id, adaptations, characters, contains, others, prequels, sequels, sideStories, spinOffs, alternativeSettings, alternativeVersions, doujinshis).subscribe(() => {});
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue