Side Nav Redesign (#2310)
This commit is contained in:
parent
5c2ebb87cc
commit
00dddaefae
88 changed files with 5971 additions and 572 deletions
|
|
@ -8,7 +8,6 @@ using API.Data;
|
|||
using API.Data.Repositories;
|
||||
using API.DTOs;
|
||||
using API.DTOs.Account;
|
||||
using API.DTOs.Dashboard;
|
||||
using API.DTOs.Email;
|
||||
using API.Entities;
|
||||
using API.Entities.Enums;
|
||||
|
|
@ -1046,123 +1045,4 @@ public class AccountController : BaseApiController
|
|||
return Ok(origin + "/" + baseUrl + "api/opds/" + user!.ApiKey);
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the layout of the user's dashboard
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[HttpGet("dashboard")]
|
||||
public async Task<ActionResult<IEnumerable<DashboardStreamDto>>> GetDashboardLayout(bool visibleOnly = true)
|
||||
{
|
||||
var streams = await _unitOfWork.UserRepository.GetDashboardStreams(User.GetUserId(), visibleOnly);
|
||||
return Ok(streams);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a Dashboard Stream from a SmartFilter and adds it to the user's dashboard as visible
|
||||
/// </summary>
|
||||
/// <param name="smartFilterId"></param>
|
||||
/// <returns></returns>
|
||||
[HttpPost("add-dashboard-stream")]
|
||||
public async Task<ActionResult<DashboardStreamDto>> AddDashboard([FromQuery] int smartFilterId)
|
||||
{
|
||||
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(User.GetUserId(), AppUserIncludes.DashboardStreams);
|
||||
if (user == null) return Unauthorized();
|
||||
|
||||
var smartFilter = await _unitOfWork.AppUserSmartFilterRepository.GetById(smartFilterId);
|
||||
if (smartFilter == null) return NoContent();
|
||||
|
||||
var stream = user?.DashboardStreams.FirstOrDefault(d => d.SmartFilter?.Id == smartFilterId);
|
||||
if (stream != null) return BadRequest("There is an existing stream with this Filter");
|
||||
|
||||
var maxOrder = user!.DashboardStreams.Max(d => d.Order);
|
||||
var createdStream = new AppUserDashboardStream()
|
||||
{
|
||||
Name = smartFilter.Name,
|
||||
IsProvided = false,
|
||||
StreamType = DashboardStreamType.SmartFilter,
|
||||
Visible = true,
|
||||
Order = maxOrder + 1,
|
||||
SmartFilter = smartFilter
|
||||
};
|
||||
|
||||
user.DashboardStreams.Add(createdStream);
|
||||
_unitOfWork.UserRepository.Update(user);
|
||||
await _unitOfWork.CommitAsync();
|
||||
|
||||
var ret = new DashboardStreamDto()
|
||||
{
|
||||
Name = createdStream.Name,
|
||||
IsProvided = createdStream.IsProvided,
|
||||
Visible = createdStream.Visible,
|
||||
Order = createdStream.Order,
|
||||
SmartFilterEncoded = smartFilter.Filter,
|
||||
StreamType = createdStream.StreamType
|
||||
};
|
||||
|
||||
|
||||
await _eventHub.SendMessageToAsync(MessageFactory.DashboardUpdate, MessageFactory.DashboardUpdateEvent(user.Id),
|
||||
User.GetUserId());
|
||||
return Ok(ret);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the visibility of a dashboard stream
|
||||
/// </summary>
|
||||
/// <param name="dto"></param>
|
||||
/// <returns></returns>
|
||||
[HttpPost("update-dashboard-stream")]
|
||||
public async Task<ActionResult> UpdateDashboardStream(DashboardStreamDto dto)
|
||||
{
|
||||
var stream = await _unitOfWork.UserRepository.GetDashboardStream(dto.Id);
|
||||
if (stream == null) return BadRequest();
|
||||
stream.Visible = dto.Visible;
|
||||
|
||||
_unitOfWork.UserRepository.Update(stream);
|
||||
await _unitOfWork.CommitAsync();
|
||||
var userId = User.GetUserId();
|
||||
await _eventHub.SendMessageToAsync(MessageFactory.DashboardUpdate, MessageFactory.DashboardUpdateEvent(userId),
|
||||
userId);
|
||||
return Ok();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the position of a dashboard stream
|
||||
/// </summary>
|
||||
/// <param name="dto"></param>
|
||||
/// <returns></returns>
|
||||
[HttpPost("update-dashboard-position")]
|
||||
public async Task<ActionResult> UpdateDashboardStreamPosition(UpdateDashboardStreamPositionDto dto)
|
||||
{
|
||||
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(User.GetUserId(),
|
||||
AppUserIncludes.DashboardStreams);
|
||||
var stream = user?.DashboardStreams.FirstOrDefault(d => d.Id == dto.DashboardStreamId);
|
||||
if (stream == null) return BadRequest();
|
||||
if (stream.Order == dto.ToPosition) return Ok();
|
||||
|
||||
var list = user!.DashboardStreams.ToList();
|
||||
ReorderItems(list, stream.Id, dto.ToPosition);
|
||||
user.DashboardStreams = list;
|
||||
|
||||
_unitOfWork.UserRepository.Update(user);
|
||||
await _unitOfWork.CommitAsync();
|
||||
await _eventHub.SendMessageToAsync(MessageFactory.DashboardUpdate, MessageFactory.DashboardUpdateEvent(user.Id),
|
||||
user.Id);
|
||||
return Ok();
|
||||
}
|
||||
|
||||
private static void ReorderItems(List<AppUserDashboardStream> items, int itemId, int toPosition)
|
||||
{
|
||||
var item = items.Find(r => r.Id == itemId);
|
||||
if (item != null)
|
||||
{
|
||||
items.Remove(item);
|
||||
items.Insert(toPosition, item);
|
||||
}
|
||||
|
||||
for (var i = 0; i < items.Count; i++)
|
||||
{
|
||||
items[i].Order = i;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -87,6 +87,10 @@ public class FilterController : BaseApiController
|
|||
// This needs to delete any dashboard filters that have it too
|
||||
var streams = await _unitOfWork.UserRepository.GetDashboardStreamWithFilter(filter.Id);
|
||||
_unitOfWork.UserRepository.Delete(streams);
|
||||
|
||||
var streams2 = await _unitOfWork.UserRepository.GetSideNavStreamWithFilter(filter.Id);
|
||||
_unitOfWork.UserRepository.Delete(streams2);
|
||||
|
||||
_unitOfWork.AppUserSmartFilterRepository.Delete(filter);
|
||||
await _unitOfWork.CommitAsync();
|
||||
return Ok();
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ using AutoMapper;
|
|||
using EasyCaching.Core;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Configuration.UserSecrets;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using TaskScheduler = API.Services.TaskScheduler;
|
||||
|
||||
|
|
@ -97,6 +98,27 @@ public class LibraryController : BaseApiController
|
|||
admin.Libraries.Add(library);
|
||||
}
|
||||
|
||||
var userIds = admins.Select(u => u.Id).Append(User.GetUserId()).ToList();
|
||||
|
||||
var userNeedingNewLibrary = (await _unitOfWork.UserRepository.GetAllUsersAsync(AppUserIncludes.SideNavStreams))
|
||||
.Where(u => userIds.Contains(u.Id))
|
||||
.ToList();
|
||||
|
||||
foreach (var user in userNeedingNewLibrary)
|
||||
{
|
||||
var maxCount = user.SideNavStreams.Select(s => s.Order).Max();
|
||||
user.SideNavStreams.Add(new AppUserSideNavStream()
|
||||
{
|
||||
Name = library.Name,
|
||||
Order = maxCount + 1,
|
||||
IsProvided = false,
|
||||
StreamType = SideNavStreamType.Library,
|
||||
LibraryId = library.Id,
|
||||
Visible = true,
|
||||
});
|
||||
_unitOfWork.UserRepository.Update(user);
|
||||
}
|
||||
|
||||
|
||||
if (!await _unitOfWork.CommitAsync()) return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-library"));
|
||||
|
||||
|
|
@ -105,6 +127,8 @@ public class LibraryController : BaseApiController
|
|||
_taskScheduler.ScanLibrary(library.Id);
|
||||
await _eventHub.SendMessageAsync(MessageFactory.LibraryModified,
|
||||
MessageFactory.LibraryModifiedEvent(library.Id, "create"), false);
|
||||
await _eventHub.SendMessageAsync(MessageFactory.SideNavUpdate,
|
||||
MessageFactory.SideNavUpdateEvent(User.GetUserId()), false);
|
||||
await _libraryCacheProvider.RemoveByPrefixAsync(CacheKey);
|
||||
return Ok();
|
||||
}
|
||||
|
|
@ -329,9 +353,15 @@ public class LibraryController : BaseApiController
|
|||
|
||||
_unitOfWork.LibraryRepository.Delete(library);
|
||||
|
||||
var streams = await _unitOfWork.UserRepository.GetSideNavStreamsByLibraryId(library.Id);
|
||||
_unitOfWork.UserRepository.Delete(streams);
|
||||
|
||||
|
||||
await _unitOfWork.CommitAsync();
|
||||
|
||||
await _libraryCacheProvider.RemoveByPrefixAsync(CacheKey);
|
||||
await _eventHub.SendMessageAsync(MessageFactory.SideNavUpdate,
|
||||
MessageFactory.SideNavUpdateEvent(User.GetUserId()), false);
|
||||
|
||||
if (chapterIds.Any())
|
||||
{
|
||||
|
|
|
|||
|
|
@ -50,4 +50,18 @@ public class PluginController : BaseApiController
|
|||
KavitaVersion = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.InstallVersion)).Value
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the version of the Kavita install
|
||||
/// </summary>
|
||||
/// <param name="apiKey">Required for authenticating to get result</param>
|
||||
/// <returns></returns>
|
||||
[AllowAnonymous]
|
||||
[HttpGet("version")]
|
||||
public async Task<ActionResult<string>> GetVersion([Required] string apiKey)
|
||||
{
|
||||
var userId = await _unitOfWork.UserRepository.GetUserIdByApiKeyAsync(apiKey);
|
||||
if (userId <= 0) return Unauthorized();
|
||||
return Ok((await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.InstallVersion)).Value);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
186
API/Controllers/StreamController.cs
Normal file
186
API/Controllers/StreamController.cs
Normal file
|
|
@ -0,0 +1,186 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using API.Data;
|
||||
using API.DTOs.Dashboard;
|
||||
using API.DTOs.SideNav;
|
||||
using API.Extensions;
|
||||
using API.Services;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace API.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// Responsible for anything that deals with Streams (SmartFilters, ExternalSource, DashboardStream, SideNavStream)
|
||||
/// </summary>
|
||||
public class StreamController : BaseApiController
|
||||
{
|
||||
private readonly IStreamService _streamService;
|
||||
private readonly IUnitOfWork _unitOfWork;
|
||||
private readonly ILogger<StreamController> _logger;
|
||||
|
||||
public StreamController(IStreamService streamService, IUnitOfWork unitOfWork, ILogger<StreamController> logger)
|
||||
{
|
||||
_streamService = streamService;
|
||||
_unitOfWork = unitOfWork;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the layout of the user's dashboard
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[HttpGet("dashboard")]
|
||||
public async Task<ActionResult<IEnumerable<DashboardStreamDto>>> GetDashboardLayout(bool visibleOnly = true)
|
||||
{
|
||||
return Ok(await _streamService.GetDashboardStreams(User.GetUserId(), visibleOnly));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Return's the user's side nav
|
||||
/// </summary>
|
||||
[HttpGet("sidenav")]
|
||||
public async Task<ActionResult<IEnumerable<SideNavStreamDto>>> GetSideNav(bool visibleOnly = true)
|
||||
{
|
||||
return Ok(await _streamService.GetSidenavStreams(User.GetUserId(), visibleOnly));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Return's the user's external sources
|
||||
/// </summary>
|
||||
[HttpGet("external-sources")]
|
||||
public async Task<ActionResult<IEnumerable<ExternalSourceDto>>> GetExternalSources()
|
||||
{
|
||||
return Ok(await _streamService.GetExternalSources(User.GetUserId()));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create an external Source
|
||||
/// </summary>
|
||||
/// <param name="dto"></param>
|
||||
/// <returns></returns>
|
||||
[HttpPost("create-external-source")]
|
||||
public async Task<ActionResult<ExternalSourceDto>> CreateExternalSource(ExternalSourceDto dto)
|
||||
{
|
||||
// Check if a host and api key exists for the current user
|
||||
return Ok(await _streamService.CreateExternalSource(User.GetUserId(), dto));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates an existing external source
|
||||
/// </summary>
|
||||
/// <param name="dto"></param>
|
||||
/// <returns></returns>
|
||||
[HttpPost("update-external-source")]
|
||||
public async Task<ActionResult<ExternalSourceDto>> UpdateExternalSource(ExternalSourceDto dto)
|
||||
{
|
||||
// Check if a host and api key exists for the current user
|
||||
return Ok(await _streamService.UpdateExternalSource(User.GetUserId(), dto));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates the external source by host is unique (for this user)
|
||||
/// </summary>
|
||||
/// <param name="host"></param>
|
||||
/// <returns></returns>
|
||||
[HttpGet("external-source-exists")]
|
||||
public async Task<ActionResult<bool>> ExternalSourceExists(string host, string name, string apiKey)
|
||||
{
|
||||
return Ok(await _unitOfWork.AppUserExternalSourceRepository.ExternalSourceExists(User.GetUserId(), host, name, apiKey));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Delete's the external source
|
||||
/// </summary>
|
||||
/// <param name="externalSourceId"></param>
|
||||
/// <returns></returns>
|
||||
[HttpDelete("delete-external-source")]
|
||||
public async Task<ActionResult> ExternalSourceExists(int externalSourceId)
|
||||
{
|
||||
await _streamService.DeleteExternalSource(User.GetUserId(), externalSourceId);
|
||||
return Ok();
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Creates a Dashboard Stream from a SmartFilter and adds it to the user's dashboard as visible
|
||||
/// </summary>
|
||||
/// <param name="smartFilterId"></param>
|
||||
/// <returns></returns>
|
||||
[HttpPost("add-dashboard-stream")]
|
||||
public async Task<ActionResult<DashboardStreamDto>> AddDashboard([FromQuery] int smartFilterId)
|
||||
{
|
||||
return Ok(await _streamService.CreateDashboardStreamFromSmartFilter(User.GetUserId(), smartFilterId));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the visibility of a dashboard stream
|
||||
/// </summary>
|
||||
/// <param name="dto"></param>
|
||||
/// <returns></returns>
|
||||
[HttpPost("update-dashboard-stream")]
|
||||
public async Task<ActionResult> UpdateDashboardStream(DashboardStreamDto dto)
|
||||
{
|
||||
await _streamService.UpdateDashboardStream(User.GetUserId(), dto);
|
||||
return Ok();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the position of a dashboard stream
|
||||
/// </summary>
|
||||
/// <param name="dto"></param>
|
||||
/// <returns></returns>
|
||||
[HttpPost("update-dashboard-position")]
|
||||
public async Task<ActionResult> UpdateDashboardStreamPosition(UpdateStreamPositionDto dto)
|
||||
{
|
||||
await _streamService.UpdateDashboardStreamPosition(User.GetUserId(), dto);
|
||||
return Ok();
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Creates a SideNav Stream from a SmartFilter and adds it to the user's sidenav as visible
|
||||
/// </summary>
|
||||
/// <param name="smartFilterId"></param>
|
||||
/// <returns></returns>
|
||||
[HttpPost("add-sidenav-stream")]
|
||||
public async Task<ActionResult<SideNavStreamDto>> AddSideNav([FromQuery] int smartFilterId)
|
||||
{
|
||||
return Ok(await _streamService.CreateSideNavStreamFromSmartFilter(User.GetUserId(), smartFilterId));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a SideNav Stream from a SmartFilter and adds it to the user's sidenav as visible
|
||||
/// </summary>
|
||||
/// <param name="externalSourceId"></param>
|
||||
/// <returns></returns>
|
||||
[HttpPost("add-sidenav-stream-from-external-source")]
|
||||
public async Task<ActionResult<SideNavStreamDto>> AddSideNavFromExternalSource([FromQuery] int externalSourceId)
|
||||
{
|
||||
return Ok(await _streamService.CreateSideNavStreamFromExternalSource(User.GetUserId(), externalSourceId));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the visibility of a dashboard stream
|
||||
/// </summary>
|
||||
/// <param name="dto"></param>
|
||||
/// <returns></returns>
|
||||
[HttpPost("update-sidenav-stream")]
|
||||
public async Task<ActionResult> UpdateSideNavStream(SideNavStreamDto dto)
|
||||
{
|
||||
await _streamService.UpdateSideNavStream(User.GetUserId(), dto);
|
||||
return Ok();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the position of a dashboard stream
|
||||
/// </summary>
|
||||
/// <param name="dto"></param>
|
||||
/// <returns></returns>
|
||||
[HttpPost("update-sidenav-position")]
|
||||
public async Task<ActionResult> UpdateSideNavStreamPosition(UpdateStreamPositionDto dto)
|
||||
{
|
||||
await _streamService.UpdateSideNavStreamPosition(User.GetUserId(), dto);
|
||||
return Ok();
|
||||
}
|
||||
}
|
||||
9
API/DTOs/Dashboard/UpdateStreamPositionDto.cs
Normal file
9
API/DTOs/Dashboard/UpdateStreamPositionDto.cs
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
namespace API.DTOs.Dashboard;
|
||||
|
||||
public class UpdateStreamPositionDto
|
||||
{
|
||||
public int FromPosition { get; set; }
|
||||
public int ToPosition { get; set; }
|
||||
public int Id { get; set; }
|
||||
public string StreamName { get; set; }
|
||||
}
|
||||
11
API/DTOs/SideNav/ExternalSourceDto.cs
Normal file
11
API/DTOs/SideNav/ExternalSourceDto.cs
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
using System;
|
||||
|
||||
namespace API.DTOs.SideNav;
|
||||
|
||||
public class ExternalSourceDto
|
||||
{
|
||||
public required int Id { get; set; } = 0;
|
||||
public required string Name { get; set; }
|
||||
public required string Host { get; set; }
|
||||
public required string ApiKey { get; set; }
|
||||
}
|
||||
39
API/DTOs/SideNav/SideNavStreamDto.cs
Normal file
39
API/DTOs/SideNav/SideNavStreamDto.cs
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
using API.Entities;
|
||||
using API.Entities.Enums;
|
||||
|
||||
namespace API.DTOs.SideNav;
|
||||
|
||||
public class SideNavStreamDto
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public required string Name { get; set; }
|
||||
/// <summary>
|
||||
/// Is System Provided
|
||||
/// </summary>
|
||||
public bool IsProvided { get; set; }
|
||||
/// <summary>
|
||||
/// Sort Order on the Dashboard
|
||||
/// </summary>
|
||||
public int Order { get; set; }
|
||||
/// <summary>
|
||||
/// If Not IsProvided, the appropriate smart filter
|
||||
/// </summary>
|
||||
/// <remarks>Encoded filter</remarks>
|
||||
public string? SmartFilterEncoded { get; set; }
|
||||
public int? SmartFilterId { get; set; }
|
||||
/// <summary>
|
||||
/// External Source Url if configured
|
||||
/// </summary>
|
||||
public int ExternalSourceId { get; set; }
|
||||
public ExternalSourceDto? ExternalSource { get; set; }
|
||||
/// <summary>
|
||||
/// For system provided
|
||||
/// </summary>
|
||||
public SideNavStreamType StreamType { get; set; }
|
||||
public bool Visible { get; set; }
|
||||
public int? LibraryId { get; set; }
|
||||
/// <summary>
|
||||
/// Only available for SideNavStreamType.Library
|
||||
/// </summary>
|
||||
public LibraryDto? Library { get; set; }
|
||||
}
|
||||
|
|
@ -56,6 +56,8 @@ public sealed class DataContext : IdentityDbContext<AppUser, AppRole, int,
|
|||
public DbSet<AppUserTableOfContent> AppUserTableOfContent { get; set; } = null!;
|
||||
public DbSet<AppUserSmartFilter> AppUserSmartFilter { get; set; } = null!;
|
||||
public DbSet<AppUserDashboardStream> AppUserDashboardStream { get; set; } = null!;
|
||||
public DbSet<AppUserSideNavStream> AppUserSideNavStream { get; set; } = null!;
|
||||
public DbSet<AppUserExternalSource> AppUserExternalSource { get; set; } = null!;
|
||||
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder builder)
|
||||
|
|
@ -128,6 +130,13 @@ public sealed class DataContext : IdentityDbContext<AppUser, AppRole, int,
|
|||
builder.Entity<AppUserDashboardStream>()
|
||||
.HasIndex(e => e.Visible)
|
||||
.IsUnique(false);
|
||||
|
||||
builder.Entity<AppUserSideNavStream>()
|
||||
.Property(b => b.StreamType)
|
||||
.HasDefaultValue(SideNavStreamType.SmartFilter);
|
||||
builder.Entity<AppUserSideNavStream>()
|
||||
.HasIndex(e => e.Visible)
|
||||
.IsUnique(false);
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
52
API/Data/ManualMigrations/MigrateUserLibrarySideNavStream.cs
Normal file
52
API/Data/ManualMigrations/MigrateUserLibrarySideNavStream.cs
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using API.Data.Repositories;
|
||||
using API.Entities;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace API.Data.ManualMigrations;
|
||||
|
||||
/// <summary>
|
||||
/// Introduced in v0.7.8.7 and v0.7.9, this adds SideNavStream's for all Libraries a User has access to
|
||||
/// </summary>
|
||||
public static class MigrateUserLibrarySideNavStream
|
||||
{
|
||||
public static async Task Migrate(IUnitOfWork unitOfWork, DataContext dataContext, ILogger<Program> logger)
|
||||
{
|
||||
logger.LogCritical("Running MigrateUserLibrarySideNavStream migration - Please be patient, this may take some time. This is not an error");
|
||||
|
||||
var usersWithLibraryStreams = await dataContext.AppUser.Include(u => u.SideNavStreams)
|
||||
.AnyAsync(u => u.SideNavStreams.Count > 0 && u.SideNavStreams.Any(s => s.LibraryId > 0));
|
||||
|
||||
if (usersWithLibraryStreams)
|
||||
{
|
||||
logger.LogCritical("Running MigrateUserLibrarySideNavStream migration - complete. Nothing to do");
|
||||
return;
|
||||
}
|
||||
|
||||
var users = await unitOfWork.UserRepository.GetAllUsersAsync(AppUserIncludes.SideNavStreams);
|
||||
foreach (var user in users)
|
||||
{
|
||||
var userLibraries = await unitOfWork.LibraryRepository.GetLibrariesForUserIdAsync(user.Id);
|
||||
foreach (var lib in userLibraries)
|
||||
{
|
||||
var prevMaxOrder = user.SideNavStreams.Max(s => s.Order);
|
||||
user.SideNavStreams.Add(new AppUserSideNavStream()
|
||||
{
|
||||
Name = lib.Name,
|
||||
LibraryId = lib.Id,
|
||||
IsProvided = false,
|
||||
Visible = true,
|
||||
StreamType = SideNavStreamType.Library,
|
||||
Order = prevMaxOrder + 1
|
||||
});
|
||||
}
|
||||
unitOfWork.UserRepository.Update(user);
|
||||
}
|
||||
|
||||
await unitOfWork.CommitAsync();
|
||||
|
||||
logger.LogCritical("Running MigrateUserLibrarySideNavStream migration - Completed. This is not an error");
|
||||
}
|
||||
}
|
||||
2472
API/Data/Migrations/20231013194957_SideNavStreamAndExternalSource.Designer.cs
generated
Normal file
2472
API/Data/Migrations/20231013194957_SideNavStreamAndExternalSource.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,98 @@
|
|||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace API.Data.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class SideNavStreamAndExternalSource : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "AppUserExternalSource",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
Name = table.Column<string>(type: "TEXT", nullable: true),
|
||||
Host = table.Column<string>(type: "TEXT", nullable: true),
|
||||
ApiKey = table.Column<string>(type: "TEXT", nullable: true),
|
||||
AppUserId = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_AppUserExternalSource", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_AppUserExternalSource_AspNetUsers_AppUserId",
|
||||
column: x => x.AppUserId,
|
||||
principalTable: "AspNetUsers",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "AppUserSideNavStream",
|
||||
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),
|
||||
LibraryId = table.Column<int>(type: "INTEGER", nullable: true),
|
||||
ExternalSourceId = table.Column<int>(type: "INTEGER", nullable: true),
|
||||
StreamType = table.Column<int>(type: "INTEGER", nullable: false, defaultValue: 5),
|
||||
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_AppUserSideNavStream", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_AppUserSideNavStream_AppUserSmartFilter_SmartFilterId",
|
||||
column: x => x.SmartFilterId,
|
||||
principalTable: "AppUserSmartFilter",
|
||||
principalColumn: "Id");
|
||||
table.ForeignKey(
|
||||
name: "FK_AppUserSideNavStream_AspNetUsers_AppUserId",
|
||||
column: x => x.AppUserId,
|
||||
principalTable: "AspNetUsers",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_AppUserExternalSource_AppUserId",
|
||||
table: "AppUserExternalSource",
|
||||
column: "AppUserId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_AppUserSideNavStream_AppUserId",
|
||||
table: "AppUserSideNavStream",
|
||||
column: "AppUserId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_AppUserSideNavStream_SmartFilterId",
|
||||
table: "AppUserSideNavStream",
|
||||
column: "SmartFilterId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_AppUserSideNavStream_Visible",
|
||||
table: "AppUserSideNavStream",
|
||||
column: "Visible");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "AppUserExternalSource");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "AppUserSideNavStream");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -15,7 +15,7 @@ namespace API.Data.Migrations
|
|||
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder.HasAnnotation("ProductVersion", "7.0.10");
|
||||
modelBuilder.HasAnnotation("ProductVersion", "7.0.11");
|
||||
|
||||
modelBuilder.Entity("API.Entities.AppRole", b =>
|
||||
{
|
||||
|
|
@ -223,6 +223,31 @@ namespace API.Data.Migrations
|
|||
b.ToTable("AppUserDashboardStream");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.AppUserExternalSource", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("ApiKey")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("AppUserId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Host")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("AppUserId");
|
||||
|
||||
b.ToTable("AppUserExternalSource");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.AppUserOnDeckRemoval", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
|
|
@ -456,6 +481,52 @@ namespace API.Data.Migrations
|
|||
b.ToTable("AspNetUserRoles", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.AppUserSideNavStream", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("AppUserId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int?>("ExternalSourceId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("IsProvided")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int?>("LibraryId")
|
||||
.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(5);
|
||||
|
||||
b.Property<bool>("Visible")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("AppUserId");
|
||||
|
||||
b.HasIndex("SmartFilterId");
|
||||
|
||||
b.HasIndex("Visible");
|
||||
|
||||
b.ToTable("AppUserSideNavStream");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.AppUserSmartFilter", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
|
|
@ -1790,6 +1861,17 @@ namespace API.Data.Migrations
|
|||
b.Navigation("SmartFilter");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.AppUserExternalSource", b =>
|
||||
{
|
||||
b.HasOne("API.Entities.AppUser", "AppUser")
|
||||
.WithMany("ExternalSources")
|
||||
.HasForeignKey("AppUserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("AppUser");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.AppUserOnDeckRemoval", b =>
|
||||
{
|
||||
b.HasOne("API.Entities.AppUser", "AppUser")
|
||||
|
|
@ -1887,6 +1969,23 @@ namespace API.Data.Migrations
|
|||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.AppUserSideNavStream", b =>
|
||||
{
|
||||
b.HasOne("API.Entities.AppUser", "AppUser")
|
||||
.WithMany("SideNavStreams")
|
||||
.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.AppUserSmartFilter", b =>
|
||||
{
|
||||
b.HasOne("API.Entities.AppUser", "AppUser")
|
||||
|
|
@ -2303,6 +2402,8 @@ namespace API.Data.Migrations
|
|||
|
||||
b.Navigation("Devices");
|
||||
|
||||
b.Navigation("ExternalSources");
|
||||
|
||||
b.Navigation("Progresses");
|
||||
|
||||
b.Navigation("Ratings");
|
||||
|
|
@ -2311,6 +2412,8 @@ namespace API.Data.Migrations
|
|||
|
||||
b.Navigation("ScrobbleHolds");
|
||||
|
||||
b.Navigation("SideNavStreams");
|
||||
|
||||
b.Navigation("SmartFilters");
|
||||
|
||||
b.Navigation("TableOfContents");
|
||||
|
|
|
|||
78
API/Data/Repositories/AppUserExternalSourceRepository.cs
Normal file
78
API/Data/Repositories/AppUserExternalSourceRepository.cs
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using API.DTOs.SideNav;
|
||||
using API.Entities;
|
||||
using AutoMapper;
|
||||
using AutoMapper.QueryableExtensions;
|
||||
using Kavita.Common.Helpers;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace API.Data.Repositories;
|
||||
|
||||
public interface IAppUserExternalSourceRepository
|
||||
{
|
||||
void Update(AppUserExternalSource source);
|
||||
void Delete(AppUserExternalSource source);
|
||||
Task<AppUserExternalSource> GetById(int externalSourceId);
|
||||
Task<IList<ExternalSourceDto>> GetExternalSources(int userId);
|
||||
Task<bool> ExternalSourceExists(int userId, string name, string host, string apiKey);
|
||||
}
|
||||
|
||||
public class AppUserExternalSourceRepository : IAppUserExternalSourceRepository
|
||||
{
|
||||
private readonly DataContext _context;
|
||||
private readonly IMapper _mapper;
|
||||
|
||||
public AppUserExternalSourceRepository(DataContext context, IMapper mapper)
|
||||
{
|
||||
_context = context;
|
||||
_mapper = mapper;
|
||||
}
|
||||
|
||||
public void Update(AppUserExternalSource source)
|
||||
{
|
||||
_context.Entry(source).State = EntityState.Modified;
|
||||
}
|
||||
|
||||
public void Delete(AppUserExternalSource source)
|
||||
{
|
||||
_context.AppUserExternalSource.Remove(source);
|
||||
}
|
||||
|
||||
public async Task<AppUserExternalSource> GetById(int externalSourceId)
|
||||
{
|
||||
return await _context.AppUserExternalSource
|
||||
.Where(s => s.Id == externalSourceId)
|
||||
.FirstOrDefaultAsync();
|
||||
}
|
||||
|
||||
public async Task<IList<ExternalSourceDto>> GetExternalSources(int userId)
|
||||
{
|
||||
return await _context.AppUserExternalSource.Where(s => s.AppUserId == userId)
|
||||
.ProjectTo<ExternalSourceDto>(_mapper.ConfigurationProvider)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if all the properties match exactly. This will allow a user to setup 2 External Sources with different Users
|
||||
/// </summary>
|
||||
/// <param name="userId"></param>
|
||||
/// <param name="host"></param>
|
||||
/// <param name="name"></param>
|
||||
/// <param name="apiKey"></param>
|
||||
/// <returns></returns>
|
||||
public async Task<bool> ExternalSourceExists(int userId, string name, string host, string apiKey)
|
||||
{
|
||||
host = host.Trim();
|
||||
if (string.IsNullOrEmpty(host) || string.IsNullOrEmpty(name) || string.IsNullOrEmpty(apiKey)) return false;
|
||||
var hostWithEndingSlash = UrlHelper.EnsureEndsWithSlash(host)!;
|
||||
return await _context.AppUserExternalSource
|
||||
.Where(s => s.AppUserId == userId )
|
||||
.Where(s => s.Host.ToUpper().Equals(hostWithEndingSlash.ToUpper())
|
||||
&& s.Name.ToUpper().Equals(name.ToUpper())
|
||||
&& s.ApiKey.Equals(apiKey))
|
||||
.AnyAsync();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,4 @@
|
|||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
|
@ -11,11 +10,11 @@ using API.DTOs.Filtering.v2;
|
|||
using API.DTOs.Reader;
|
||||
using API.DTOs.Scrobbling;
|
||||
using API.DTOs.SeriesDetail;
|
||||
using API.DTOs.SideNav;
|
||||
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;
|
||||
|
|
@ -37,7 +36,9 @@ public enum AppUserIncludes
|
|||
Devices = 256,
|
||||
ScrobbleHolds = 512,
|
||||
SmartFilters = 1024,
|
||||
DashboardStreams = 2048
|
||||
DashboardStreams = 2048,
|
||||
SideNavStreams = 4096,
|
||||
ExternalSources = 8192 // 2^13
|
||||
}
|
||||
|
||||
public interface IUserRepository
|
||||
|
|
@ -46,10 +47,12 @@ public interface IUserRepository
|
|||
void Update(AppUserPreferences preferences);
|
||||
void Update(AppUserBookmark bookmark);
|
||||
void Update(AppUserDashboardStream stream);
|
||||
void Update(AppUserSideNavStream stream);
|
||||
void Add(AppUserBookmark bookmark);
|
||||
void Delete(AppUser? user);
|
||||
void Delete(AppUserBookmark bookmark);
|
||||
void Delete(IList<AppUserDashboardStream> streams);
|
||||
void Delete(IEnumerable<AppUserDashboardStream> streams);
|
||||
void Delete(IEnumerable<AppUserSideNavStream> streams);
|
||||
Task<IEnumerable<MemberDto>> GetEmailConfirmedMemberDtosAsync(bool emailConfirmed = true);
|
||||
Task<IEnumerable<AppUser>> GetAdminUsersAsync();
|
||||
Task<bool> IsUserAdminAsync(AppUser? user);
|
||||
|
|
@ -83,6 +86,11 @@ public interface IUserRepository
|
|||
Task<IList<DashboardStreamDto>> GetDashboardStreams(int userId, bool visibleOnly = false);
|
||||
Task<AppUserDashboardStream?> GetDashboardStream(int streamId);
|
||||
Task<IList<AppUserDashboardStream>> GetDashboardStreamWithFilter(int filterId);
|
||||
Task<IList<SideNavStreamDto>> GetSideNavStreams(int userId, bool visibleOnly = false);
|
||||
Task<AppUserSideNavStream?> GetSideNavStream(int streamId);
|
||||
Task<IList<AppUserSideNavStream>> GetSideNavStreamWithFilter(int filterId);
|
||||
Task<IList<AppUserSideNavStream>> GetSideNavStreamsByLibraryId(int libraryId);
|
||||
Task<IList<AppUserSideNavStream>> GetSideNavStreamWithExternalSource(int externalSourceId);
|
||||
}
|
||||
|
||||
public class UserRepository : IUserRepository
|
||||
|
|
@ -118,6 +126,11 @@ public class UserRepository : IUserRepository
|
|||
_context.Entry(stream).State = EntityState.Modified;
|
||||
}
|
||||
|
||||
public void Update(AppUserSideNavStream stream)
|
||||
{
|
||||
_context.Entry(stream).State = EntityState.Modified;
|
||||
}
|
||||
|
||||
public void Add(AppUserBookmark bookmark)
|
||||
{
|
||||
_context.AppUserBookmark.Add(bookmark);
|
||||
|
|
@ -134,11 +147,16 @@ public class UserRepository : IUserRepository
|
|||
_context.AppUserBookmark.Remove(bookmark);
|
||||
}
|
||||
|
||||
public void Delete(IList<AppUserDashboardStream> streams)
|
||||
public void Delete(IEnumerable<AppUserDashboardStream> streams)
|
||||
{
|
||||
_context.AppUserDashboardStream.RemoveRange(streams);
|
||||
}
|
||||
|
||||
public void Delete(IEnumerable<AppUserSideNavStream> streams)
|
||||
{
|
||||
_context.AppUserSideNavStream.RemoveRange(streams);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A one stop shop to get a tracked AppUser instance with any number of JOINs generated by passing bitwise flags.
|
||||
/// </summary>
|
||||
|
|
@ -353,6 +371,89 @@ public class UserRepository : IUserRepository
|
|||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<IList<SideNavStreamDto>> GetSideNavStreams(int userId, bool visibleOnly = false)
|
||||
{
|
||||
var sideNavStreams = await _context.AppUserSideNavStream
|
||||
.Where(d => d.AppUserId == userId)
|
||||
.WhereIf(visibleOnly, d => d.Visible)
|
||||
.OrderBy(d => d.Order)
|
||||
.Include(d => d.SmartFilter)
|
||||
.Select(d => new SideNavStreamDto()
|
||||
{
|
||||
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,
|
||||
LibraryId = d.LibraryId ?? 0,
|
||||
ExternalSourceId = d.ExternalSourceId ?? 0,
|
||||
StreamType = d.StreamType,
|
||||
Order = d.Order,
|
||||
Visible = d.Visible
|
||||
})
|
||||
.ToListAsync();
|
||||
|
||||
var libraryIds = sideNavStreams.Where(d => d.StreamType == SideNavStreamType.Library)
|
||||
.Select(d => d.LibraryId)
|
||||
.ToList();
|
||||
|
||||
var libraryDtos = _context.Library
|
||||
.Where(l => libraryIds.Contains(l.Id))
|
||||
.ProjectTo<LibraryDto>(_mapper.ConfigurationProvider)
|
||||
.ToList();
|
||||
|
||||
foreach (var dto in sideNavStreams.Where(dto => dto.StreamType == SideNavStreamType.Library))
|
||||
{
|
||||
dto.Library = libraryDtos.FirstOrDefault(l => l.Id == dto.LibraryId);
|
||||
}
|
||||
|
||||
var externalSourceIds = sideNavStreams.Where(d => d.StreamType == SideNavStreamType.ExternalSource)
|
||||
.Select(d => d.ExternalSourceId)
|
||||
.ToList();
|
||||
|
||||
var externalSourceDtos = _context.AppUserExternalSource
|
||||
.Where(l => externalSourceIds.Contains(l.Id))
|
||||
.ProjectTo<ExternalSourceDto>(_mapper.ConfigurationProvider)
|
||||
.ToList();
|
||||
|
||||
foreach (var dto in sideNavStreams.Where(dto => dto.StreamType == SideNavStreamType.ExternalSource))
|
||||
{
|
||||
dto.ExternalSource = externalSourceDtos.FirstOrDefault(l => l.Id == dto.ExternalSourceId);
|
||||
}
|
||||
|
||||
return sideNavStreams;
|
||||
}
|
||||
|
||||
public async Task<AppUserSideNavStream> GetSideNavStream(int streamId)
|
||||
{
|
||||
return await _context.AppUserSideNavStream
|
||||
.Include(d => d.SmartFilter)
|
||||
.FirstOrDefaultAsync(d => d.Id == streamId);
|
||||
}
|
||||
|
||||
public async Task<IList<AppUserSideNavStream>> GetSideNavStreamWithFilter(int filterId)
|
||||
{
|
||||
return await _context.AppUserSideNavStream
|
||||
.Include(d => d.SmartFilter)
|
||||
.Where(d => d.SmartFilter != null && d.SmartFilter.Id == filterId)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<IList<AppUserSideNavStream>> GetSideNavStreamsByLibraryId(int libraryId)
|
||||
{
|
||||
return await _context.AppUserSideNavStream
|
||||
.Where(d => d.LibraryId == libraryId)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<IList<AppUserSideNavStream>> GetSideNavStreamWithExternalSource(int externalSourceId)
|
||||
{
|
||||
return await _context.AppUserSideNavStream
|
||||
.Where(d => d.ExternalSourceId == externalSourceId)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
|
||||
public async Task<IEnumerable<AppUser>> GetAdminUsersAsync()
|
||||
{
|
||||
return await _userManager.GetUsersInRoleAsync(PolicyConstants.AdminRole);
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ public static class Seed
|
|||
{
|
||||
new()
|
||||
{
|
||||
Name = "On Deck",
|
||||
Name = "on-deck",
|
||||
StreamType = DashboardStreamType.OnDeck,
|
||||
Order = 0,
|
||||
IsProvided = true,
|
||||
|
|
@ -52,7 +52,7 @@ public static class Seed
|
|||
},
|
||||
new()
|
||||
{
|
||||
Name = "Recently Updated",
|
||||
Name = "recently-updated",
|
||||
StreamType = DashboardStreamType.RecentlyUpdated,
|
||||
Order = 1,
|
||||
IsProvided = true,
|
||||
|
|
@ -60,7 +60,7 @@ public static class Seed
|
|||
},
|
||||
new()
|
||||
{
|
||||
Name = "Newly Added",
|
||||
Name = "newly-added",
|
||||
StreamType = DashboardStreamType.NewlyAdded,
|
||||
Order = 2,
|
||||
IsProvided = true,
|
||||
|
|
@ -68,7 +68,7 @@ public static class Seed
|
|||
},
|
||||
new()
|
||||
{
|
||||
Name = "More In",
|
||||
Name = "more-in-genre",
|
||||
StreamType = DashboardStreamType.MoreInGenre,
|
||||
Order = 3,
|
||||
IsProvided = true,
|
||||
|
|
@ -76,6 +76,50 @@ public static class Seed
|
|||
},
|
||||
}.ToArray());
|
||||
|
||||
public static readonly ImmutableArray<AppUserSideNavStream> DefaultSideNavStreams = ImmutableArray.Create(new[]
|
||||
{
|
||||
new AppUserSideNavStream()
|
||||
{
|
||||
Name = "want-to-read",
|
||||
StreamType = SideNavStreamType.WantToRead,
|
||||
Order = 1,
|
||||
IsProvided = true,
|
||||
Visible = true
|
||||
},
|
||||
new AppUserSideNavStream()
|
||||
{
|
||||
Name = "collections",
|
||||
StreamType = SideNavStreamType.Collections,
|
||||
Order = 2,
|
||||
IsProvided = true,
|
||||
Visible = true
|
||||
},
|
||||
new AppUserSideNavStream()
|
||||
{
|
||||
Name = "reading-lists",
|
||||
StreamType = SideNavStreamType.ReadingLists,
|
||||
Order = 3,
|
||||
IsProvided = true,
|
||||
Visible = true
|
||||
},
|
||||
new AppUserSideNavStream()
|
||||
{
|
||||
Name = "bookmarks",
|
||||
StreamType = SideNavStreamType.Bookmarks,
|
||||
Order = 4,
|
||||
IsProvided = true,
|
||||
Visible = true
|
||||
},
|
||||
new AppUserSideNavStream()
|
||||
{
|
||||
Name = "all-series",
|
||||
StreamType = SideNavStreamType.AllSeries,
|
||||
Order = 5,
|
||||
IsProvided = true,
|
||||
Visible = true
|
||||
}
|
||||
});
|
||||
|
||||
public static async Task SeedRoles(RoleManager<AppRole> roleManager)
|
||||
{
|
||||
var roles = typeof(PolicyConstants)
|
||||
|
|
@ -137,6 +181,31 @@ public static class Seed
|
|||
}
|
||||
}
|
||||
|
||||
public static async Task SeedDefaultSideNavStreams(IUnitOfWork unitOfWork)
|
||||
{
|
||||
var allUsers = await unitOfWork.UserRepository.GetAllUsersAsync(AppUserIncludes.SideNavStreams);
|
||||
foreach (var user in allUsers)
|
||||
{
|
||||
if (user.SideNavStreams.Count != 0) continue;
|
||||
user.SideNavStreams ??= new List<AppUserSideNavStream>();
|
||||
foreach (var defaultStream in DefaultSideNavStreams)
|
||||
{
|
||||
var newStream = new AppUserSideNavStream()
|
||||
{
|
||||
Name = defaultStream.Name,
|
||||
IsProvided = defaultStream.IsProvided,
|
||||
Order = defaultStream.Order,
|
||||
StreamType = defaultStream.StreamType,
|
||||
Visible = defaultStream.Visible,
|
||||
};
|
||||
|
||||
user.SideNavStreams.Add(newStream);
|
||||
}
|
||||
unitOfWork.UserRepository.Update(user);
|
||||
await unitOfWork.CommitAsync();
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task SeedSettings(DataContext context, IDirectoryService directoryService)
|
||||
{
|
||||
await context.Database.EnsureCreatedAsync();
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ public interface IUnitOfWork
|
|||
IScrobbleRepository ScrobbleRepository { get; }
|
||||
IUserTableOfContentRepository UserTableOfContentRepository { get; }
|
||||
IAppUserSmartFilterRepository AppUserSmartFilterRepository { get; }
|
||||
IAppUserExternalSourceRepository AppUserExternalSourceRepository { get; }
|
||||
bool Commit();
|
||||
Task<bool> CommitAsync();
|
||||
bool HasChanges();
|
||||
|
|
@ -70,6 +71,7 @@ public class UnitOfWork : IUnitOfWork
|
|||
public IScrobbleRepository ScrobbleRepository => new ScrobbleRepository(_context, _mapper);
|
||||
public IUserTableOfContentRepository UserTableOfContentRepository => new UserTableOfContentRepository(_context, _mapper);
|
||||
public IAppUserSmartFilterRepository AppUserSmartFilterRepository => new AppUserSmartFilterRepository(_context, _mapper);
|
||||
public IAppUserExternalSourceRepository AppUserExternalSourceRepository => new AppUserExternalSourceRepository(_context, _mapper);
|
||||
|
||||
/// <summary>
|
||||
/// Commits changes to the DB. Completes the open transaction.
|
||||
|
|
|
|||
|
|
@ -76,6 +76,11 @@ public class AppUser : IdentityUser<int>, IHasConcurrencyToken
|
|||
/// An ordered list of Streams (pre-configured) or Smart Filters that makes up the User's Dashboard
|
||||
/// </summary>
|
||||
public IList<AppUserDashboardStream> DashboardStreams { get; set; } = null!;
|
||||
/// <summary>
|
||||
/// An ordered list of Streams (pre-configured) or Smart Filters that makes up the User's SideNav
|
||||
/// </summary>
|
||||
public IList<AppUserSideNavStream> SideNavStreams { get; set; } = null!;
|
||||
public IList<AppUserExternalSource> ExternalSources { get; set; } = null!;
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
|
|
|
|||
12
API/Entities/AppUserExternalSource.cs
Normal file
12
API/Entities/AppUserExternalSource.cs
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
namespace API.Entities;
|
||||
|
||||
public class AppUserExternalSource
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public required string Name { get; set; }
|
||||
public required string Host { get; set; }
|
||||
public required string ApiKey { get; set; }
|
||||
|
||||
public int AppUserId { get; set; }
|
||||
public AppUser AppUser { get; set; }
|
||||
}
|
||||
34
API/Entities/AppUserSideNavStream.cs
Normal file
34
API/Entities/AppUserSideNavStream.cs
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
namespace API.Entities;
|
||||
|
||||
public class AppUserSideNavStream
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public required string Name { get; set; }
|
||||
/// <summary>
|
||||
/// Is System Provided
|
||||
/// </summary>
|
||||
public bool IsProvided { get; set; }
|
||||
/// <summary>
|
||||
/// Sort Order on the Dashboard
|
||||
/// </summary>
|
||||
public int Order { get; set; }
|
||||
/// <summary>
|
||||
/// Library Id is for StreamType.Library only
|
||||
/// </summary>
|
||||
public int? LibraryId { get; set; }
|
||||
/// <summary>
|
||||
/// Only set for StreamType.ExternalSource
|
||||
/// </summary>
|
||||
public int? ExternalSourceId { get; set; }
|
||||
/// <summary>
|
||||
/// For system provided
|
||||
/// </summary>
|
||||
public SideNavStreamType StreamType { get; set; }
|
||||
public bool Visible { get; set; }
|
||||
/// <summary>
|
||||
/// If Not IsProvided, the appropriate smart filter
|
||||
/// </summary>
|
||||
public AppUserSmartFilter? SmartFilter { get; set; }
|
||||
public int AppUserId { get; set; }
|
||||
public AppUser AppUser { get; set; }
|
||||
}
|
||||
13
API/Entities/SideNavStreamType.cs
Normal file
13
API/Entities/SideNavStreamType.cs
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
namespace API.Entities;
|
||||
|
||||
public enum SideNavStreamType
|
||||
{
|
||||
Collections = 1,
|
||||
ReadingLists = 2,
|
||||
Bookmarks = 3,
|
||||
Library = 4,
|
||||
SmartFilter = 5,
|
||||
ExternalSource = 6,
|
||||
AllSeries = 7,
|
||||
WantToRead = 8,
|
||||
}
|
||||
|
|
@ -51,6 +51,7 @@ public static class ApplicationServiceExtensions
|
|||
services.AddScoped<IMediaErrorService, MediaErrorService>();
|
||||
services.AddScoped<IMediaConversionService, MediaConversionService>();
|
||||
services.AddScoped<IRecommendationService, RecommendationService>();
|
||||
services.AddScoped<IStreamService, StreamService>();
|
||||
|
||||
services.AddScoped<IScannerService, ScannerService>();
|
||||
services.AddScoped<IMetadataService, MetadataService>();
|
||||
|
|
|
|||
|
|
@ -141,6 +141,17 @@ public static class IncludesExtensions
|
|||
.ThenInclude(s => s.SmartFilter);
|
||||
}
|
||||
|
||||
if (includeFlags.HasFlag(AppUserIncludes.SideNavStreams))
|
||||
{
|
||||
query = query.Include(u => u.SideNavStreams)
|
||||
.ThenInclude(s => s.SmartFilter);
|
||||
}
|
||||
|
||||
if (includeFlags.HasFlag(AppUserIncludes.ExternalSources))
|
||||
{
|
||||
query = query.Include(u => u.ExternalSources);
|
||||
}
|
||||
|
||||
return query.AsSplitQuery();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ using API.DTOs.Scrobbling;
|
|||
using API.DTOs.Search;
|
||||
using API.DTOs.SeriesDetail;
|
||||
using API.DTOs.Settings;
|
||||
using API.DTOs.SideNav;
|
||||
using API.DTOs.Theme;
|
||||
using API.Entities;
|
||||
using API.Entities.Enums;
|
||||
|
|
@ -54,6 +55,7 @@ public class AutoMapperProfiles : Profile
|
|||
CreateMap<AgeRating, AgeRatingDto>();
|
||||
CreateMap<PublicationStatus, PublicationStatusDto>();
|
||||
CreateMap<MediaError, MediaErrorDto>();
|
||||
CreateMap<AppUserExternalSource, ExternalSourceDto>();
|
||||
CreateMap<ScrobbleHold, ScrobbleHoldDto>()
|
||||
.ForMember(dest => dest.LibraryId,
|
||||
opt =>
|
||||
|
|
|
|||
|
|
@ -29,20 +29,39 @@ public class AppUserBuilder : IEntityBuilder<AppUser>
|
|||
Progresses = new List<AppUserProgress>(),
|
||||
Devices = new List<Device>(),
|
||||
Id = 0,
|
||||
DashboardStreams = new List<AppUserDashboardStream>()
|
||||
DashboardStreams = new List<AppUserDashboardStream>(),
|
||||
SideNavStreams = new List<AppUserSideNavStream>()
|
||||
};
|
||||
foreach (var s in Seed.DefaultStreams)
|
||||
{
|
||||
_appUser.DashboardStreams.Add(s);
|
||||
}
|
||||
foreach (var s in Seed.DefaultSideNavStreams)
|
||||
{
|
||||
_appUser.SideNavStreams.Add(s);
|
||||
}
|
||||
}
|
||||
|
||||
public AppUserBuilder WithLibrary(Library library)
|
||||
public AppUserBuilder WithLibrary(Library library, bool createSideNavStream = false)
|
||||
{
|
||||
_appUser.Libraries.Add(library);
|
||||
if (!createSideNavStream) return this;
|
||||
|
||||
if (library.Id != 0 && _appUser.SideNavStreams.Any(s => s.LibraryId == library.Id)) return this;
|
||||
_appUser.SideNavStreams.Add(new AppUserSideNavStream()
|
||||
{
|
||||
Name = library.Name,
|
||||
IsProvided = false,
|
||||
Visible = true,
|
||||
LibraryId = library.Id,
|
||||
StreamType = SideNavStreamType.Library,
|
||||
Order = _appUser.SideNavStreams.Max(s => s.Order) + 1,
|
||||
});
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
public AppUserBuilder WithLocale(string locale)
|
||||
{
|
||||
_appUser.UserPreferences.Locale = locale;
|
||||
|
|
|
|||
|
|
@ -158,6 +158,13 @@
|
|||
"search-description": "Search for Series, Collections, or Reading Lists",
|
||||
"favicon-doesnt-exist": "Favicon does not exist",
|
||||
"smart-filter-doesnt-exist": "Smart Filter doesn't exist",
|
||||
"smart-filter-already-in-use": "There is an existing stream with this Smart Filter",
|
||||
"dashboard-stream-doesnt-exist": "Dashboard Stream doesn't exist",
|
||||
"sidenav-stream-doesnt-exist": "SideNav Stream doesn't exist",
|
||||
"external-source-already-exists": "External Source already exists",
|
||||
"external-source-required": "ApiKey and Host required",
|
||||
"external-source-doesnt-exist": "External Source doesn't exist",
|
||||
"external-source-already-in-use": "There is an existing stream with this External Source",
|
||||
|
||||
"not-authenticated": "User is not authenticated",
|
||||
"unable-to-register-k+": "Unable to register license due to error. Reach out to Kavita+ Support",
|
||||
|
|
|
|||
|
|
@ -64,6 +64,7 @@ public class Program
|
|||
|
||||
using var scope = host.Services.CreateScope();
|
||||
var services = scope.ServiceProvider;
|
||||
var unitOfWork = services.GetRequiredService<IUnitOfWork>();
|
||||
|
||||
try
|
||||
{
|
||||
|
|
@ -87,10 +88,12 @@ public class Program
|
|||
|
||||
await context.Database.MigrateAsync();
|
||||
|
||||
|
||||
await Seed.SeedRoles(services.GetRequiredService<RoleManager<AppRole>>());
|
||||
await Seed.SeedSettings(context, directoryService);
|
||||
await Seed.SeedThemes(context);
|
||||
await Seed.SeedDefaultStreams(services.GetRequiredService<IUnitOfWork>());
|
||||
await Seed.SeedDefaultStreams(unitOfWork);
|
||||
await Seed.SeedDefaultSideNavStreams(unitOfWork);
|
||||
await Seed.SeedUserApiKeys(context);
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
|
@ -106,7 +109,6 @@ public class Program
|
|||
}
|
||||
|
||||
// Update the logger with the log level
|
||||
var unitOfWork = services.GetRequiredService<IUnitOfWork>();
|
||||
var settings = await unitOfWork.SettingsRepository.GetSettingsDtoAsync();
|
||||
LogLevelOptions.SwitchLogLevel(settings.LoggingLevel);
|
||||
|
||||
|
|
|
|||
354
API/Services/StreamService.cs
Normal file
354
API/Services/StreamService.cs
Normal file
|
|
@ -0,0 +1,354 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using API.Data;
|
||||
using API.Data.Repositories;
|
||||
using API.DTOs.Dashboard;
|
||||
using API.DTOs.SideNav;
|
||||
using API.Entities;
|
||||
using API.Entities.Enums;
|
||||
using API.SignalR;
|
||||
using Kavita.Common;
|
||||
using Kavita.Common.Helpers;
|
||||
|
||||
namespace API.Services;
|
||||
|
||||
/// <summary>
|
||||
/// For SideNavStream and DashboardStream manipulation
|
||||
/// </summary>
|
||||
public interface IStreamService
|
||||
{
|
||||
Task<IEnumerable<DashboardStreamDto>> GetDashboardStreams(int userId, bool visibleOnly = true);
|
||||
Task<IEnumerable<SideNavStreamDto>> GetSidenavStreams(int userId, bool visibleOnly = true);
|
||||
Task<IEnumerable<ExternalSourceDto>> GetExternalSources(int userId);
|
||||
Task<DashboardStreamDto> CreateDashboardStreamFromSmartFilter(int userId, int smartFilterId);
|
||||
Task UpdateDashboardStream(int userId, DashboardStreamDto dto);
|
||||
Task UpdateDashboardStreamPosition(int userId, UpdateStreamPositionDto dto);
|
||||
Task<SideNavStreamDto> CreateSideNavStreamFromSmartFilter(int userId, int smartFilterId);
|
||||
Task<SideNavStreamDto> CreateSideNavStreamFromExternalSource(int userId, int externalSourceId);
|
||||
Task UpdateSideNavStream(int userId, SideNavStreamDto dto);
|
||||
Task UpdateSideNavStreamPosition(int userId, UpdateStreamPositionDto dto);
|
||||
Task<ExternalSourceDto> CreateExternalSource(int userId, ExternalSourceDto dto);
|
||||
Task<ExternalSourceDto> UpdateExternalSource(int userId, ExternalSourceDto dto);
|
||||
Task DeleteExternalSource(int userId, int externalSourceId);
|
||||
}
|
||||
|
||||
public class StreamService : IStreamService
|
||||
{
|
||||
private readonly IUnitOfWork _unitOfWork;
|
||||
private readonly IEventHub _eventHub;
|
||||
private readonly ILocalizationService _localizationService;
|
||||
|
||||
public StreamService(IUnitOfWork unitOfWork, IEventHub eventHub, ILocalizationService localizationService)
|
||||
{
|
||||
_unitOfWork = unitOfWork;
|
||||
_eventHub = eventHub;
|
||||
_localizationService = localizationService;
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<DashboardStreamDto>> GetDashboardStreams(int userId, bool visibleOnly = true)
|
||||
{
|
||||
return await _unitOfWork.UserRepository.GetDashboardStreams(userId, visibleOnly);
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<SideNavStreamDto>> GetSidenavStreams(int userId, bool visibleOnly = true)
|
||||
{
|
||||
return await _unitOfWork.UserRepository.GetSideNavStreams(userId, visibleOnly);
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<ExternalSourceDto>> GetExternalSources(int userId)
|
||||
{
|
||||
return await _unitOfWork.AppUserExternalSourceRepository.GetExternalSources(userId);
|
||||
}
|
||||
|
||||
public async Task<DashboardStreamDto> CreateDashboardStreamFromSmartFilter(int userId, int smartFilterId)
|
||||
{
|
||||
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId, AppUserIncludes.DashboardStreams);
|
||||
if (user == null) throw new KavitaException(await _localizationService.Translate(userId, "no-user"));
|
||||
|
||||
var smartFilter = await _unitOfWork.AppUserSmartFilterRepository.GetById(smartFilterId);
|
||||
if (smartFilter == null) throw new KavitaException(await _localizationService.Translate(userId, "smart-filter-doesnt-exist"));
|
||||
|
||||
var stream = user?.DashboardStreams.FirstOrDefault(d => d.SmartFilter?.Id == smartFilterId);
|
||||
if (stream != null) throw new KavitaException(await _localizationService.Translate(userId, "smart-filter-already-in-use"));
|
||||
|
||||
var maxOrder = user!.DashboardStreams.Max(d => d.Order);
|
||||
var createdStream = new AppUserDashboardStream()
|
||||
{
|
||||
Name = smartFilter.Name,
|
||||
IsProvided = false,
|
||||
StreamType = DashboardStreamType.SmartFilter,
|
||||
Visible = true,
|
||||
Order = maxOrder + 1,
|
||||
SmartFilter = smartFilter
|
||||
};
|
||||
|
||||
user.DashboardStreams.Add(createdStream);
|
||||
_unitOfWork.UserRepository.Update(user);
|
||||
await _unitOfWork.CommitAsync();
|
||||
|
||||
var ret = new DashboardStreamDto()
|
||||
{
|
||||
Name = createdStream.Name,
|
||||
IsProvided = createdStream.IsProvided,
|
||||
Visible = createdStream.Visible,
|
||||
Order = createdStream.Order,
|
||||
SmartFilterEncoded = smartFilter.Filter,
|
||||
StreamType = createdStream.StreamType
|
||||
};
|
||||
|
||||
await _eventHub.SendMessageToAsync(MessageFactory.DashboardUpdate, MessageFactory.DashboardUpdateEvent(user.Id),
|
||||
userId);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
public async Task UpdateDashboardStream(int userId, DashboardStreamDto dto)
|
||||
{
|
||||
var stream = await _unitOfWork.UserRepository.GetDashboardStream(dto.Id);
|
||||
if (stream == null) throw new KavitaException(await _localizationService.Translate(userId, "dashboard-stream-doesnt-exist"));
|
||||
stream.Visible = dto.Visible;
|
||||
|
||||
_unitOfWork.UserRepository.Update(stream);
|
||||
await _unitOfWork.CommitAsync();
|
||||
await _eventHub.SendMessageToAsync(MessageFactory.DashboardUpdate, MessageFactory.DashboardUpdateEvent(userId),
|
||||
userId);
|
||||
}
|
||||
|
||||
public async Task UpdateDashboardStreamPosition(int userId, UpdateStreamPositionDto dto)
|
||||
{
|
||||
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId,
|
||||
AppUserIncludes.DashboardStreams);
|
||||
var stream = user?.DashboardStreams.FirstOrDefault(d => d.Id == dto.Id);
|
||||
if (stream == null)
|
||||
throw new KavitaException(await _localizationService.Translate(userId, "dashboard-stream-doesnt-exist"));
|
||||
if (stream.Order == dto.ToPosition) return ;
|
||||
|
||||
var list = user!.DashboardStreams.ToList();
|
||||
ReorderItems(list, stream.Id, dto.ToPosition);
|
||||
user.DashboardStreams = list;
|
||||
|
||||
_unitOfWork.UserRepository.Update(user);
|
||||
await _unitOfWork.CommitAsync();
|
||||
await _eventHub.SendMessageToAsync(MessageFactory.DashboardUpdate, MessageFactory.DashboardUpdateEvent(user.Id),
|
||||
user.Id);
|
||||
}
|
||||
|
||||
public async Task<SideNavStreamDto> CreateSideNavStreamFromSmartFilter(int userId, int smartFilterId)
|
||||
{
|
||||
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId, AppUserIncludes.SideNavStreams);
|
||||
if (user == null) throw new KavitaException(await _localizationService.Translate(userId, "no-user"));
|
||||
|
||||
var smartFilter = await _unitOfWork.AppUserSmartFilterRepository.GetById(smartFilterId);
|
||||
if (smartFilter == null) throw new KavitaException(await _localizationService.Translate(userId, "smart-filter-doesnt-exist"));
|
||||
|
||||
var stream = user?.SideNavStreams.FirstOrDefault(d => d.SmartFilter?.Id == smartFilterId);
|
||||
if (stream != null) throw new KavitaException(await _localizationService.Translate(userId, "smart-filter-already-in-use"));
|
||||
|
||||
var maxOrder = user!.SideNavStreams.Max(d => d.Order);
|
||||
var createdStream = new AppUserSideNavStream()
|
||||
{
|
||||
Name = smartFilter.Name,
|
||||
IsProvided = false,
|
||||
StreamType = SideNavStreamType.SmartFilter,
|
||||
Visible = true,
|
||||
Order = maxOrder + 1,
|
||||
SmartFilter = smartFilter
|
||||
};
|
||||
|
||||
user.SideNavStreams.Add(createdStream);
|
||||
_unitOfWork.UserRepository.Update(user);
|
||||
await _unitOfWork.CommitAsync();
|
||||
|
||||
var ret = new SideNavStreamDto()
|
||||
{
|
||||
Name = createdStream.Name,
|
||||
IsProvided = createdStream.IsProvided,
|
||||
Visible = createdStream.Visible,
|
||||
Order = createdStream.Order,
|
||||
SmartFilterEncoded = smartFilter.Filter,
|
||||
StreamType = createdStream.StreamType
|
||||
};
|
||||
|
||||
|
||||
await _eventHub.SendMessageToAsync(MessageFactory.SideNavUpdate, MessageFactory.SideNavUpdateEvent(userId),
|
||||
userId);
|
||||
return ret;
|
||||
}
|
||||
|
||||
public async Task<SideNavStreamDto> CreateSideNavStreamFromExternalSource(int userId, int externalSourceId)
|
||||
{
|
||||
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId, AppUserIncludes.SideNavStreams);
|
||||
if (user == null) throw new KavitaException(await _localizationService.Translate(userId, "no-user"));
|
||||
|
||||
var externalSource = await _unitOfWork.AppUserExternalSourceRepository.GetById(externalSourceId);
|
||||
if (externalSource == null) throw new KavitaException(await _localizationService.Translate(userId, "external-source-doesnt-exist"));
|
||||
|
||||
var stream = user?.SideNavStreams.FirstOrDefault(d => d.ExternalSourceId == externalSourceId);
|
||||
if (stream != null) throw new KavitaException(await _localizationService.Translate(userId, "external-source-already-in-use"));
|
||||
|
||||
var maxOrder = user!.SideNavStreams.Max(d => d.Order);
|
||||
var createdStream = new AppUserSideNavStream()
|
||||
{
|
||||
Name = externalSource.Name,
|
||||
IsProvided = false,
|
||||
StreamType = SideNavStreamType.ExternalSource,
|
||||
Visible = true,
|
||||
Order = maxOrder + 1,
|
||||
ExternalSourceId = externalSource.Id
|
||||
};
|
||||
|
||||
user.SideNavStreams.Add(createdStream);
|
||||
_unitOfWork.UserRepository.Update(user);
|
||||
await _unitOfWork.CommitAsync();
|
||||
|
||||
var ret = new SideNavStreamDto()
|
||||
{
|
||||
Name = createdStream.Name,
|
||||
IsProvided = createdStream.IsProvided,
|
||||
Visible = createdStream.Visible,
|
||||
Order = createdStream.Order,
|
||||
StreamType = createdStream.StreamType,
|
||||
ExternalSource = new ExternalSourceDto()
|
||||
{
|
||||
Host = externalSource.Host,
|
||||
Id = externalSource.Id,
|
||||
Name = externalSource.Name,
|
||||
ApiKey = externalSource.ApiKey
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
await _eventHub.SendMessageToAsync(MessageFactory.SideNavUpdate, MessageFactory.SideNavUpdateEvent(userId),
|
||||
userId);
|
||||
return ret;
|
||||
}
|
||||
|
||||
public async Task UpdateSideNavStream(int userId, SideNavStreamDto dto)
|
||||
{
|
||||
var stream = await _unitOfWork.UserRepository.GetSideNavStream(dto.Id);
|
||||
if (stream == null)
|
||||
throw new KavitaException(await _localizationService.Translate(userId, "sidenav-stream-doesnt-exist"));
|
||||
stream.Visible = dto.Visible;
|
||||
|
||||
_unitOfWork.UserRepository.Update(stream);
|
||||
await _unitOfWork.CommitAsync();
|
||||
await _eventHub.SendMessageToAsync(MessageFactory.SideNavUpdate, MessageFactory.SideNavUpdateEvent(userId),
|
||||
userId);
|
||||
}
|
||||
|
||||
public async Task UpdateSideNavStreamPosition(int userId, UpdateStreamPositionDto dto)
|
||||
{
|
||||
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId,
|
||||
AppUserIncludes.SideNavStreams);
|
||||
var stream = user?.SideNavStreams.FirstOrDefault(d => d.Id == dto.Id);
|
||||
if (stream == null) throw new KavitaException(await _localizationService.Translate(userId, "sidenav-stream-doesnt-exist"));
|
||||
if (stream.Order == dto.ToPosition) return;
|
||||
|
||||
var list = user!.SideNavStreams.ToList();
|
||||
ReorderItems(list, stream.Id, dto.ToPosition);
|
||||
user.SideNavStreams = list;
|
||||
|
||||
_unitOfWork.UserRepository.Update(user);
|
||||
await _unitOfWork.CommitAsync();
|
||||
await _eventHub.SendMessageToAsync(MessageFactory.SideNavUpdate, MessageFactory.SideNavUpdateEvent(userId),
|
||||
userId);
|
||||
}
|
||||
|
||||
public async Task<ExternalSourceDto> CreateExternalSource(int userId, ExternalSourceDto dto)
|
||||
{
|
||||
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId,
|
||||
AppUserIncludes.ExternalSources);
|
||||
if (user == null) throw new KavitaException("not-authenticated");
|
||||
|
||||
if (user.ExternalSources.Any(s => s.Host == dto.Host))
|
||||
{
|
||||
throw new KavitaException("external-source-already-exists");
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(dto.ApiKey) || string.IsNullOrEmpty(dto.Name)) throw new KavitaException("external-source-required");
|
||||
if (!UrlHelper.StartsWithHttpOrHttps(dto.Host)) throw new KavitaException("external-source-host-format");
|
||||
|
||||
|
||||
var newSource = new AppUserExternalSource()
|
||||
{
|
||||
Name = dto.Name,
|
||||
Host = UrlHelper.EnsureEndsWithSlash(
|
||||
UrlHelper.EnsureStartsWithHttpOrHttps(dto.Host)),
|
||||
ApiKey = dto.ApiKey
|
||||
};
|
||||
user.ExternalSources.Add(newSource);
|
||||
|
||||
_unitOfWork.UserRepository.Update(user);
|
||||
await _unitOfWork.CommitAsync();
|
||||
|
||||
dto.Id = newSource.Id;
|
||||
|
||||
return dto;
|
||||
}
|
||||
|
||||
public async Task<ExternalSourceDto> UpdateExternalSource(int userId, ExternalSourceDto dto)
|
||||
{
|
||||
var source = await _unitOfWork.AppUserExternalSourceRepository.GetById(dto.Id);
|
||||
if (source == null) throw new KavitaException("external-source-doesnt-exist");
|
||||
if (source.AppUserId != userId) throw new KavitaException("external-source-doesnt-exist");
|
||||
|
||||
if (string.IsNullOrEmpty(dto.ApiKey) || string.IsNullOrEmpty(dto.Host) || string.IsNullOrEmpty(dto.Name)) throw new KavitaException("external-source-required");
|
||||
|
||||
source.Host = UrlHelper.EnsureEndsWithSlash(
|
||||
UrlHelper.EnsureStartsWithHttpOrHttps(dto.Host));
|
||||
source.ApiKey = dto.ApiKey;
|
||||
source.Name = dto.Name;
|
||||
|
||||
_unitOfWork.AppUserExternalSourceRepository.Update(source);
|
||||
await _unitOfWork.CommitAsync();
|
||||
|
||||
dto.Host = source.Host;
|
||||
return dto;
|
||||
}
|
||||
|
||||
public async Task DeleteExternalSource(int userId, int externalSourceId)
|
||||
{
|
||||
var source = await _unitOfWork.AppUserExternalSourceRepository.GetById(externalSourceId);
|
||||
if (source == null) throw new KavitaException("external-source-doesnt-exist");
|
||||
if (source.AppUserId != userId) throw new KavitaException("external-source-doesnt-exist");
|
||||
|
||||
_unitOfWork.AppUserExternalSourceRepository.Delete(source);
|
||||
|
||||
// Find all SideNav's with this source and delete them as well
|
||||
var streams2 = await _unitOfWork.UserRepository.GetSideNavStreamWithExternalSource(externalSourceId);
|
||||
_unitOfWork.UserRepository.Delete(streams2);
|
||||
|
||||
await _unitOfWork.CommitAsync();
|
||||
}
|
||||
|
||||
private static void ReorderItems(List<AppUserDashboardStream> items, int itemId, int toPosition)
|
||||
{
|
||||
var item = items.Find(r => r.Id == itemId);
|
||||
if (item != null)
|
||||
{
|
||||
items.Remove(item);
|
||||
items.Insert(toPosition, item);
|
||||
}
|
||||
|
||||
for (var i = 0; i < items.Count; i++)
|
||||
{
|
||||
items[i].Order = i;
|
||||
}
|
||||
}
|
||||
|
||||
private static void ReorderItems(List<AppUserSideNavStream> items, int itemId, int toPosition)
|
||||
{
|
||||
var item = items.Find(r => r.Id == itemId);
|
||||
if (item != null)
|
||||
{
|
||||
items.Remove(item);
|
||||
items.Insert(toPosition, item);
|
||||
}
|
||||
|
||||
for (var i = 0; i < items.Count; i++)
|
||||
{
|
||||
items[i].Order = i;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -5,6 +5,7 @@ using System.Threading.Tasks;
|
|||
using API.DTOs.Update;
|
||||
using API.SignalR;
|
||||
using Flurl.Http;
|
||||
using HtmlAgilityPack;
|
||||
using Kavita.Common.EnvironmentInfo;
|
||||
using Kavita.Common.Helpers;
|
||||
using MarkdownDeep;
|
||||
|
|
@ -103,6 +104,7 @@ public class VersionUpdaterService : IVersionUpdaterService
|
|||
};
|
||||
}
|
||||
|
||||
|
||||
public async Task PushUpdate(UpdateNotificationDto? update)
|
||||
{
|
||||
if (update == null) return;
|
||||
|
|
|
|||
|
|
@ -126,6 +126,10 @@ public static class MessageFactory
|
|||
/// Order, Visibility, etc has changed on the Dashboard. UI will refresh the layout
|
||||
/// </summary>
|
||||
public const string DashboardUpdate = "DashboardUpdate";
|
||||
/// <summary>
|
||||
/// Order, Visibility, etc has changed on the Sidenav. UI will refresh the layout
|
||||
/// </summary>
|
||||
public const string SideNavUpdate = "SideNavUpdate";
|
||||
|
||||
public static SignalRMessage DashboardUpdateEvent(int userId)
|
||||
{
|
||||
|
|
@ -142,6 +146,21 @@ public static class MessageFactory
|
|||
};
|
||||
}
|
||||
|
||||
public static SignalRMessage SideNavUpdateEvent(int userId)
|
||||
{
|
||||
return new SignalRMessage()
|
||||
{
|
||||
Name = SideNavUpdate,
|
||||
Title = "SideNav Update",
|
||||
Progress = ProgressType.None,
|
||||
EventType = ProgressEventType.Single,
|
||||
Body = new
|
||||
{
|
||||
UserId = userId
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
public static SignalRMessage ScanSeriesEvent(int libraryId, int seriesId, string seriesName)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -250,6 +250,9 @@ public class Startup
|
|||
// v0.7.6
|
||||
await MigrateExistingRatings.Migrate(dataContext, logger);
|
||||
|
||||
// v0.7.9
|
||||
await MigrateUserLibrarySideNavStream.Migrate(unitOfWork, dataContext, logger);
|
||||
|
||||
// Update the version in the DB after all migrations are run
|
||||
var installVersion = await unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.InstallVersion);
|
||||
installVersion.Value = BuildInfo.Version.ToString();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue