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

@ -41,24 +41,36 @@ namespace API.Data
public DbSet<Genre> Genre { get; set; }
public DbSet<Tag> Tag { get; set; }
public DbSet<SiteTheme> SiteTheme { get; set; }
public DbSet<SeriesRelation> SeriesRelation { get; set; }
protected override void OnModelCreating(ModelBuilder builder)
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(builder);
base.OnModelCreating(modelBuilder);
builder.Entity<AppUser>()
modelBuilder.Entity<AppUser>()
.HasMany(ur => ur.UserRoles)
.WithOne(u => u.User)
.HasForeignKey(ur => ur.UserId)
.IsRequired();
builder.Entity<AppRole>()
modelBuilder.Entity<AppRole>()
.HasMany(ur => ur.UserRoles)
.WithOne(u => u.Role)
.HasForeignKey(ur => ur.RoleId)
.IsRequired();
modelBuilder.Entity<SeriesRelation>()
.HasOne(pt => pt.Series)
.WithMany(p => p.Relations)
.HasForeignKey(pt => pt.SeriesId)
.OnDelete(DeleteBehavior.ClientCascade);
modelBuilder.Entity<SeriesRelation>()
.HasOne(pt => pt.TargetSeries)
.WithMany(t => t.RelationOf)
.HasForeignKey(pt => pt.TargetSeriesId);
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,55 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace API.Data.Migrations
{
public partial class SeriesRelations : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "SeriesRelation",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
RelationKind = table.Column<int>(type: "INTEGER", nullable: false),
TargetSeriesId = table.Column<int>(type: "INTEGER", nullable: false),
SeriesId = table.Column<int>(type: "INTEGER", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_SeriesRelation", x => x.Id);
table.ForeignKey(
name: "FK_SeriesRelation_Series_SeriesId",
column: x => x.SeriesId,
principalTable: "Series",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
table.ForeignKey(
name: "FK_SeriesRelation_Series_TargetSeriesId",
column: x => x.TargetSeriesId,
principalTable: "Series",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_SeriesRelation_SeriesId",
table: "SeriesRelation",
column: "SeriesId");
migrationBuilder.CreateIndex(
name: "IX_SeriesRelation_TargetSeriesId",
table: "SeriesRelation",
column: "TargetSeriesId");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "SeriesRelation");
}
}
}

View file

