using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Kavita.API.Attributes;
using Kavita.API.Database;
using Kavita.API.Services;
using Kavita.Common;
using Kavita.Common.Helpers;
using Kavita.Models.Constants;
using Kavita.Models.DTOs.Metadata.Browse.Requests;
using Kavita.Models.DTOs.Reader;
using Kavita.Server.Attributes;
using Kavita.Server.Extensions;
using Microsoft.AspNetCore.Mvc;
namespace Kavita.Server.Controllers;
public class AnnotationController(
IUnitOfWork unitOfWork,
ILocalizationService localizationService,
IAnnotationService annotationService)
: BaseApiController
{
///
/// Returns a list of annotations for browsing
///
///
///
///
[HttpPost("all-filtered")]
public async Task>> GetAnnotationsForBrowse(BrowseAnnotationFilterDto filter, [FromQuery] UserParams? userParams)
{
userParams ??= UserParams.Default;
var list = await unitOfWork.AnnotationRepository.GetAnnotationDtos(UserId, filter, userParams);
Response.AddPaginationHeader(list.CurrentPage, list.PageSize, list.TotalCount, list.TotalPages);
return Ok(list);
}
///
/// Returns the annotations for the given chapter
///
///
///
[HttpGet("all")]
public async Task>> GetAnnotations(int chapterId)
{
return Ok(await unitOfWork.UserRepository.GetAnnotations(UserId, chapterId));
}
///
/// Returns all annotations by Series
///
///
///
[HttpGet("all-for-series")]
public async Task> GetAnnotationsBySeries(int seriesId)
{
return Ok(await unitOfWork.UserRepository.GetAnnotationDtosBySeries(UserId, seriesId));
}
///
/// Returns the Annotation by Id. User must have access to annotation.
///
///
///
[HttpGet("{annotationId}")]
public async Task> GetAnnotation(int annotationId)
{
return Ok(await unitOfWork.UserRepository.GetAnnotationDtoById(UserId, annotationId));
}
///
/// Create a new Annotation for the user against a Chapter
///
///
///
[HttpPost("create")]
[DisallowRole(PolicyConstants.ReadOnlyRole)]
public async Task> CreateAnnotation(AnnotationDto dto)
{
try
{
return Ok(await annotationService.CreateAnnotation(UserId, dto));
}
catch (KavitaException ex)
{
return BadRequest(await localizationService.Translate(UserId, ex.Message));
}
}
///
/// Update the modifiable fields (Spoiler, highlight slot, and comment) for an annotation
///
///
///
[HttpPost("update")]
[DisallowRole(PolicyConstants.ReadOnlyRole)]
public async Task> UpdateAnnotation(AnnotationDto dto)
{
try
{
return Ok(await annotationService.UpdateAnnotation(UserId, dto));
}
catch (KavitaException ex)
{
return BadRequest(await localizationService.Translate(UserId, ex.Message));
}
}
///
/// Adds a like for the currently authenticated user if not already from the annotations with given ids
///
///
///
[HttpPost("like")]
[DisallowRole(PolicyConstants.ReadOnlyRole)]
public async Task LikeAnnotations(IList ids)
{
var userId = UserId;
var annotations = await unitOfWork.AnnotationRepository.GetAnnotations(userId, ids);
if (annotations.Count != ids.Count)
{
return BadRequest();
}
foreach (var annotation in annotations.Where(a => !a.Likes.Contains(userId) && a.AppUserId != userId))
{
annotation.Likes.Add(userId);
unitOfWork.AnnotationRepository.Update(annotation);
}
if (unitOfWork.HasChanges())
{
await unitOfWork.CommitAsync();
}
return Ok();
}
///
/// Removes likes for the currently authenticated user if present from the annotations with given ids
///
///
///
[HttpPost("unlike")]
[DisallowRole(PolicyConstants.ReadOnlyRole)]
public async Task UnLikeAnnotations(IList ids)
{
var userId = UserId;
var annotations = await unitOfWork.AnnotationRepository.GetAnnotations(userId, ids);
if (annotations.Count != ids.Count)
{
return BadRequest();
}
foreach (var annotation in annotations.Where(a => a.Likes.Contains(userId)))
{
annotation.Likes.Remove(userId);
unitOfWork.AnnotationRepository.Update(annotation);
}
if (unitOfWork.HasChanges())
{
await unitOfWork.CommitAsync();
}
return Ok();
}
///
/// Delete the annotation for the user
///
///
///
[HttpDelete]
[DisallowRole(PolicyConstants.ReadOnlyRole)]
public async Task DeleteAnnotation(int annotationId)
{
var annotation = await unitOfWork.AnnotationRepository.GetAnnotation(annotationId);
if (annotation == null || annotation.AppUserId != UserId) return BadRequest(await localizationService.Translate(UserId, "annotation-delete"));
unitOfWork.AnnotationRepository.Remove(annotation);
await unitOfWork.CommitAsync();
return Ok();
}
///
/// Removes annotations in bulk. Requires every annotation to be owned by the authenticated user
///
///
///
[HttpPost("bulk-delete")]
[DisallowRole(PolicyConstants.ReadOnlyRole)]
public async Task DeleteAnnotationsBulk(IList annotationIds)
{
var userId = UserId;
var annotations = await unitOfWork.AnnotationRepository.GetAnnotations(userId, annotationIds);
if (annotations.Any(a => a.AppUserId != userId))
{
return BadRequest();
}
unitOfWork.AnnotationRepository.Remove(annotations);
await unitOfWork.CommitAsync();
return Ok();
}
///
/// Exports annotations for the given users
///
///
[HttpPost("export-filter")]
[DisallowRole(PolicyConstants.ReadOnlyRole)]
public async Task ExportAnnotationsFilter(BrowseAnnotationFilterDto filter, [FromQuery] UserParams? userParams)
{
userParams ??= UserParams.Default;
var list = await unitOfWork.AnnotationRepository.GetAnnotationDtos(UserId, filter, userParams);
var annotations = list.Select(a => a.Id).ToList();
var json = await annotationService.ExportAnnotations(UserId, annotations);
if (string.IsNullOrEmpty(json)) return BadRequest();
var bytes = Encoding.UTF8.GetBytes(json);
var fileName = System.Web.HttpUtility.UrlEncode($"annotations_export_{UserId}_{DateTime.UtcNow:yyyyMMdd_HHmmss}_filtered");
return File(bytes, "application/json", fileName + ".json");
}
///
/// Exports Annotations for the User
///
/// Export annotations with the given ids
///
[HttpPost("export")]
[DisallowRole(PolicyConstants.ReadOnlyRole)]
public async Task ExportAnnotations(IList? annotations = null)
{
var json = await annotationService.ExportAnnotations(UserId, annotations);
if (string.IsNullOrEmpty(json)) return BadRequest();
var bytes = Encoding.UTF8.GetBytes(json);
var fileName = System.Web.HttpUtility.UrlEncode($"annotations_export_{UserId}_{DateTime.UtcNow:yyyyMMdd_HHmmss}");
if (annotations != null)
{
fileName += "_user_selection";
}
return File(bytes, "application/json", fileName + ".json");
}
}