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:
Joseph Milazzo 2022-04-24 11:59:09 -05:00 committed by GitHub
parent 7253765f1d
commit 4206ae3e22
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
47 changed files with 2571 additions and 195 deletions

View file

@ -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>

View file

@ -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(() => {});
}
}