@ -592,6 +592,30 @@ namespace API.Data.Migrations
b.ToTable("SeriesMetadata");
});
modelBuilder.Entity("API.Entities.Metadata.SeriesRelation", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("RelationKind")
.HasColumnType("INTEGER");
b.Property<int>("SeriesId")
.HasColumnType("INTEGER");
b.Property<int>("TargetSeriesId")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("SeriesId");
b.HasIndex("TargetSeriesId");
b.ToTable("SeriesRelation");
});
modelBuilder.Entity("API.Entities.Person", b =>
{
b.Property<int>("Id")
@ -1182,6 +1206,25 @@ namespace API.Data.Migrations
b.Navigation("Series");
});
modelBuilder.Entity("API.Entities.Metadata.SeriesRelation", b =>
{
b.HasOne("API.Entities.Series", "Series")
.WithMany("Relations")
.HasForeignKey("SeriesId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.HasOne("API.Entities.Series", "TargetSeries")
.WithMany("RelationOf")
.HasForeignKey("TargetSeriesId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Series");
b.Navigation("TargetSeries");
});
modelBuilder.Entity("API.Entities.ReadingList", b =>
{
b.HasOne("API.Entities.AppUser", "AppUser")
@ -1451,6 +1494,10 @@ namespace API.Data.Migrations
b.Navigation("Ratings");
b.Navigation("RelationOf");
b.Navigation("Relations");
b.Navigation("Volumes");
});

View file

@ -84,7 +84,6 @@ public class CollectionTagRepository : ICollectionTagRepository
public async Task<IEnumerable<CollectionTagDto>> GetAllTagDtosAsync()
{
return await _context.CollectionTag
.Select(c => c)
.OrderBy(c => c.NormalizedTitle)
.AsNoTracking()
.ProjectTo<CollectionTagDto>(_mapper.ConfigurationProvider)

View file

@ -11,6 +11,7 @@ using API.DTOs.Filtering;
using API.DTOs.Metadata;
using API.DTOs.ReadingLists;
using API.DTOs.Search;
using API.DTOs.SeriesDetail;
using API.Entities;
using API.Entities.Enums;
using API.Entities.Metadata;
@ -24,6 +25,17 @@ using Microsoft.EntityFrameworkCore;
namespace API.Data.Repositories;
[Flags]
public enum SeriesIncludes
{
None = 1,
Volumes = 2,
Metadata = 4,
Related = 8,
//Related = 16,
//UserPreferences = 32
}
internal class RecentlyAddedSeries
{
public int LibraryId { get; init; }
@ -68,7 +80,7 @@ public interface ISeriesRepository
Task<IEnumerable<Series>> GetSeriesForLibraryIdAsync(int libraryId);
Task<SeriesDto> GetSeriesDtoByIdAsync(int seriesId, int userId);
Task<bool> DeleteSeriesAsync(int seriesId);
Task<Series> GetSeriesByIdAsync(int seriesId);
Task<Series> GetSeriesByIdAsync(int seriesId, SeriesIncludes includes = SeriesIncludes.Volumes | SeriesIncludes.Metadata);
Task<IList<Series>> GetSeriesByIdsAsync(IList<int> seriesIds);
Task<int[]> GetChapterIdsForSeriesAsync(IList<int> seriesIds);
Task<IDictionary<int, IList<int>>> GetChapterIdWithSeriesIdForSeriesAsync(int[] seriesIds);
@ -96,6 +108,9 @@ public interface ISeriesRepository
Task<IList<LanguageDto>> GetAllLanguagesForLibrariesAsync(List<int> libraryIds);
IEnumerable<PublicationStatusDto> GetAllPublicationStatusesDtosForLibrariesAsync(List<int> libraryIds);
Task<IEnumerable<GroupedSeriesDto>> GetRecentlyUpdatedSeries(int userId, int pageSize = 30);
Task<RelatedSeriesDto> GetRelatedSeries(int userId, int seriesId);
Task<IEnumerable<SeriesDto>> GetSeriesForRelationKind(int userId, int seriesId, RelationKind kind);
}
public class SeriesRepository : ISeriesRepository
@ -376,19 +391,35 @@ public class SeriesRepository : ISeriesRepository
/// </summary>
/// <param name="seriesId"></param>
/// <returns></returns>
public async Task<Series> GetSeriesByIdAsync(int seriesId)
public async Task<Series> GetSeriesByIdAsync(int seriesId, SeriesIncludes includes = SeriesIncludes.Volumes | SeriesIncludes.Metadata)
{
return await _context.Series
.Include(s => s.Volumes)
.Include(s => s.Metadata)
.ThenInclude(m => m.CollectionTags)
.Include(s => s.Metadata)
.ThenInclude(m => m.Genres)
.Include(s => s.Metadata)
.ThenInclude(m => m.People)
var query = _context.Series
.Where(s => s.Id == seriesId)
.AsSplitQuery()
.SingleOrDefaultAsync();
.AsSplitQuery();
if (includes.HasFlag(SeriesIncludes.Volumes))
{
query = query.Include(s => s.Volumes);
}
if (includes.HasFlag(SeriesIncludes.Related))
{
query = query.Include(s => s.Relations)
.ThenInclude(r => r.TargetSeries)
.Include(s => s.RelationOf);
}
if (includes.HasFlag(SeriesIncludes.Metadata))
{
query = query.Include(s => s.Metadata)
.ThenInclude(m => m.CollectionTags)
.Include(s => s.Metadata)
.ThenInclude(m => m.Genres)
.Include(s => s.Metadata)
.ThenInclude(m => m.People);
}
return await query.SingleOrDefaultAsync();
}
/// <summary>
@ -939,6 +970,79 @@ public class SeriesRepository : ISeriesRepository
return seriesMap.Values.AsEnumerable();
}
public async Task<IEnumerable<SeriesDto>> GetSeriesForRelationKind(int userId, int seriesId, RelationKind kind)
{
var libraryIds = _context.AppUser
.Where(u => u.Id == userId)
.SelectMany(l => l.Libraries.Select(lib => lib.Id));
var usersSeriesIds = _context.Series
.Where(s => libraryIds.Contains(s.LibraryId))
.Select(s => s.Id);
var targetSeries = _context.SeriesRelation
.Where(sr =>
sr.SeriesId == seriesId && sr.RelationKind == kind && usersSeriesIds.Contains(sr.TargetSeriesId))
.Include(sr => sr.TargetSeries)
.AsSplitQuery()
.AsNoTracking()
.Select(sr => sr.TargetSeriesId);
return await _context.Series
.Where(s => targetSeries.Contains(s.Id))
.AsSplitQuery()
.AsNoTracking()
.ProjectTo<SeriesDto>(_mapper.ConfigurationProvider)
.ToListAsync();
}
public async Task<RelatedSeriesDto> GetRelatedSeries(int userId, int seriesId)
{
var libraryIds = _context.AppUser
.Where(u => u.Id == userId)
.SelectMany(l => l.Libraries.Select(lib => lib.Id));
var usersSeriesIds = _context.Series
.Where(s => libraryIds.Contains(s.LibraryId))
.Select(s => s.Id);
return new RelatedSeriesDto()
{
SourceSeriesId = seriesId,
Adaptations = await GetRelatedSeriesQuery(seriesId, usersSeriesIds, RelationKind.Adaptation),
Characters = await GetRelatedSeriesQuery(seriesId, usersSeriesIds, RelationKind.Character),
Prequels = await GetRelatedSeriesQuery(seriesId, usersSeriesIds, RelationKind.Prequel),
Sequels = await GetRelatedSeriesQuery(seriesId, usersSeriesIds, RelationKind.Sequel),
Contains = await GetRelatedSeriesQuery(seriesId, usersSeriesIds, RelationKind.Contains),
SideStories = await GetRelatedSeriesQuery(seriesId, usersSeriesIds, RelationKind.SideStory),
SpinOffs = await GetRelatedSeriesQuery(seriesId, usersSeriesIds, RelationKind.SpinOff),
Others = await GetRelatedSeriesQuery(seriesId, usersSeriesIds, RelationKind.Other),
AlternativeSettings = await GetRelatedSeriesQuery(seriesId, usersSeriesIds, RelationKind.AlternativeSetting),
AlternativeVersions = await GetRelatedSeriesQuery(seriesId, usersSeriesIds, RelationKind.AlternativeVersion),
Doujinshis = await GetRelatedSeriesQuery(seriesId, usersSeriesIds, RelationKind.Doujinshi),
Parent = await _context.Series
.SelectMany(s =>
s.RelationOf.Where(r => r.TargetSeriesId == seriesId
&& usersSeriesIds.Contains(r.TargetSeriesId)
&& r.RelationKind != RelationKind.Prequel
&& r.RelationKind != RelationKind.Sequel)
.Select(sr => sr.Series))
.AsSplitQuery()
.AsNoTracking()
.ProjectTo<SeriesDto>(_mapper.ConfigurationProvider)
.ToListAsync()
};
}
private async Task<IEnumerable<SeriesDto>> GetRelatedSeriesQuery(int seriesId, IEnumerable<int> usersSeriesIds, RelationKind kind)
{
return await _context.Series.SelectMany(s =>
s.Relations.Where(sr => sr.RelationKind == kind && sr.SeriesId == seriesId && usersSeriesIds.Contains(sr.TargetSeriesId))
.Select(sr => sr.TargetSeries))
.AsSplitQuery()
.AsNoTracking()
.ProjectTo<SeriesDto>(_mapper.ConfigurationProvider)
.ToListAsync();
}
private async Task<IEnumerable<RecentlyAddedSeries>> GetRecentlyAddedChaptersQuery(int userId, int maxRecords = 3000)
{
var libraries = await _context.AppUser