Smart Filters & Dashboard Customization (#2282)
Co-authored-by: Robbie Davis <robbie@therobbiedavis.com>
This commit is contained in:
parent
3d501c9532
commit
84f85b4f24
92 changed files with 7149 additions and 555 deletions
|
|
@ -8,6 +8,7 @@ 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;
|
||||
|
|
@ -1035,4 +1036,123 @@ 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,15 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using API.Constants;
|
||||
using API.Data;
|
||||
using API.Data.Repositories;
|
||||
using API.DTOs.Dashboard;
|
||||
using API.DTOs.Filtering.v2;
|
||||
using API.Entities;
|
||||
using API.Extensions;
|
||||
using API.Helpers;
|
||||
using EasyCaching.Core;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
|
|
@ -22,38 +29,66 @@ public class FilterController : BaseApiController
|
|||
_cacheFactory = cacheFactory;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<ActionResult<FilterV2Dto?>> GetFilter(string name)
|
||||
/// <summary>
|
||||
/// Creates or Updates the filter
|
||||
/// </summary>
|
||||
/// <param name="dto"></param>
|
||||
/// <returns></returns>
|
||||
[HttpPost("update")]
|
||||
public async Task<ActionResult> CreateOrUpdateSmartFilter(FilterV2Dto dto)
|
||||
{
|
||||
var provider = _cacheFactory.GetCachingProvider(EasyCacheProfiles.Filter);
|
||||
if (string.IsNullOrEmpty(name)) return Ok(null);
|
||||
var filter = await provider.GetAsync<FilterV2Dto>(name);
|
||||
if (filter.HasValue)
|
||||
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(User.GetUserId(), AppUserIncludes.SmartFilters);
|
||||
if (user == null) return Unauthorized();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(dto.Name)) return BadRequest("Name must be set");
|
||||
if (Seed.DefaultStreams.Any(s => s.Name.Equals(dto.Name, StringComparison.InvariantCultureIgnoreCase)))
|
||||
{
|
||||
filter.Value.Name = name;
|
||||
return Ok(filter.Value);
|
||||
return BadRequest("You cannot use the name of a system provided stream");
|
||||
}
|
||||
|
||||
return Ok(null);
|
||||
// I might just want to use DashboardStream instead of a separate entity. It will drastically simplify implementation
|
||||
|
||||
var existingFilter =
|
||||
user.SmartFilters.FirstOrDefault(f => f.Name.Equals(dto.Name, StringComparison.InvariantCultureIgnoreCase));
|
||||
if (existingFilter != null)
|
||||
{
|
||||
// Update the filter
|
||||
existingFilter.Filter = SmartFilterHelper.Encode(dto);
|
||||
_unitOfWork.AppUserSmartFilterRepository.Update(existingFilter);
|
||||
}
|
||||
else
|
||||
{
|
||||
existingFilter = new AppUserSmartFilter()
|
||||
{
|
||||
Name = dto.Name,
|
||||
Filter = SmartFilterHelper.Encode(dto)
|
||||
};
|
||||
user.SmartFilters.Add(existingFilter);
|
||||
_unitOfWork.UserRepository.Update(user);
|
||||
}
|
||||
|
||||
if (!_unitOfWork.HasChanges()) return Ok();
|
||||
await _unitOfWork.CommitAsync();
|
||||
|
||||
return Ok();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Caches the filter in the backend and returns a temp string for retrieving.
|
||||
/// </summary>
|
||||
/// <remarks>The cache line lives for only 1 hour</remarks>
|
||||
/// <param name="filterDto"></param>
|
||||
/// <returns></returns>
|
||||
[HttpPost("create-temp")]
|
||||
public async Task<ActionResult<string>> CreateTempFilter(FilterV2Dto filterDto)
|
||||
[HttpGet]
|
||||
public ActionResult<IEnumerable<SmartFilterDto>> GetFilters()
|
||||
{
|
||||
var provider = _cacheFactory.GetCachingProvider(EasyCacheProfiles.Filter);
|
||||
var name = filterDto.Name;
|
||||
if (string.IsNullOrEmpty(filterDto.Name))
|
||||
{
|
||||
name = Guid.NewGuid().ToString();
|
||||
}
|
||||
return Ok(_unitOfWork.AppUserSmartFilterRepository.GetAllDtosByUserId(User.GetUserId()));
|
||||
}
|
||||
|
||||
await provider.SetAsync(name, filterDto, TimeSpan.FromHours(1));
|
||||
return name;
|
||||
[HttpDelete]
|
||||
public async Task<ActionResult> DeleteFilter(int filterId)
|
||||
{
|
||||
var filter = await _unitOfWork.AppUserSmartFilterRepository.GetById(filterId);
|
||||
if (filter == null) return Ok();
|
||||
// This needs to delete any dashboard filters that have it too
|
||||
var streams = await _unitOfWork.UserRepository.GetDashboardStreamWithFilter(filter.Id);
|
||||
_unitOfWork.UserRepository.Delete(streams);
|
||||
_unitOfWork.AppUserSmartFilterRepository.Delete(filter);
|
||||
await _unitOfWork.CommitAsync();
|
||||
return Ok();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using System.Collections.Generic;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using API.DTOs.Filtering;
|
||||
|
|
@ -19,11 +20,26 @@ public class LocaleController : BaseApiController
|
|||
[HttpGet]
|
||||
public ActionResult<IEnumerable<string>> GetAllLocales()
|
||||
{
|
||||
var languages = _localizationService.GetLocales().Select(c => new CultureInfo(c)).Select(c =>
|
||||
new LanguageDto()
|
||||
var languages = _localizationService.GetLocales().Select(c =>
|
||||
{
|
||||
Title = c.DisplayName,
|
||||
IsoCode = c.IetfLanguageTag
|
||||
try
|
||||
{
|
||||
var cult = new CultureInfo(c);
|
||||
return new LanguageDto()
|
||||
{
|
||||
Title = cult.DisplayName,
|
||||
IsoCode = cult.IetfLanguageTag
|
||||
};
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Some OS' don't have all culture codes supported like PT_BR, thus we need to default
|
||||
return new LanguageDto()
|
||||
{
|
||||
Title = c,
|
||||
IsoCode = c
|
||||
};
|
||||
}
|
||||
})
|
||||
.Where(l => !string.IsNullOrEmpty(l.IsoCode))
|
||||
.OrderBy(d => d.Title);
|
||||
|
|
|
|||
|
|
@ -102,32 +102,68 @@ public class OpdsController : BaseApiController
|
|||
|
||||
var feed = CreateFeed("Kavita", string.Empty, apiKey, prefix);
|
||||
SetFeedId(feed, "root");
|
||||
feed.Entries.Add(new FeedEntry()
|
||||
|
||||
// Get the user's customized dashboard
|
||||
var streams = await _unitOfWork.UserRepository.GetDashboardStreams(userId, true);
|
||||
foreach (var stream in streams)
|
||||
{
|
||||
Id = "onDeck",
|
||||
Title = await _localizationService.Translate(userId, "on-deck"),
|
||||
Content = new FeedEntryContent()
|
||||
switch (stream.StreamType)
|
||||
{
|
||||
Text = await _localizationService.Translate(userId, "browse-on-deck")
|
||||
},
|
||||
Links = new List<FeedLink>()
|
||||
{
|
||||
CreateLink(FeedLinkRelation.SubSection, FeedLinkType.AtomNavigation, $"{prefix}{apiKey}/on-deck"),
|
||||
case DashboardStreamType.OnDeck:
|
||||
feed.Entries.Add(new FeedEntry()
|
||||
{
|
||||
Id = "onDeck",
|
||||
Title = await _localizationService.Translate(userId, "on-deck"),
|
||||
Content = new FeedEntryContent()
|
||||
{
|
||||
Text = await _localizationService.Translate(userId, "browse-on-deck")
|
||||
},
|
||||
Links = new List<FeedLink>()
|
||||
{
|
||||
CreateLink(FeedLinkRelation.SubSection, FeedLinkType.AtomNavigation, $"{prefix}{apiKey}/on-deck"),
|
||||
}
|
||||
});
|
||||
break;
|
||||
case DashboardStreamType.NewlyAdded:
|
||||
feed.Entries.Add(new FeedEntry()
|
||||
{
|
||||
Id = "recentlyAdded",
|
||||
Title = await _localizationService.Translate(userId, "recently-added"),
|
||||
Content = new FeedEntryContent()
|
||||
{
|
||||
Text = await _localizationService.Translate(userId, "browse-recently-added")
|
||||
},
|
||||
Links = new List<FeedLink>()
|
||||
{
|
||||
CreateLink(FeedLinkRelation.SubSection, FeedLinkType.AtomNavigation, $"{prefix}{apiKey}/recently-added"),
|
||||
}
|
||||
});
|
||||
break;
|
||||
case DashboardStreamType.RecentlyUpdated:
|
||||
// TODO: See if we can implement this and use (count) on series name for number of updates
|
||||
break;
|
||||
case DashboardStreamType.MoreInGenre:
|
||||
// TODO: See if we can implement this
|
||||
break;
|
||||
case DashboardStreamType.SmartFilter:
|
||||
|
||||
feed.Entries.Add(new FeedEntry()
|
||||
{
|
||||
Id = "smartFilter-" + stream.Id,
|
||||
Title = stream.Name,
|
||||
Content = new FeedEntryContent()
|
||||
{
|
||||
Text = stream.Name
|
||||
},
|
||||
Links = new List<FeedLink>()
|
||||
{
|
||||
CreateLink(FeedLinkRelation.SubSection, FeedLinkType.AtomNavigation, $"{prefix}{apiKey}/smart-filter/{stream.SmartFilterId}/"),
|
||||
}
|
||||
});
|
||||
break;
|
||||
}
|
||||
});
|
||||
feed.Entries.Add(new FeedEntry()
|
||||
{
|
||||
Id = "recentlyAdded",
|
||||
Title = await _localizationService.Translate(userId, "recently-added"),
|
||||
Content = new FeedEntryContent()
|
||||
{
|
||||
Text = await _localizationService.Translate(userId, "browse-recently-added")
|
||||
},
|
||||
Links = new List<FeedLink>()
|
||||
{
|
||||
CreateLink(FeedLinkRelation.SubSection, FeedLinkType.AtomNavigation, $"{prefix}{apiKey}/recently-added"),
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
feed.Entries.Add(new FeedEntry()
|
||||
{
|
||||
Id = "readingList",
|
||||
|
|
@ -180,6 +216,19 @@ public class OpdsController : BaseApiController
|
|||
CreateLink(FeedLinkRelation.SubSection, FeedLinkType.AtomNavigation, $"{prefix}{apiKey}/collections"),
|
||||
}
|
||||
});
|
||||
feed.Entries.Add(new FeedEntry()
|
||||
{
|
||||
Id = "allSmartFilters",
|
||||
Title = await _localizationService.Translate(userId, "smart-filters"),
|
||||
Content = new FeedEntryContent()
|
||||
{
|
||||
Text = await _localizationService.Translate(userId, "browse-smart-filters")
|
||||
},
|
||||
Links = new List<FeedLink>()
|
||||
{
|
||||
CreateLink(FeedLinkRelation.SubSection, FeedLinkType.AtomNavigation, $"{prefix}{apiKey}/smart-filters"),
|
||||
}
|
||||
});
|
||||
return CreateXmlResult(SerializeXml(feed));
|
||||
}
|
||||
|
||||
|
|
@ -196,6 +245,67 @@ public class OpdsController : BaseApiController
|
|||
return new Tuple<string, string>(baseUrl, prefix);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the Series matching this smart filter. If FromDashboard, will only return 20 records.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[HttpGet("{apiKey}/smart-filter/{filterId}")]
|
||||
[Produces("application/xml")]
|
||||
public async Task<IActionResult> GetSmartFilter(string apiKey, int filterId)
|
||||
{
|
||||
var userId = await GetUser(apiKey);
|
||||
if (!(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableOpds)
|
||||
return BadRequest(await _localizationService.Translate(userId, "opds-disabled"));
|
||||
var (baseUrl, prefix) = await GetPrefix();
|
||||
|
||||
|
||||
var filter = await _unitOfWork.AppUserSmartFilterRepository.GetById(filterId);
|
||||
if (filter == null) return BadRequest(_localizationService.Translate(userId, "smart-filter-doesnt-exist"));
|
||||
var feed = CreateFeed(await _localizationService.Translate(userId, "smartFilter-" + filter.Id), $"{prefix}{apiKey}/smart-filter/{filter.Id}/", apiKey, prefix);
|
||||
SetFeedId(feed, "smartFilter-" + filter.Id);
|
||||
|
||||
var decodedFilter = SmartFilterHelper.Decode(filter.Filter);
|
||||
var series = await _unitOfWork.SeriesRepository.GetSeriesDtoForLibraryIdV2Async(userId, UserParams.Default,
|
||||
decodedFilter);
|
||||
var seriesMetadatas = await _unitOfWork.SeriesRepository.GetSeriesMetadataForIds(series.Select(s => s.Id));
|
||||
|
||||
foreach (var seriesDto in series)
|
||||
{
|
||||
feed.Entries.Add(CreateSeries(seriesDto, seriesMetadatas.First(s => s.SeriesId == seriesDto.Id), apiKey, prefix, baseUrl));
|
||||
}
|
||||
|
||||
AddPagination(feed, series, $"{prefix}{apiKey}/smart-filter/{filterId}/");
|
||||
return CreateXmlResult(SerializeXml(feed));
|
||||
}
|
||||
|
||||
[HttpGet("{apiKey}/smart-filters")]
|
||||
[Produces("application/xml")]
|
||||
public async Task<IActionResult> GetSmartFilters(string apiKey)
|
||||
{
|
||||
var userId = await GetUser(apiKey);
|
||||
if (!(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableOpds)
|
||||
return BadRequest(await _localizationService.Translate(userId, "opds-disabled"));
|
||||
var (baseUrl, prefix) = await GetPrefix();
|
||||
|
||||
var filters = _unitOfWork.AppUserSmartFilterRepository.GetAllDtosByUserId(userId);
|
||||
var feed = CreateFeed(await _localizationService.Translate(userId, "smartFilters"), $"{prefix}{apiKey}/smart-filters", apiKey, prefix);
|
||||
SetFeedId(feed, "smartFilters");
|
||||
foreach (var filter in filters)
|
||||
{
|
||||
feed.Entries.Add(new FeedEntry()
|
||||
{
|
||||
Id = filter.Id.ToString(),
|
||||
Title = filter.Name,
|
||||
Links = new List<FeedLink>()
|
||||
{
|
||||
CreateLink(FeedLinkRelation.SubSection, FeedLinkType.AtomNavigation, $"{prefix}{apiKey}/smart-filter/{filter.Id}")
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return CreateXmlResult(SerializeXml(feed));
|
||||
}
|
||||
|
||||
|
||||
[HttpGet("{apiKey}/libraries")]
|
||||
[Produces("application/xml")]
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ using API.Constants;
|
|||
using API.Data;
|
||||
using API.Data.Repositories;
|
||||
using API.DTOs;
|
||||
using API.DTOs.Dashboard;
|
||||
using API.DTOs.Filtering;
|
||||
using API.DTOs.Filtering.v2;
|
||||
using API.DTOs.Metadata;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue