diff --git a/API/Controllers/AccountController.cs b/API/Controllers/AccountController.cs index 54d304dd6..025017476 100644 --- a/API/Controllers/AccountController.cs +++ b/API/Controllers/AccountController.cs @@ -38,7 +38,8 @@ namespace API.Controllers [HttpPost("register")] public async Task> Register(RegisterDto registerDto) { - if (await UserExists(registerDto.Username)) + + if (await _userManager.Users.AnyAsync(x => x.UserName == registerDto.Username)) { return BadRequest("Username is taken."); } @@ -88,9 +89,9 @@ namespace API.Controllers }; } - private async Task UserExists(string username) - { - return await _userManager.Users.AnyAsync(user => user.UserName == username.ToLower()); - } + // private async Task UserExists(string username) + // { + // return await _userManager.Users.AnyAsync(user => user.UserName == username.ToLower()); + // } } } \ No newline at end of file diff --git a/API/Controllers/LibraryController.cs b/API/Controllers/LibraryController.cs index 9cc9202e8..1cab508b2 100644 --- a/API/Controllers/LibraryController.cs +++ b/API/Controllers/LibraryController.cs @@ -147,8 +147,13 @@ namespace API.Controllers } [HttpGet("series")] - public async Task>> GetSeriesForLibrary(int libraryId) + public async Task>> GetSeriesForLibrary(int libraryId, bool forUser = false) { + if (forUser) + { + var user = await _userRepository.GetUserByUsernameAsync(User.GetUsername()); + return Ok(await _seriesRepository.GetSeriesDtoForLibraryIdAsync(libraryId, user.Id)); + } return Ok(await _seriesRepository.GetSeriesDtoForLibraryIdAsync(libraryId)); } diff --git a/API/Controllers/ReaderController.cs b/API/Controllers/ReaderController.cs index 17a5d753d..370fe071f 100644 --- a/API/Controllers/ReaderController.cs +++ b/API/Controllers/ReaderController.cs @@ -1,7 +1,15 @@ -using System.Threading.Tasks; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using API.Data; using API.DTOs; +using API.Entities; +using API.Extensions; using API.Interfaces; +using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; namespace API.Controllers { @@ -9,11 +17,23 @@ namespace API.Controllers { private readonly IDirectoryService _directoryService; private readonly ICacheService _cacheService; + private readonly ILogger _logger; + private readonly UserManager _userManager; + private readonly DataContext _dataContext; // TODO: Refactor code into repo + private readonly IUserRepository _userRepository; + private readonly ISeriesRepository _seriesRepository; - public ReaderController(IDirectoryService directoryService, ICacheService cacheService) + public ReaderController(IDirectoryService directoryService, ICacheService cacheService, + ILogger logger, UserManager userManager, DataContext dataContext, + IUserRepository userRepository, ISeriesRepository seriesRepository) { _directoryService = directoryService; _cacheService = cacheService; + _logger = logger; + _userManager = userManager; + _dataContext = dataContext; + _userRepository = userRepository; + _seriesRepository = seriesRepository; } [HttpGet("image")] @@ -28,5 +48,54 @@ namespace API.Controllers return Ok(file); } + + [HttpGet("get-bookmark")] + public async Task> GetBookmark(int volumeId) + { + var user = await _userRepository.GetUserByUsernameAsync(User.GetUsername()); + if (user.Progresses == null) return Ok(0); + var progress = user.Progresses.SingleOrDefault(x => x.AppUserId == user.Id && x.VolumeId == volumeId); + + if (progress != null) return Ok(progress.PagesRead); + + return Ok(0); + } + + [HttpPost("bookmark")] + public async Task Bookmark(BookmarkDto bookmarkDto) + { + var user = await _userRepository.GetUserByUsernameAsync(User.GetUsername()); + _logger.LogInformation($"Saving {user.UserName} progress for {bookmarkDto.VolumeId} to page {bookmarkDto.PageNum}"); + + user.Progresses ??= new List(); + var userProgress = user.Progresses.SingleOrDefault(x => x.VolumeId == bookmarkDto.VolumeId && x.AppUserId == user.Id); + + if (userProgress == null) + { + + user.Progresses.Add(new AppUserProgress + { + PagesRead = bookmarkDto.PageNum, // TODO: PagesRead is misleading. Should it be PageNumber or PagesRead (+1)? + VolumeId = bookmarkDto.VolumeId, + SeriesId = bookmarkDto.SeriesId, + }); + } + else + { + userProgress.PagesRead = bookmarkDto.PageNum; + userProgress.SeriesId = bookmarkDto.SeriesId; + + } + + _userRepository.Update(user); + + if (await _userRepository.SaveAllAsync()) + { + return Ok(); + } + + + return BadRequest("Could not save progress"); + } } } \ No newline at end of file diff --git a/API/Controllers/SeriesController.cs b/API/Controllers/SeriesController.cs index 199c5971c..7df62afc2 100644 --- a/API/Controllers/SeriesController.cs +++ b/API/Controllers/SeriesController.cs @@ -19,16 +19,18 @@ namespace API.Controllers private readonly ITaskScheduler _taskScheduler; private readonly ISeriesRepository _seriesRepository; private readonly ICacheService _cacheService; + private readonly IUserRepository _userRepository; public SeriesController(ILogger logger, IMapper mapper, ITaskScheduler taskScheduler, ISeriesRepository seriesRepository, - ICacheService cacheService) + ICacheService cacheService, IUserRepository userRepository) { _logger = logger; _mapper = mapper; _taskScheduler = taskScheduler; _seriesRepository = seriesRepository; _cacheService = cacheService; + _userRepository = userRepository; } [HttpGet("{seriesId}")] diff --git a/API/Controllers/UsersController.cs b/API/Controllers/UsersController.cs index 535345b75..c3ca25288 100644 --- a/API/Controllers/UsersController.cs +++ b/API/Controllers/UsersController.cs @@ -46,6 +46,8 @@ namespace API.Controllers [HttpGet("has-library-access")] public async Task> HasLibraryAccess(int libraryId) { + // TODO: refactor this to use either userexists or usermanager + var user = await _userRepository.GetUserByUsernameAsync(User.GetUsername()); if (user == null) return BadRequest("Could not validate user"); diff --git a/API/DTOs/SeriesDto.cs b/API/DTOs/SeriesDto.cs index eb10f3d0d..942acc2f1 100644 --- a/API/DTOs/SeriesDto.cs +++ b/API/DTOs/SeriesDto.cs @@ -8,5 +8,11 @@ public string SortName { get; set; } public string Summary { get; set; } public byte[] CoverImage { get; set; } + + // Read Progress + public int Pages { get; set; } + public int PagesRead { get; set; } + //public int VolumesComplete { get; set; } + //public int TotalVolumes { get; set; } } } \ No newline at end of file diff --git a/API/DTOs/VolumeDto.cs b/API/DTOs/VolumeDto.cs index 33ed702c8..a57465857 100644 --- a/API/DTOs/VolumeDto.cs +++ b/API/DTOs/VolumeDto.cs @@ -8,5 +8,6 @@ namespace API.DTOs public string Name { get; set; } public byte[] CoverImage { get; set; } public int Pages { get; set; } + public int PagesRead { get; set; } } } \ No newline at end of file diff --git a/API/Data/BookmarkDto.cs b/API/Data/BookmarkDto.cs new file mode 100644 index 000000000..ea6654165 --- /dev/null +++ b/API/Data/BookmarkDto.cs @@ -0,0 +1,9 @@ +namespace API.Data +{ + public class BookmarkDto + { + public int VolumeId { get; init; } + public int PageNum { get; init; } + public int SeriesId { get; init; } + } +} \ No newline at end of file diff --git a/API/Data/DataContext.cs b/API/Data/DataContext.cs index 7a75ad138..2bb9424c0 100644 --- a/API/Data/DataContext.cs +++ b/API/Data/DataContext.cs @@ -21,7 +21,8 @@ namespace API.Data public DbSet Library { get; set; } public DbSet Series { get; set; } public DbSet Volume { get; set; } - + public DbSet AppUser { get; set; } + public DbSet AppUserProgresses { get; set; } protected override void OnModelCreating(ModelBuilder builder) { diff --git a/API/Data/Migrations/20210114214506_UserProgress.Designer.cs b/API/Data/Migrations/20210114214506_UserProgress.Designer.cs new file mode 100644 index 000000000..cd7e5a53b --- /dev/null +++ b/API/Data/Migrations/20210114214506_UserProgress.Designer.cs @@ -0,0 +1,576 @@ +// +using System; +using API.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +namespace API.Data.Migrations +{ + [DbContext(typeof(DataContext))] + [Migration("20210114214506_UserProgress")] + partial class UserProgress + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "5.0.1"); + + modelBuilder.Entity("API.Entities.AppRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("AspNetRoles"); + }); + + modelBuilder.Entity("API.Entities.AppUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AccessFailedCount") + .HasColumnType("INTEGER"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("EmailConfirmed") + .HasColumnType("INTEGER"); + + b.Property("LastActive") + .HasColumnType("TEXT"); + + b.Property("LockoutEnabled") + .HasColumnType("INTEGER"); + + b.Property("LockoutEnd") + .HasColumnType("TEXT"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("PasswordHash") + .HasColumnType("TEXT"); + + b.Property("PhoneNumber") + .HasColumnType("TEXT"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("INTEGER"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("SecurityStamp") + .HasColumnType("TEXT"); + + b.Property("TwoFactorEnabled") + .HasColumnType("INTEGER"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.ToTable("AspNetUsers"); + }); + + modelBuilder.Entity("API.Entities.AppUserProgress", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("PagesRead") + .HasColumnType("INTEGER"); + + b.Property("VolumeId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.HasIndex("VolumeId"); + + b.ToTable("AppUserProgress"); + }); + + modelBuilder.Entity("API.Entities.AppUserRole", b => + { + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.Property("RoleId") + .HasColumnType("INTEGER"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles"); + }); + + modelBuilder.Entity("API.Entities.FolderPath", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.Property("Path") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("LibraryId"); + + b.ToTable("FolderPath"); + }); + + modelBuilder.Entity("API.Entities.Library", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("Library"); + }); + + modelBuilder.Entity("API.Entities.MangaFile", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Chapter") + .HasColumnType("INTEGER"); + + b.Property("FilePath") + .HasColumnType("TEXT"); + + b.Property("Format") + .HasColumnType("INTEGER"); + + b.Property("NumberOfPages") + .HasColumnType("INTEGER"); + + b.Property("VolumeId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("VolumeId"); + + b.ToTable("MangaFile"); + }); + + modelBuilder.Entity("API.Entities.Series", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CoverImage") + .HasColumnType("BLOB"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("OriginalName") + .HasColumnType("TEXT"); + + b.Property("SortName") + .HasColumnType("TEXT"); + + b.Property("Summary") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("LibraryId"); + + b.ToTable("Series"); + }); + + modelBuilder.Entity("API.Entities.Volume", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CoverImage") + .HasColumnType("BLOB"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Number") + .HasColumnType("INTEGER"); + + b.Property("Pages") + .HasColumnType("INTEGER"); + + b.Property("ProgressId") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ProgressId"); + + b.HasIndex("SeriesId"); + + b.ToTable("Volume"); + }); + + modelBuilder.Entity("AppUserLibrary", b => + { + b.Property("AppUsersId") + .HasColumnType("INTEGER"); + + b.Property("LibrariesId") + .HasColumnType("INTEGER"); + + b.HasKey("AppUsersId", "LibrariesId"); + + b.HasIndex("LibrariesId"); + + b.ToTable("AppUserLibrary"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClaimType") + .HasColumnType("TEXT"); + + b.Property("ClaimValue") + .HasColumnType("TEXT"); + + b.Property("RoleId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClaimType") + .HasColumnType("TEXT"); + + b.Property("ClaimValue") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("TEXT"); + + b.Property("ProviderKey") + .HasColumnType("TEXT"); + + b.Property("ProviderDisplayName") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.Property("LoginProvider") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens"); + }); + + modelBuilder.Entity("API.Entities.AppUserProgress", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany() + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Volume", null) + .WithMany("Progresses") + .HasForeignKey("VolumeId"); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("API.Entities.AppUserRole", b => + { + b.HasOne("API.Entities.AppRole", "Role") + .WithMany("UserRoles") + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.AppUser", "User") + .WithMany("UserRoles") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Role"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("API.Entities.FolderPath", b => + { + b.HasOne("API.Entities.Library", "Library") + .WithMany("Folders") + .HasForeignKey("LibraryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Library"); + }); + + modelBuilder.Entity("API.Entities.MangaFile", b => + { + b.HasOne("API.Entities.Volume", "Volume") + .WithMany("Files") + .HasForeignKey("VolumeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Volume"); + }); + + modelBuilder.Entity("API.Entities.Series", b => + { + b.HasOne("API.Entities.Library", "Library") + .WithMany("Series") + .HasForeignKey("LibraryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Library"); + }); + + modelBuilder.Entity("API.Entities.Volume", b => + { + b.HasOne("API.Entities.AppUserProgress", "Progress") + .WithMany() + .HasForeignKey("ProgressId"); + + b.HasOne("API.Entities.Series", "Series") + .WithMany("Volumes") + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Progress"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("AppUserLibrary", b => + { + b.HasOne("API.Entities.AppUser", null) + .WithMany() + .HasForeignKey("AppUsersId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Library", null) + .WithMany() + .HasForeignKey("LibrariesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("API.Entities.AppRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("API.Entities.AppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("API.Entities.AppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("API.Entities.AppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("API.Entities.AppRole", b => + { + b.Navigation("UserRoles"); + }); + + modelBuilder.Entity("API.Entities.AppUser", b => + { + b.Navigation("UserRoles"); + }); + + modelBuilder.Entity("API.Entities.Library", b => + { + b.Navigation("Folders"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.Series", b => + { + b.Navigation("Volumes"); + }); + + modelBuilder.Entity("API.Entities.Volume", b => + { + b.Navigation("Files"); + + b.Navigation("Progresses"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/API/Data/Migrations/20210114214506_UserProgress.cs b/API/Data/Migrations/20210114214506_UserProgress.cs new file mode 100644 index 000000000..6d966fbdc --- /dev/null +++ b/API/Data/Migrations/20210114214506_UserProgress.cs @@ -0,0 +1,84 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +namespace API.Data.Migrations +{ + public partial class UserProgress : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "ProgressId", + table: "Volume", + type: "INTEGER", + nullable: true); + + migrationBuilder.CreateTable( + name: "AppUserProgress", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + PagesRead = table.Column(type: "INTEGER", nullable: false), + AppUserId = table.Column(type: "INTEGER", nullable: false), + VolumeId = table.Column(type: "INTEGER", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AppUserProgress", x => x.Id); + table.ForeignKey( + name: "FK_AppUserProgress_AspNetUsers_AppUserId", + column: x => x.AppUserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_AppUserProgress_Volume_VolumeId", + column: x => x.VolumeId, + principalTable: "Volume", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateIndex( + name: "IX_Volume_ProgressId", + table: "Volume", + column: "ProgressId"); + + migrationBuilder.CreateIndex( + name: "IX_AppUserProgress_AppUserId", + table: "AppUserProgress", + column: "AppUserId"); + + migrationBuilder.CreateIndex( + name: "IX_AppUserProgress_VolumeId", + table: "AppUserProgress", + column: "VolumeId"); + + migrationBuilder.AddForeignKey( + name: "FK_Volume_AppUserProgress_ProgressId", + table: "Volume", + column: "ProgressId", + principalTable: "AppUserProgress", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_Volume_AppUserProgress_ProgressId", + table: "Volume"); + + migrationBuilder.DropTable( + name: "AppUserProgress"); + + migrationBuilder.DropIndex( + name: "IX_Volume_ProgressId", + table: "Volume"); + + migrationBuilder.DropColumn( + name: "ProgressId", + table: "Volume"); + } + } +} diff --git a/API/Data/Migrations/20210117180406_ReadStatusModifications.Designer.cs b/API/Data/Migrations/20210117180406_ReadStatusModifications.Designer.cs new file mode 100644 index 000000000..d4133c335 --- /dev/null +++ b/API/Data/Migrations/20210117180406_ReadStatusModifications.Designer.cs @@ -0,0 +1,562 @@ +// +using System; +using API.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +namespace API.Data.Migrations +{ + [DbContext(typeof(DataContext))] + [Migration("20210117180406_ReadStatusModifications")] + partial class ReadStatusModifications + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "5.0.1"); + + modelBuilder.Entity("API.Entities.AppRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("AspNetRoles"); + }); + + modelBuilder.Entity("API.Entities.AppUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AccessFailedCount") + .HasColumnType("INTEGER"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("EmailConfirmed") + .HasColumnType("INTEGER"); + + b.Property("LastActive") + .HasColumnType("TEXT"); + + b.Property("LockoutEnabled") + .HasColumnType("INTEGER"); + + b.Property("LockoutEnd") + .HasColumnType("TEXT"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("PasswordHash") + .HasColumnType("TEXT"); + + b.Property("PhoneNumber") + .HasColumnType("TEXT"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("INTEGER"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("SecurityStamp") + .HasColumnType("TEXT"); + + b.Property("TwoFactorEnabled") + .HasColumnType("INTEGER"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.ToTable("AspNetUsers"); + }); + + modelBuilder.Entity("API.Entities.AppUserProgress", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("PagesRead") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("VolumeId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.ToTable("AppUserProgresses"); + }); + + modelBuilder.Entity("API.Entities.AppUserRole", b => + { + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.Property("RoleId") + .HasColumnType("INTEGER"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles"); + }); + + modelBuilder.Entity("API.Entities.FolderPath", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.Property("Path") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("LibraryId"); + + b.ToTable("FolderPath"); + }); + + modelBuilder.Entity("API.Entities.Library", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("Library"); + }); + + modelBuilder.Entity("API.Entities.MangaFile", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Chapter") + .HasColumnType("INTEGER"); + + b.Property("FilePath") + .HasColumnType("TEXT"); + + b.Property("Format") + .HasColumnType("INTEGER"); + + b.Property("NumberOfPages") + .HasColumnType("INTEGER"); + + b.Property("VolumeId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("VolumeId"); + + b.ToTable("MangaFile"); + }); + + modelBuilder.Entity("API.Entities.Series", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CoverImage") + .HasColumnType("BLOB"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("OriginalName") + .HasColumnType("TEXT"); + + b.Property("SortName") + .HasColumnType("TEXT"); + + b.Property("Summary") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("LibraryId"); + + b.ToTable("Series"); + }); + + modelBuilder.Entity("API.Entities.Volume", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CoverImage") + .HasColumnType("BLOB"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Number") + .HasColumnType("INTEGER"); + + b.Property("Pages") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("SeriesId"); + + b.ToTable("Volume"); + }); + + modelBuilder.Entity("AppUserLibrary", b => + { + b.Property("AppUsersId") + .HasColumnType("INTEGER"); + + b.Property("LibrariesId") + .HasColumnType("INTEGER"); + + b.HasKey("AppUsersId", "LibrariesId"); + + b.HasIndex("LibrariesId"); + + b.ToTable("AppUserLibrary"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClaimType") + .HasColumnType("TEXT"); + + b.Property("ClaimValue") + .HasColumnType("TEXT"); + + b.Property("RoleId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClaimType") + .HasColumnType("TEXT"); + + b.Property("ClaimValue") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("TEXT"); + + b.Property("ProviderKey") + .HasColumnType("TEXT"); + + b.Property("ProviderDisplayName") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.Property("LoginProvider") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens"); + }); + + modelBuilder.Entity("API.Entities.AppUserProgress", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("Progresses") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("API.Entities.AppUserRole", b => + { + b.HasOne("API.Entities.AppRole", "Role") + .WithMany("UserRoles") + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.AppUser", "User") + .WithMany("UserRoles") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Role"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("API.Entities.FolderPath", b => + { + b.HasOne("API.Entities.Library", "Library") + .WithMany("Folders") + .HasForeignKey("LibraryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Library"); + }); + + modelBuilder.Entity("API.Entities.MangaFile", b => + { + b.HasOne("API.Entities.Volume", "Volume") + .WithMany("Files") + .HasForeignKey("VolumeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Volume"); + }); + + modelBuilder.Entity("API.Entities.Series", b => + { + b.HasOne("API.Entities.Library", "Library") + .WithMany("Series") + .HasForeignKey("LibraryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Library"); + }); + + modelBuilder.Entity("API.Entities.Volume", b => + { + b.HasOne("API.Entities.Series", "Series") + .WithMany("Volumes") + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("AppUserLibrary", b => + { + b.HasOne("API.Entities.AppUser", null) + .WithMany() + .HasForeignKey("AppUsersId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Library", null) + .WithMany() + .HasForeignKey("LibrariesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("API.Entities.AppRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("API.Entities.AppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("API.Entities.AppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("API.Entities.AppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("API.Entities.AppRole", b => + { + b.Navigation("UserRoles"); + }); + + modelBuilder.Entity("API.Entities.AppUser", b => + { + b.Navigation("Progresses"); + + b.Navigation("UserRoles"); + }); + + modelBuilder.Entity("API.Entities.Library", b => + { + b.Navigation("Folders"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.Series", b => + { + b.Navigation("Volumes"); + }); + + modelBuilder.Entity("API.Entities.Volume", b => + { + b.Navigation("Files"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/API/Data/Migrations/20210117180406_ReadStatusModifications.cs b/API/Data/Migrations/20210117180406_ReadStatusModifications.cs new file mode 100644 index 000000000..d852d8843 --- /dev/null +++ b/API/Data/Migrations/20210117180406_ReadStatusModifications.cs @@ -0,0 +1,154 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +namespace API.Data.Migrations +{ + public partial class ReadStatusModifications : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_AppUserProgress_AspNetUsers_AppUserId", + table: "AppUserProgress"); + + migrationBuilder.DropForeignKey( + name: "FK_AppUserProgress_Volume_VolumeId", + table: "AppUserProgress"); + + migrationBuilder.DropForeignKey( + name: "FK_Volume_AppUserProgress_ProgressId", + table: "Volume"); + + migrationBuilder.DropIndex( + name: "IX_Volume_ProgressId", + table: "Volume"); + + migrationBuilder.DropPrimaryKey( + name: "PK_AppUserProgress", + table: "AppUserProgress"); + + migrationBuilder.DropIndex( + name: "IX_AppUserProgress_VolumeId", + table: "AppUserProgress"); + + migrationBuilder.DropColumn( + name: "ProgressId", + table: "Volume"); + + migrationBuilder.RenameTable( + name: "AppUserProgress", + newName: "AppUserProgresses"); + + migrationBuilder.RenameIndex( + name: "IX_AppUserProgress_AppUserId", + table: "AppUserProgresses", + newName: "IX_AppUserProgresses_AppUserId"); + + migrationBuilder.AlterColumn( + name: "VolumeId", + table: "AppUserProgresses", + type: "INTEGER", + nullable: false, + defaultValue: 0, + oldClrType: typeof(int), + oldType: "INTEGER", + oldNullable: true); + + migrationBuilder.AddColumn( + name: "SeriesId", + table: "AppUserProgresses", + type: "INTEGER", + nullable: false, + defaultValue: 0); + + migrationBuilder.AddPrimaryKey( + name: "PK_AppUserProgresses", + table: "AppUserProgresses", + column: "Id"); + + migrationBuilder.AddForeignKey( + name: "FK_AppUserProgresses_AspNetUsers_AppUserId", + table: "AppUserProgresses", + column: "AppUserId", + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_AppUserProgresses_AspNetUsers_AppUserId", + table: "AppUserProgresses"); + + migrationBuilder.DropPrimaryKey( + name: "PK_AppUserProgresses", + table: "AppUserProgresses"); + + migrationBuilder.DropColumn( + name: "SeriesId", + table: "AppUserProgresses"); + + migrationBuilder.RenameTable( + name: "AppUserProgresses", + newName: "AppUserProgress"); + + migrationBuilder.RenameIndex( + name: "IX_AppUserProgresses_AppUserId", + table: "AppUserProgress", + newName: "IX_AppUserProgress_AppUserId"); + + migrationBuilder.AddColumn( + name: "ProgressId", + table: "Volume", + type: "INTEGER", + nullable: true); + + migrationBuilder.AlterColumn( + name: "VolumeId", + table: "AppUserProgress", + type: "INTEGER", + nullable: true, + oldClrType: typeof(int), + oldType: "INTEGER"); + + migrationBuilder.AddPrimaryKey( + name: "PK_AppUserProgress", + table: "AppUserProgress", + column: "Id"); + + migrationBuilder.CreateIndex( + name: "IX_Volume_ProgressId", + table: "Volume", + column: "ProgressId"); + + migrationBuilder.CreateIndex( + name: "IX_AppUserProgress_VolumeId", + table: "AppUserProgress", + column: "VolumeId"); + + migrationBuilder.AddForeignKey( + name: "FK_AppUserProgress_AspNetUsers_AppUserId", + table: "AppUserProgress", + column: "AppUserId", + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "FK_AppUserProgress_Volume_VolumeId", + table: "AppUserProgress", + column: "VolumeId", + principalTable: "Volume", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + + migrationBuilder.AddForeignKey( + name: "FK_Volume_AppUserProgress_ProgressId", + table: "Volume", + column: "ProgressId", + principalTable: "AppUserProgress", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + } + } +} diff --git a/API/Data/Migrations/20210117181421_SeriesPages.Designer.cs b/API/Data/Migrations/20210117181421_SeriesPages.Designer.cs new file mode 100644 index 000000000..8caa3acc1 --- /dev/null +++ b/API/Data/Migrations/20210117181421_SeriesPages.Designer.cs @@ -0,0 +1,565 @@ +// +using System; +using API.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +namespace API.Data.Migrations +{ + [DbContext(typeof(DataContext))] + [Migration("20210117181421_SeriesPages")] + partial class SeriesPages + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "5.0.1"); + + modelBuilder.Entity("API.Entities.AppRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("AspNetRoles"); + }); + + modelBuilder.Entity("API.Entities.AppUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AccessFailedCount") + .HasColumnType("INTEGER"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("EmailConfirmed") + .HasColumnType("INTEGER"); + + b.Property("LastActive") + .HasColumnType("TEXT"); + + b.Property("LockoutEnabled") + .HasColumnType("INTEGER"); + + b.Property("LockoutEnd") + .HasColumnType("TEXT"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("PasswordHash") + .HasColumnType("TEXT"); + + b.Property("PhoneNumber") + .HasColumnType("TEXT"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("INTEGER"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("SecurityStamp") + .HasColumnType("TEXT"); + + b.Property("TwoFactorEnabled") + .HasColumnType("INTEGER"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.ToTable("AspNetUsers"); + }); + + modelBuilder.Entity("API.Entities.AppUserProgress", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("PagesRead") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("VolumeId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.ToTable("AppUserProgresses"); + }); + + modelBuilder.Entity("API.Entities.AppUserRole", b => + { + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.Property("RoleId") + .HasColumnType("INTEGER"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles"); + }); + + modelBuilder.Entity("API.Entities.FolderPath", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.Property("Path") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("LibraryId"); + + b.ToTable("FolderPath"); + }); + + modelBuilder.Entity("API.Entities.Library", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("Library"); + }); + + modelBuilder.Entity("API.Entities.MangaFile", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Chapter") + .HasColumnType("INTEGER"); + + b.Property("FilePath") + .HasColumnType("TEXT"); + + b.Property("Format") + .HasColumnType("INTEGER"); + + b.Property("NumberOfPages") + .HasColumnType("INTEGER"); + + b.Property("VolumeId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("VolumeId"); + + b.ToTable("MangaFile"); + }); + + modelBuilder.Entity("API.Entities.Series", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CoverImage") + .HasColumnType("BLOB"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("OriginalName") + .HasColumnType("TEXT"); + + b.Property("Pages") + .HasColumnType("INTEGER"); + + b.Property("SortName") + .HasColumnType("TEXT"); + + b.Property("Summary") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("LibraryId"); + + b.ToTable("Series"); + }); + + modelBuilder.Entity("API.Entities.Volume", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CoverImage") + .HasColumnType("BLOB"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Number") + .HasColumnType("INTEGER"); + + b.Property("Pages") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("SeriesId"); + + b.ToTable("Volume"); + }); + + modelBuilder.Entity("AppUserLibrary", b => + { + b.Property("AppUsersId") + .HasColumnType("INTEGER"); + + b.Property("LibrariesId") + .HasColumnType("INTEGER"); + + b.HasKey("AppUsersId", "LibrariesId"); + + b.HasIndex("LibrariesId"); + + b.ToTable("AppUserLibrary"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClaimType") + .HasColumnType("TEXT"); + + b.Property("ClaimValue") + .HasColumnType("TEXT"); + + b.Property("RoleId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClaimType") + .HasColumnType("TEXT"); + + b.Property("ClaimValue") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("TEXT"); + + b.Property("ProviderKey") + .HasColumnType("TEXT"); + + b.Property("ProviderDisplayName") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.Property("LoginProvider") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens"); + }); + + modelBuilder.Entity("API.Entities.AppUserProgress", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("Progresses") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("API.Entities.AppUserRole", b => + { + b.HasOne("API.Entities.AppRole", "Role") + .WithMany("UserRoles") + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.AppUser", "User") + .WithMany("UserRoles") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Role"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("API.Entities.FolderPath", b => + { + b.HasOne("API.Entities.Library", "Library") + .WithMany("Folders") + .HasForeignKey("LibraryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Library"); + }); + + modelBuilder.Entity("API.Entities.MangaFile", b => + { + b.HasOne("API.Entities.Volume", "Volume") + .WithMany("Files") + .HasForeignKey("VolumeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Volume"); + }); + + modelBuilder.Entity("API.Entities.Series", b => + { + b.HasOne("API.Entities.Library", "Library") + .WithMany("Series") + .HasForeignKey("LibraryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Library"); + }); + + modelBuilder.Entity("API.Entities.Volume", b => + { + b.HasOne("API.Entities.Series", "Series") + .WithMany("Volumes") + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("AppUserLibrary", b => + { + b.HasOne("API.Entities.AppUser", null) + .WithMany() + .HasForeignKey("AppUsersId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Library", null) + .WithMany() + .HasForeignKey("LibrariesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("API.Entities.AppRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("API.Entities.AppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("API.Entities.AppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("API.Entities.AppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("API.Entities.AppRole", b => + { + b.Navigation("UserRoles"); + }); + + modelBuilder.Entity("API.Entities.AppUser", b => + { + b.Navigation("Progresses"); + + b.Navigation("UserRoles"); + }); + + modelBuilder.Entity("API.Entities.Library", b => + { + b.Navigation("Folders"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.Series", b => + { + b.Navigation("Volumes"); + }); + + modelBuilder.Entity("API.Entities.Volume", b => + { + b.Navigation("Files"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/API/Data/Migrations/20210117181421_SeriesPages.cs b/API/Data/Migrations/20210117181421_SeriesPages.cs new file mode 100644 index 000000000..97ee23b1b --- /dev/null +++ b/API/Data/Migrations/20210117181421_SeriesPages.cs @@ -0,0 +1,24 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +namespace API.Data.Migrations +{ + public partial class SeriesPages : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "Pages", + table: "Series", + type: "INTEGER", + nullable: false, + defaultValue: 0); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Pages", + table: "Series"); + } + } +} diff --git a/API/Data/Migrations/DataContextModelSnapshot.cs b/API/Data/Migrations/DataContextModelSnapshot.cs index aa9889bcc..7d345a870 100644 --- a/API/Data/Migrations/DataContextModelSnapshot.cs +++ b/API/Data/Migrations/DataContextModelSnapshot.cs @@ -118,6 +118,31 @@ namespace API.Data.Migrations b.ToTable("AspNetUsers"); }); + modelBuilder.Entity("API.Entities.AppUserProgress", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("PagesRead") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("VolumeId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.ToTable("AppUserProgresses"); + }); + modelBuilder.Entity("API.Entities.AppUserRole", b => { b.Property("UserId") @@ -230,6 +255,9 @@ namespace API.Data.Migrations b.Property("OriginalName") .HasColumnType("TEXT"); + b.Property("Pages") + .HasColumnType("INTEGER"); + b.Property("SortName") .HasColumnType("TEXT"); @@ -376,6 +404,17 @@ namespace API.Data.Migrations b.ToTable("AspNetUserTokens"); }); + modelBuilder.Entity("API.Entities.AppUserProgress", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("Progresses") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + modelBuilder.Entity("API.Entities.AppUserRole", b => { b.HasOne("API.Entities.AppRole", "Role") @@ -497,6 +536,8 @@ namespace API.Data.Migrations modelBuilder.Entity("API.Entities.AppUser", b => { + b.Navigation("Progresses"); + b.Navigation("UserRoles"); }); diff --git a/API/Data/SeriesRepository.cs b/API/Data/SeriesRepository.cs index 932b5ca37..ff48c31af 100644 --- a/API/Data/SeriesRepository.cs +++ b/API/Data/SeriesRepository.cs @@ -46,12 +46,26 @@ namespace API.Data return _context.Series.SingleOrDefault(x => x.Name == name); } - public async Task> GetSeriesDtoForLibraryIdAsync(int libraryId) + public async Task> GetSeriesDtoForLibraryIdAsync(int libraryId, int userId = 0) { - return await _context.Series - .Where(series => series.LibraryId == libraryId) + var series = await _context.Series + .Where(s => s.LibraryId == libraryId) .OrderBy(s => s.SortName) - .ProjectTo(_mapper.ConfigurationProvider).ToListAsync(); + .ProjectTo(_mapper.ConfigurationProvider) + .ToListAsync(); + if (userId > 0) + { + var userProgress = await _context.AppUserProgresses + .Where(p => p.AppUserId == userId && series.Select(s => s.Id).Contains(p.SeriesId)) + .ToListAsync(); + + foreach (var s in series) + { + s.PagesRead = userProgress.Where(p => p.SeriesId == s.Id).Sum(p => p.PagesRead); + } + } + + return series; } public async Task> GetVolumesDtoAsync(int seriesId) @@ -112,5 +126,10 @@ namespace API.Data return await _context.SaveChangesAsync() > 0; } + + public async Task GetVolumeByIdAsync(int volumeId) + { + return await _context.Volume.SingleOrDefaultAsync(x => x.Id == volumeId); + } } } \ No newline at end of file diff --git a/API/Data/UserRepository.cs b/API/Data/UserRepository.cs index 5098b156c..3f04505aa 100644 --- a/API/Data/UserRepository.cs +++ b/API/Data/UserRepository.cs @@ -32,7 +32,8 @@ namespace API.Data public void Delete(AppUser user) { - _context.Users.Remove(user); + // TODO: Check how to implement for _userMangaer + _context.AppUser.Remove(user); } public async Task SaveAllAsync() @@ -42,17 +43,25 @@ namespace API.Data public async Task> GetUsersAsync() { - return await _context.Users.ToListAsync(); + return await _userManager.Users.ToListAsync(); + //return await _context.Users.ToListAsync(); } public async Task GetUserByIdAsync(int id) { - return await _context.Users.FindAsync(id); + // TODO: How to use userManager + return await _context.AppUser.FindAsync(id); } + /// + /// Gets an AppUser by username. Returns back Progress information. + /// + /// + /// public async Task GetUserByUsernameAsync(string username) { - return await _context.Users + return await _userManager.Users + .Include(u => u.Progresses) .SingleOrDefaultAsync(x => x.UserName == username); } @@ -88,11 +97,16 @@ namespace API.Data public async Task GetMemberAsync(string username) { - return await _context.Users.Where(x => x.UserName == username) + return await _userManager.Users.Where(x => x.UserName == username) .Include(x => x.Libraries) .ProjectTo(_mapper.ConfigurationProvider) .SingleOrDefaultAsync(); } + + public void UpdateReadingProgressAsync(int volumeId, int pageNum) + { + + } } } \ No newline at end of file diff --git a/API/Entities/AppUser.cs b/API/Entities/AppUser.cs index 9e66a7d00..46480fdd1 100644 --- a/API/Entities/AppUser.cs +++ b/API/Entities/AppUser.cs @@ -12,12 +12,15 @@ namespace API.Entities public DateTime Created { get; set; } = DateTime.Now; public DateTime LastActive { get; set; } public ICollection Libraries { get; set; } - [ConcurrencyCheck] - public uint RowVersion { get; set; } + public ICollection UserRoles { get; set; } //public ICollection SeriesProgresses { get; set; } + public ICollection Progresses { get; set; } + + [ConcurrencyCheck] + public uint RowVersion { get; set; } public void OnSavingChanges() { diff --git a/API/Entities/AppUserProgress.cs b/API/Entities/AppUserProgress.cs index cfff3d9d4..2f9274e26 100644 --- a/API/Entities/AppUserProgress.cs +++ b/API/Entities/AppUserProgress.cs @@ -5,6 +5,13 @@ /// public class AppUserProgress { + public int Id { get; set; } + public int PagesRead { get; set; } + public AppUser AppUser { get; set; } + public int AppUserId { get; set; } + public int VolumeId { get; set; } + public int SeriesId { get; set; } // shortcut + //public bool VolumeCompleted { get; set; } // This will be set true if PagesRead == Sum of MangaFiles on volume } } \ No newline at end of file diff --git a/API/Entities/Series.cs b/API/Entities/Series.cs index 368a04e50..3fc8c0b72 100644 --- a/API/Entities/Series.cs +++ b/API/Entities/Series.cs @@ -26,6 +26,14 @@ namespace API.Entities public DateTime Created { get; set; } public DateTime LastModified { get; set; } public byte[] CoverImage { get; set; } + /// + /// Sum of all Volume pages + /// + public int Pages { get; set; } + /// + /// Total Volumes linked to Entity + /// + //public int TotalVolumes { get; set; } public ICollection Volumes { get; set; } public Library Library { get; set; } public int LibraryId { get; set; } diff --git a/API/Entities/Volume.cs b/API/Entities/Volume.cs index 1550ab335..8239792f9 100644 --- a/API/Entities/Volume.cs +++ b/API/Entities/Volume.cs @@ -15,8 +15,7 @@ namespace API.Entities public byte[] CoverImage { get; set; } public int Pages { get; set; } - // public string CachePath {get; set;} // Path where cache is located. Default null, resets to null on deletion. - //public ICollection AppUserProgress { get; set; } + // Many-to-One relationships public Series Series { get; set; } diff --git a/API/Interfaces/ISeriesRepository.cs b/API/Interfaces/ISeriesRepository.cs index 490628163..1400431d7 100644 --- a/API/Interfaces/ISeriesRepository.cs +++ b/API/Interfaces/ISeriesRepository.cs @@ -12,7 +12,7 @@ namespace API.Interfaces Task GetSeriesByNameAsync(string name); Series GetSeriesByName(string name); bool SaveAll(); - Task> GetSeriesDtoForLibraryIdAsync(int libraryId); + Task> GetSeriesDtoForLibraryIdAsync(int libraryId, int userId = 0); Task> GetVolumesDtoAsync(int seriesId); IEnumerable GetVolumes(int seriesId); Task GetSeriesDtoByIdAsync(int seriesId); @@ -22,5 +22,6 @@ namespace API.Interfaces Task> GetVolumesForSeriesAsync(int[] seriesIds); Task DeleteSeriesAsync(int seriesId); + Task GetVolumeByIdAsync(int volumeId); } } \ No newline at end of file diff --git a/API/Services/DirectoryService.cs b/API/Services/DirectoryService.cs index 02358862c..5828b507a 100644 --- a/API/Services/DirectoryService.cs +++ b/API/Services/DirectoryService.cs @@ -125,6 +125,8 @@ namespace API.Services var volumes = UpdateVolumes(series, infos, forceUpdate); series.Volumes = volumes; series.CoverImage = volumes.OrderBy(x => x.Number).FirstOrDefault()?.CoverImage; + series.Pages = volumes.Sum(v => v.Pages); + //series.TotalVolumes = volumes.Count; return series; }