Smart Filters & Dashboard Customization (#2282)

Co-authored-by: Robbie Davis <robbie@therobbiedavis.com>
This commit is contained in:
Joe Milazzo 2023-09-12 11:24:47 -07:00 committed by GitHub
parent 3d501c9532
commit 84f85b4f24
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
92 changed files with 7149 additions and 555 deletions

View file

@ -54,6 +54,8 @@ public sealed class DataContext : IdentityDbContext<AppUser, AppRole, int,
public DbSet<ScrobbleHold> ScrobbleHold { get; set; } = null!;
public DbSet<AppUserOnDeckRemoval> AppUserOnDeckRemoval { get; set; } = null!;
public DbSet<AppUserTableOfContent> AppUserTableOfContent { get; set; } = null!;
public DbSet<AppUserSmartFilter> AppUserSmartFilter { get; set; } = null!;
public DbSet<AppUserDashboardStream> AppUserDashboardStream { get; set; } = null!;
protected override void OnModelCreating(ModelBuilder builder)
@ -119,6 +121,13 @@ public sealed class DataContext : IdentityDbContext<AppUser, AppRole, int,
builder.Entity<Chapter>()
.Property(b => b.ISBN)
.HasDefaultValue(string.Empty);
builder.Entity<AppUserDashboardStream>()
.Property(b => b.StreamType)
.HasDefaultValue(DashboardStreamType.SmartFilter);
builder.Entity<AppUserDashboardStream>()
.HasIndex(e => e.Visible)
.IsUnique(false);
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,47 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace API.Data.Migrations
{
/// <inheritdoc />
public partial class SmartFilters : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "AppUserSmartFilter",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
Name = table.Column<string>(type: "TEXT", nullable: true),
Filter = table.Column<string>(type: "TEXT", nullable: true),
AppUserId = table.Column<int>(type: "INTEGER", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_AppUserSmartFilter", x => x.Id);
table.ForeignKey(
name: "FK_AppUserSmartFilter_AspNetUsers_AppUserId",
column: x => x.AppUserId,
principalTable: "AspNetUsers",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_AppUserSmartFilter_AppUserId",
table: "AppUserSmartFilter",
column: "AppUserId");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "AppUserSmartFilter");
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,66 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace API.Data.Migrations
{
/// <inheritdoc />
public partial class DashboardStream : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "AppUserDashboardStream",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
Name = table.Column<string>(type: "TEXT", nullable: true),
IsProvided = table.Column<bool>(type: "INTEGER", nullable: false),
Order = table.Column<int>(type: "INTEGER", nullable: false),
StreamType = table.Column<int>(type: "INTEGER", nullable: false, defaultValue: 4),
Visible = table.Column<bool>(type: "INTEGER", nullable: false),
SmartFilterId = table.Column<int>(type: "INTEGER", nullable: true),
AppUserId = table.Column<int>(type: "INTEGER", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_AppUserDashboardStream", x => x.Id);
table.ForeignKey(
name: "FK_AppUserDashboardStream_AppUserSmartFilter_SmartFilterId",
column: x => x.SmartFilterId,
principalTable: "AppUserSmartFilter",
principalColumn: "Id");
table.ForeignKey(
name: "FK_AppUserDashboardStream_AspNetUsers_AppUserId",
column: x => x.AppUserId,
principalTable: "AspNetUsers",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_AppUserDashboardStream_AppUserId",
table: "AppUserDashboardStream",
column: "AppUserId");
migrationBuilder.CreateIndex(
name: "IX_AppUserDashboardStream_SmartFilterId",
table: "AppUserDashboardStream",
column: "SmartFilterId");
migrationBuilder.CreateIndex(
name: "IX_AppUserDashboardStream_Visible",
table: "AppUserDashboardStream",
column: "Visible");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "AppUserDashboardStream");
}
}
}

View file

@ -180,7 +180,47 @@ namespace API.Data.Migrations
b.HasIndex("AppUserId");
b.ToTable("AppUserBookmark", (string)null);
b.ToTable("AppUserBookmark");
});
modelBuilder.Entity("API.Entities.AppUserDashboardStream", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("AppUserId")
.HasColumnType("INTEGER");
b.Property<bool>("IsProvided")
.HasColumnType("INTEGER");
b.Property<string>("Name")
.HasColumnType("TEXT");
b.Property<int>("Order")
.HasColumnType("INTEGER");
b.Property<int?>("SmartFilterId")
.HasColumnType("INTEGER");
b.Property<int>("StreamType")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(4);
b.Property<bool>("Visible")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("AppUserId");
b.HasIndex("SmartFilterId");
b.HasIndex("Visible");
b.ToTable("AppUserDashboardStream");
});
modelBuilder.Entity("API.Entities.AppUserOnDeckRemoval", b =>
@ -201,7 +241,7 @@ namespace API.Data.Migrations
b.HasIndex("SeriesId");
b.ToTable("AppUserOnDeckRemoval", (string)null);
b.ToTable("AppUserOnDeckRemoval");
});
modelBuilder.Entity("API.Entities.AppUserPreferences", b =>
@ -315,7 +355,7 @@ namespace API.Data.Migrations
b.HasIndex("ThemeId");
b.ToTable("AppUserPreferences", (string)null);
b.ToTable("AppUserPreferences");
});
modelBuilder.Entity("API.Entities.AppUserProgress", b =>
@ -365,7 +405,7 @@ namespace API.Data.Migrations
b.HasIndex("SeriesId");
b.ToTable("AppUserProgresses", (string)null);
b.ToTable("AppUserProgresses");
});
modelBuilder.Entity("API.Entities.AppUserRating", b =>
@ -398,7 +438,7 @@ namespace API.Data.Migrations
b.HasIndex("SeriesId");
b.ToTable("AppUserRating", (string)null);
b.ToTable("AppUserRating");
});
modelBuilder.Entity("API.Entities.AppUserRole", b =>
@ -416,6 +456,28 @@ namespace API.Data.Migrations
b.ToTable("AspNetUserRoles", (string)null);
});
modelBuilder.Entity("API.Entities.AppUserSmartFilter", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("AppUserId")
.HasColumnType("INTEGER");
b.Property<string>("Filter")
.HasColumnType("TEXT");
b.Property<string>("Name")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("AppUserId");
b.ToTable("AppUserSmartFilter");
});
modelBuilder.Entity("API.Entities.AppUserTableOfContent", b =>
{
b.Property<int>("Id")
@ -466,7 +528,7 @@ namespace API.Data.Migrations
b.HasIndex("SeriesId");
b.ToTable("AppUserTableOfContent", (string)null);
b.ToTable("AppUserTableOfContent");
});
modelBuilder.Entity("API.Entities.Chapter", b =>
@ -576,7 +638,7 @@ namespace API.Data.Migrations
b.HasIndex("VolumeId");
b.ToTable("Chapter", (string)null);
b.ToTable("Chapter");
});
modelBuilder.Entity("API.Entities.CollectionTag", b =>
@ -611,7 +673,7 @@ namespace API.Data.Migrations
b.HasIndex("Id", "Promoted")
.IsUnique();
b.ToTable("CollectionTag", (string)null);
b.ToTable("CollectionTag");
});
modelBuilder.Entity("API.Entities.Device", b =>
@ -657,7 +719,7 @@ namespace API.Data.Migrations
b.HasIndex("AppUserId");
b.ToTable("Device", (string)null);
b.ToTable("Device");
});
modelBuilder.Entity("API.Entities.FolderPath", b =>
@ -679,7 +741,7 @@ namespace API.Data.Migrations
b.HasIndex("LibraryId");
b.ToTable("FolderPath", (string)null);
b.ToTable("FolderPath");
});
modelBuilder.Entity("API.Entities.Genre", b =>
@ -699,7 +761,7 @@ namespace API.Data.Migrations
b.HasIndex("NormalizedTitle")
.IsUnique();
b.ToTable("Genre", (string)null);
b.ToTable("Genre");
});
modelBuilder.Entity("API.Entities.Library", b =>
@ -757,7 +819,7 @@ namespace API.Data.Migrations
b.HasKey("Id");
b.ToTable("Library", (string)null);
b.ToTable("Library");
});
modelBuilder.Entity("API.Entities.MangaFile", b =>
@ -806,7 +868,7 @@ namespace API.Data.Migrations
b.HasIndex("ChapterId");
b.ToTable("MangaFile", (string)null);
b.ToTable("MangaFile");
});
modelBuilder.Entity("API.Entities.MediaError", b =>
@ -841,7 +903,7 @@ namespace API.Data.Migrations
b.HasKey("Id");
b.ToTable("MediaError", (string)null);
b.ToTable("MediaError");
});
modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b =>
@ -942,7 +1004,7 @@ namespace API.Data.Migrations
b.HasIndex("Id", "SeriesId")
.IsUnique();
b.ToTable("SeriesMetadata", (string)null);
b.ToTable("SeriesMetadata");
});
modelBuilder.Entity("API.Entities.Metadata.SeriesRelation", b =>
@ -966,7 +1028,7 @@ namespace API.Data.Migrations
b.HasIndex("TargetSeriesId");
b.ToTable("SeriesRelation", (string)null);
b.ToTable("SeriesRelation");
});
modelBuilder.Entity("API.Entities.Person", b =>
@ -986,7 +1048,7 @@ namespace API.Data.Migrations
b.HasKey("Id");
b.ToTable("Person", (string)null);
b.ToTable("Person");
});
modelBuilder.Entity("API.Entities.ReadingList", b =>
@ -1049,7 +1111,7 @@ namespace API.Data.Migrations
b.HasIndex("AppUserId");
b.ToTable("ReadingList", (string)null);
b.ToTable("ReadingList");
});
modelBuilder.Entity("API.Entities.ReadingListItem", b =>
@ -1083,7 +1145,7 @@ namespace API.Data.Migrations
b.HasIndex("VolumeId");
b.ToTable("ReadingListItem", (string)null);
b.ToTable("ReadingListItem");
});
modelBuilder.Entity("API.Entities.Scrobble.ScrobbleError", b =>
@ -1128,7 +1190,7 @@ namespace API.Data.Migrations
b.HasIndex("SeriesId");
b.ToTable("ScrobbleError", (string)null);
b.ToTable("ScrobbleError");
});
modelBuilder.Entity("API.Entities.Scrobble.ScrobbleEvent", b =>
@ -1188,8 +1250,8 @@ namespace API.Data.Migrations
b.Property<int>("SeriesId")
.HasColumnType("INTEGER");
b.Property<float?>("VolumeNumber")
.HasColumnType("REAL");
b.Property<int?>("VolumeNumber")
.HasColumnType("INTEGER");
b.HasKey("Id");
@ -1199,7 +1261,7 @@ namespace API.Data.Migrations
b.HasIndex("SeriesId");
b.ToTable("ScrobbleEvent", (string)null);
b.ToTable("ScrobbleEvent");
});
modelBuilder.Entity("API.Entities.Scrobble.ScrobbleHold", b =>
@ -1232,7 +1294,7 @@ namespace API.Data.Migrations
b.HasIndex("SeriesId");
b.ToTable("ScrobbleHold", (string)null);
b.ToTable("ScrobbleHold");
});
modelBuilder.Entity("API.Entities.Series", b =>
@ -1328,7 +1390,7 @@ namespace API.Data.Migrations
b.HasIndex("LibraryId");
b.ToTable("Series", (string)null);
b.ToTable("Series");
});
modelBuilder.Entity("API.Entities.ServerSetting", b =>
@ -1345,7 +1407,7 @@ namespace API.Data.Migrations
b.HasKey("Key");
b.ToTable("ServerSetting", (string)null);
b.ToTable("ServerSetting");
});
modelBuilder.Entity("API.Entities.ServerStatistics", b =>
@ -1383,7 +1445,7 @@ namespace API.Data.Migrations
b.HasKey("Id");
b.ToTable("ServerStatistics", (string)null);
b.ToTable("ServerStatistics");
});
modelBuilder.Entity("API.Entities.SiteTheme", b =>
@ -1421,7 +1483,7 @@ namespace API.Data.Migrations
b.HasKey("Id");
b.ToTable("SiteTheme", (string)null);
b.ToTable("SiteTheme");
});
modelBuilder.Entity("API.Entities.Tag", b =>
@ -1441,7 +1503,7 @@ namespace API.Data.Migrations
b.HasIndex("NormalizedTitle")
.IsUnique();
b.ToTable("Tag", (string)null);
b.ToTable("Tag");
});
modelBuilder.Entity("API.Entities.Volume", b =>
@ -1477,8 +1539,8 @@ namespace API.Data.Migrations
b.Property<string>("Name")
.HasColumnType("TEXT");
b.Property<float>("Number")
.HasColumnType("REAL");
b.Property<int>("Number")
.HasColumnType("INTEGER");
b.Property<int>("Pages")
.HasColumnType("INTEGER");
@ -1493,7 +1555,7 @@ namespace API.Data.Migrations
b.HasIndex("SeriesId");
b.ToTable("Volume", (string)null);
b.ToTable("Volume");
});
modelBuilder.Entity("AppUserLibrary", b =>
@ -1508,7 +1570,7 @@ namespace API.Data.Migrations
b.HasIndex("LibrariesId");
b.ToTable("AppUserLibrary", (string)null);
b.ToTable("AppUserLibrary");
});
modelBuilder.Entity("ChapterGenre", b =>
@ -1523,7 +1585,7 @@ namespace API.Data.Migrations
b.HasIndex("GenresId");
b.ToTable("ChapterGenre", (string)null);
b.ToTable("ChapterGenre");
});
modelBuilder.Entity("ChapterPerson", b =>
@ -1538,7 +1600,7 @@ namespace API.Data.Migrations
b.HasIndex("PeopleId");
b.ToTable("ChapterPerson", (string)null);
b.ToTable("ChapterPerson");
});
modelBuilder.Entity("ChapterTag", b =>
@ -1553,7 +1615,7 @@ namespace API.Data.Migrations
b.HasIndex("TagsId");
b.ToTable("ChapterTag", (string)null);
b.ToTable("ChapterTag");
});
modelBuilder.Entity("CollectionTagSeriesMetadata", b =>
@ -1568,7 +1630,7 @@ namespace API.Data.Migrations
b.HasIndex("SeriesMetadatasId");
b.ToTable("CollectionTagSeriesMetadata", (string)null);
b.ToTable("CollectionTagSeriesMetadata");
});
modelBuilder.Entity("GenreSeriesMetadata", b =>
@ -1583,7 +1645,7 @@ namespace API.Data.Migrations
b.HasIndex("SeriesMetadatasId");
b.ToTable("GenreSeriesMetadata", (string)null);
b.ToTable("GenreSeriesMetadata");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<int>", b =>
@ -1682,7 +1744,7 @@ namespace API.Data.Migrations
b.HasIndex("SeriesMetadatasId");
b.ToTable("PersonSeriesMetadata", (string)null);
b.ToTable("PersonSeriesMetadata");
});
modelBuilder.Entity("SeriesMetadataTag", b =>
@ -1697,7 +1759,7 @@ namespace API.Data.Migrations
b.HasIndex("TagsId");
b.ToTable("SeriesMetadataTag", (string)null);
b.ToTable("SeriesMetadataTag");
});
modelBuilder.Entity("API.Entities.AppUserBookmark", b =>
@ -1711,6 +1773,23 @@ namespace API.Data.Migrations
b.Navigation("AppUser");
});
modelBuilder.Entity("API.Entities.AppUserDashboardStream", b =>
{
b.HasOne("API.Entities.AppUser", "AppUser")
.WithMany("DashboardStreams")
.HasForeignKey("AppUserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("API.Entities.AppUserSmartFilter", "SmartFilter")
.WithMany()
.HasForeignKey("SmartFilterId");
b.Navigation("AppUser");
b.Navigation("SmartFilter");
});
modelBuilder.Entity("API.Entities.AppUserOnDeckRemoval", b =>
{
b.HasOne("API.Entities.AppUser", "AppUser")
@ -1808,6 +1887,17 @@ namespace API.Data.Migrations
b.Navigation("User");
});
modelBuilder.Entity("API.Entities.AppUserSmartFilter", b =>
{
b.HasOne("API.Entities.AppUser", "AppUser")
.WithMany("SmartFilters")
.HasForeignKey("AppUserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("AppUser");
});
modelBuilder.Entity("API.Entities.AppUserTableOfContent", b =>
{
b.HasOne("API.Entities.AppUser", "AppUser")
@ -2209,6 +2299,8 @@ namespace API.Data.Migrations
{
b.Navigation("Bookmarks");
b.Navigation("DashboardStreams");
b.Navigation("Devices");
b.Navigation("Progresses");
@ -2219,6 +2311,8 @@ namespace API.Data.Migrations
b.Navigation("ScrobbleHolds");
b.Navigation("SmartFilters");
b.Navigation("TableOfContents");
b.Navigation("UserPreferences");

View file

@ -0,0 +1,60 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using API.DTOs.Dashboard;
using API.Entities;
using AutoMapper;
using AutoMapper.QueryableExtensions;
using Microsoft.EntityFrameworkCore;
namespace API.Data.Repositories;
public interface IAppUserSmartFilterRepository
{
void Update(AppUserSmartFilter filter);
void Attach(AppUserSmartFilter filter);
void Delete(AppUserSmartFilter filter);
IEnumerable<SmartFilterDto> GetAllDtosByUserId(int userId);
Task<AppUserSmartFilter?> GetById(int smartFilterId);
}
public class AppUserSmartFilterRepository : IAppUserSmartFilterRepository
{
private readonly DataContext _context;
private readonly IMapper _mapper;
public AppUserSmartFilterRepository(DataContext context, IMapper mapper)
{
_context = context;
_mapper = mapper;
}
public void Update(AppUserSmartFilter filter)
{
_context.Entry(filter).State = EntityState.Modified;
}
public void Attach(AppUserSmartFilter filter)
{
_context.AppUserSmartFilter.Attach(filter);
}
public void Delete(AppUserSmartFilter filter)
{
_context.AppUserSmartFilter.Remove(filter);
}
public IEnumerable<SmartFilterDto> GetAllDtosByUserId(int userId)
{
return _context.AppUserSmartFilter
.Where(f => f.AppUserId == userId)
.ProjectTo<SmartFilterDto>(_mapper.ConfigurationProvider)
.AsEnumerable();
}
public async Task<AppUserSmartFilter?> GetById(int smartFilterId)
{
return await _context.AppUserSmartFilter.FirstOrDefaultAsync(d => d.Id == smartFilterId);
}
}

View file

@ -8,6 +8,7 @@ using API.Data.Misc;
using API.Data.Scanner;
using API.DTOs;
using API.DTOs.CollectionTags;
using API.DTOs.Dashboard;
using API.DTOs.Filtering;
using API.DTOs.Filtering.v2;
using API.DTOs.Metadata;
@ -952,6 +953,9 @@ public class SeriesRepository : ISeriesRepository
// First setup any FilterField.Libraries in the statements, as these don't have any traditional query statements applied here
query = ApplyLibraryFilter(filter, query);
query = ApplyWantToReadFilter(filter, query, userId);
query = BuildFilterQuery(userId, filter, query);
@ -968,6 +972,24 @@ public class SeriesRepository : ISeriesRepository
.AsSplitQuery(), filter.LimitTo);
}
private IQueryable<Series> ApplyWantToReadFilter(FilterV2Dto filter, IQueryable<Series> query, int userId)
{
var wantToReadStmt = filter.Statements.FirstOrDefault(stmt => stmt.Field == FilterField.WantToRead);
if (wantToReadStmt == null) return query;
var seriesIds = _context.AppUser.Where(u => u.Id == userId).SelectMany(u => u.WantToRead).Select(s => s.Id);
if (bool.Parse(wantToReadStmt.Value))
{
query = query.Where(s => seriesIds.Contains(s.Id));
}
else
{
query = query.Where(s => !seriesIds.Contains(s.Id));
}
return query;
}
private static IQueryable<Series> ApplyLibraryFilter(FilterV2Dto filter, IQueryable<Series> query)
{
var filterIncludeLibs = new List<int>();
@ -1060,6 +1082,9 @@ public class SeriesRepository : ISeriesRepository
FilterField.Libraries =>
// This is handled in the code before this as it's handled in a more general, combined manner
query,
FilterField.WantToRead =>
// This is handled in the higher level of code as it's more general
query,
FilterField.ReadProgress => query.HasReadingProgress(true, statement.Comparison, (int) value, userId),
FilterField.Formats => query.HasFormat(true, statement.Comparison, (IList<MangaFormat>) value),
FilterField.ReleaseYear => query.HasReleaseYear(true, statement.Comparison, (int) value),

View file

@ -6,7 +6,7 @@ using System.Threading.Tasks;
using API.Constants;
using API.DTOs;
using API.DTOs.Account;
using API.DTOs.Filtering;
using API.DTOs.Dashboard;
using API.DTOs.Filtering.v2;
using API.DTOs.Reader;
using API.DTOs.Scrobbling;
@ -15,6 +15,7 @@ using API.Entities;
using API.Extensions;
using API.Extensions.QueryExtensions;
using API.Extensions.QueryExtensions.Filtering;
using API.Helpers;
using AutoMapper;
using AutoMapper.QueryableExtensions;
using Microsoft.AspNetCore.Identity;
@ -34,8 +35,9 @@ public enum AppUserIncludes
WantToRead = 64,
ReadingListsWithItems = 128,
Devices = 256,
ScrobbleHolds = 512
ScrobbleHolds = 512,
SmartFilters = 1024,
DashboardStreams = 2048
}
public interface IUserRepository
@ -43,9 +45,11 @@ public interface IUserRepository
void Update(AppUser user);
void Update(AppUserPreferences preferences);
void Update(AppUserBookmark bookmark);
void Update(AppUserDashboardStream stream);
void Add(AppUserBookmark bookmark);
public void Delete(AppUser? user);
void Delete(AppUser? user);
void Delete(AppUserBookmark bookmark);
void Delete(IList<AppUserDashboardStream> streams);
Task<IEnumerable<MemberDto>> GetEmailConfirmedMemberDtosAsync(bool emailConfirmed = true);
Task<IEnumerable<AppUser>> GetAdminUsersAsync();
Task<bool> IsUserAdminAsync(AppUser? user);
@ -76,6 +80,9 @@ public interface IUserRepository
Task<bool> HasHoldOnSeries(int userId, int seriesId);
Task<IList<ScrobbleHoldDto>> GetHolds(int userId);
Task<string> GetLocale(int userId);
Task<IList<DashboardStreamDto>> GetDashboardStreams(int userId, bool visibleOnly = false);
Task<AppUserDashboardStream?> GetDashboardStream(int streamId);
Task<IList<AppUserDashboardStream>> GetDashboardStreamWithFilter(int filterId);
}
public class UserRepository : IUserRepository
@ -106,6 +113,11 @@ public class UserRepository : IUserRepository
_context.Entry(bookmark).State = EntityState.Modified;
}
public void Update(AppUserDashboardStream stream)
{
_context.Entry(stream).State = EntityState.Modified;
}
public void Add(AppUserBookmark bookmark)
{
_context.AppUserBookmark.Add(bookmark);
@ -122,6 +134,11 @@ public class UserRepository : IUserRepository
_context.AppUserBookmark.Remove(bookmark);
}
public void Delete(IList<AppUserDashboardStream> streams)
{
_context.AppUserDashboardStream.RemoveRange(streams);
}
/// <summary>
/// A one stop shop to get a tracked AppUser instance with any number of JOINs generated by passing bitwise flags.
/// </summary>
@ -300,6 +317,42 @@ public class UserRepository : IUserRepository
.SingleAsync();
}
public async Task<IList<DashboardStreamDto>> GetDashboardStreams(int userId, bool visibleOnly = false)
{
return await _context.AppUserDashboardStream
.Where(d => d.AppUserId == userId)
.WhereIf(visibleOnly, d => d.Visible)
.OrderBy(d => d.Order)
.Include(d => d.SmartFilter)
.Select(d => new DashboardStreamDto()
{
Id = d.Id,
Name = d.Name,
IsProvided = d.IsProvided,
SmartFilterId = d.SmartFilter == null ? 0 : d.SmartFilter.Id,
SmartFilterEncoded = d.SmartFilter == null ? null : d.SmartFilter.Filter,
StreamType = d.StreamType,
Order = d.Order,
Visible = d.Visible
})
.ToListAsync();
}
public async Task<AppUserDashboardStream?> GetDashboardStream(int streamId)
{
return await _context.AppUserDashboardStream
.Include(d => d.SmartFilter)
.FirstOrDefaultAsync(d => d.Id == streamId);
}
public async Task<IList<AppUserDashboardStream>> GetDashboardStreamWithFilter(int filterId)
{
return await _context.AppUserDashboardStream
.Include(d => d.SmartFilter)
.Where(d => d.SmartFilter != null && d.SmartFilter.Id == filterId)
.ToListAsync();
}
public async Task<IEnumerable<AppUser>> GetAdminUsersAsync()
{
return await _userManager.GetUsersInRoleAsync(PolicyConstants.AdminRole);

View file

@ -6,6 +6,7 @@ using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using API.Constants;
using API.Data.Repositories;
using API.Entities;
using API.Entities.Enums;
using API.Entities.Enums.Theme;
@ -38,6 +39,43 @@ public static class Seed
}
}.ToArray());
public static readonly ImmutableArray<AppUserDashboardStream> DefaultStreams = ImmutableArray.Create(
new List<AppUserDashboardStream>
{
new()
{
Name = "On Deck",
StreamType = DashboardStreamType.OnDeck,
Order = 0,
IsProvided = true,
Visible = true
},
new()
{
Name = "Recently Updated",
StreamType = DashboardStreamType.RecentlyUpdated,
Order = 1,
IsProvided = true,
Visible = true
},
new()
{
Name = "Newly Added",
StreamType = DashboardStreamType.NewlyAdded,
Order = 2,
IsProvided = true,
Visible = true
},
new()
{
Name = "More In",
StreamType = DashboardStreamType.MoreInGenre,
Order = 3,
IsProvided = true,
Visible = false
},
}.ToArray());
public static async Task SeedRoles(RoleManager<AppRole> roleManager)
{
var roles = typeof(PolicyConstants)
@ -74,6 +112,31 @@ public static class Seed
await context.SaveChangesAsync();
}
public static async Task SeedDefaultStreams(IUnitOfWork unitOfWork)
{
var allUsers = await unitOfWork.UserRepository.GetAllUsersAsync(AppUserIncludes.DashboardStreams);
foreach (var user in allUsers)
{
if (user.DashboardStreams.Count != 0) continue;
user.DashboardStreams ??= new List<AppUserDashboardStream>();
foreach (var defaultStream in DefaultStreams)
{
var newStream = new AppUserDashboardStream
{
Name = defaultStream.Name,
IsProvided = defaultStream.IsProvided,
Order = defaultStream.Order,
StreamType = defaultStream.StreamType,
Visible = defaultStream.Visible,
};
user.DashboardStreams.Add(newStream);
}
unitOfWork.UserRepository.Update(user);
await unitOfWork.CommitAsync();
}
}
public static async Task SeedSettings(DataContext context, IDirectoryService directoryService)
{
await context.Database.EnsureCreatedAsync();

View file

@ -28,6 +28,7 @@ public interface IUnitOfWork
IMediaErrorRepository MediaErrorRepository { get; }
IScrobbleRepository ScrobbleRepository { get; }
IUserTableOfContentRepository UserTableOfContentRepository { get; }
IAppUserSmartFilterRepository AppUserSmartFilterRepository { get; }
bool Commit();
Task<bool> CommitAsync();
bool HasChanges();
@ -68,6 +69,7 @@ public class UnitOfWork : IUnitOfWork
public IMediaErrorRepository MediaErrorRepository => new MediaErrorRepository(_context, _mapper);
public IScrobbleRepository ScrobbleRepository => new ScrobbleRepository(_context, _mapper);
public IUserTableOfContentRepository UserTableOfContentRepository => new UserTableOfContentRepository(_context, _mapper);
public IAppUserSmartFilterRepository AppUserSmartFilterRepository => new AppUserSmartFilterRepository(_context, _mapper);
/// <summary>
/// Commits changes to the DB. Completes the open transaction.