mirror of
https://github.com/Kareadita/Kavita.git
synced 2025-07-09 03:04:19 -04:00
Rating Overhaul (#2159)
* Switched Ratings to a float system. Allow rating something as 0%. Allow half step ratings. Added new css variable: --rating-star-color. By default, N/A will show for series that have no ratings. N/A ratings are not included in overall rating calculations. * Show extended entity properties on desktop for list view cards. * Refactored the code for series metadata detail to use a re-usable component to reduce the copy/paste for the Genres tags like sections. * List Item will show extended properties about a chapter/volume, like weblinks on Desktop viewports. * Refactored even further so all of series detail uses the same component code. Tweaked the spacing on the series detail area. List items will now show Characters and Tags which are helpful for more Hentai related content. * Fixed a bug with removing something from "OnDeckRemoval" table when something was read.
This commit is contained in:
parent
f5ad821cd9
commit
734e299f7f
@ -30,9 +30,13 @@ public class SeriesDto : IHasReadTimeEstimate
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Rating from logged in user. Calculated at API-time.
|
/// Rating from logged in user. Calculated at API-time.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int UserRating { get; set; }
|
public float UserRating { get; set; }
|
||||||
public MangaFormat Format { get; set; }
|
/// <summary>
|
||||||
|
/// If the user has set the rating or not
|
||||||
|
/// </summary>
|
||||||
|
public bool HasUserRated { get; set; }
|
||||||
|
|
||||||
|
public MangaFormat Format { get; set; }
|
||||||
public DateTime Created { get; set; }
|
public DateTime Created { get; set; }
|
||||||
|
|
||||||
public bool NameLocked { get; set; }
|
public bool NameLocked { get; set; }
|
||||||
|
@ -1,9 +1,7 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
namespace API.DTOs;
|
||||||
|
|
||||||
namespace API.DTOs;
|
|
||||||
|
|
||||||
public class UpdateSeriesRatingDto
|
public class UpdateSeriesRatingDto
|
||||||
{
|
{
|
||||||
public int SeriesId { get; init; }
|
public int SeriesId { get; init; }
|
||||||
public int UserRating { get; init; }
|
public float UserRating { get; init; }
|
||||||
}
|
}
|
||||||
|
2269
API/Data/Migrations/20230725133536_ChangeRatingScale.Designer.cs
generated
Normal file
2269
API/Data/Migrations/20230725133536_ChangeRatingScale.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
45
API/Data/Migrations/20230725133536_ChangeRatingScale.cs
Normal file
45
API/Data/Migrations/20230725133536_ChangeRatingScale.cs
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace API.Data.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class ChangeRatingScale : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AlterColumn<float>(
|
||||||
|
name: "Rating",
|
||||||
|
table: "AppUserRating",
|
||||||
|
type: "REAL",
|
||||||
|
nullable: false,
|
||||||
|
oldClrType: typeof(int),
|
||||||
|
oldType: "INTEGER");
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<bool>(
|
||||||
|
name: "HasBeenRated",
|
||||||
|
table: "AppUserRating",
|
||||||
|
type: "INTEGER",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "HasBeenRated",
|
||||||
|
table: "AppUserRating");
|
||||||
|
|
||||||
|
migrationBuilder.AlterColumn<int>(
|
||||||
|
name: "Rating",
|
||||||
|
table: "AppUserRating",
|
||||||
|
type: "INTEGER",
|
||||||
|
nullable: false,
|
||||||
|
oldClrType: typeof(float),
|
||||||
|
oldType: "REAL");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -15,7 +15,7 @@ namespace API.Data.Migrations
|
|||||||
protected override void BuildModel(ModelBuilder modelBuilder)
|
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||||
{
|
{
|
||||||
#pragma warning disable 612, 618
|
#pragma warning disable 612, 618
|
||||||
modelBuilder.HasAnnotation("ProductVersion", "7.0.8");
|
modelBuilder.HasAnnotation("ProductVersion", "7.0.9");
|
||||||
|
|
||||||
modelBuilder.Entity("API.Entities.AppRole", b =>
|
modelBuilder.Entity("API.Entities.AppRole", b =>
|
||||||
{
|
{
|
||||||
@ -180,7 +180,7 @@ namespace API.Data.Migrations
|
|||||||
|
|
||||||
b.HasIndex("AppUserId");
|
b.HasIndex("AppUserId");
|
||||||
|
|
||||||
b.ToTable("AppUserBookmark", (string)null);
|
b.ToTable("AppUserBookmark");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("API.Entities.AppUserOnDeckRemoval", b =>
|
modelBuilder.Entity("API.Entities.AppUserOnDeckRemoval", b =>
|
||||||
@ -201,7 +201,7 @@ namespace API.Data.Migrations
|
|||||||
|
|
||||||
b.HasIndex("SeriesId");
|
b.HasIndex("SeriesId");
|
||||||
|
|
||||||
b.ToTable("AppUserOnDeckRemoval", (string)null);
|
b.ToTable("AppUserOnDeckRemoval");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("API.Entities.AppUserPreferences", b =>
|
modelBuilder.Entity("API.Entities.AppUserPreferences", b =>
|
||||||
@ -309,7 +309,7 @@ namespace API.Data.Migrations
|
|||||||
|
|
||||||
b.HasIndex("ThemeId");
|
b.HasIndex("ThemeId");
|
||||||
|
|
||||||
b.ToTable("AppUserPreferences", (string)null);
|
b.ToTable("AppUserPreferences");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("API.Entities.AppUserProgress", b =>
|
modelBuilder.Entity("API.Entities.AppUserProgress", b =>
|
||||||
@ -359,7 +359,7 @@ namespace API.Data.Migrations
|
|||||||
|
|
||||||
b.HasIndex("SeriesId");
|
b.HasIndex("SeriesId");
|
||||||
|
|
||||||
b.ToTable("AppUserProgresses", (string)null);
|
b.ToTable("AppUserProgresses");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("API.Entities.AppUserRating", b =>
|
modelBuilder.Entity("API.Entities.AppUserRating", b =>
|
||||||
@ -371,9 +371,12 @@ namespace API.Data.Migrations
|
|||||||
b.Property<int>("AppUserId")
|
b.Property<int>("AppUserId")
|
||||||
.HasColumnType("INTEGER");
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
b.Property<int>("Rating")
|
b.Property<bool>("HasBeenRated")
|
||||||
.HasColumnType("INTEGER");
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<float>("Rating")
|
||||||
|
.HasColumnType("REAL");
|
||||||
|
|
||||||
b.Property<string>("Review")
|
b.Property<string>("Review")
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
@ -389,7 +392,7 @@ namespace API.Data.Migrations
|
|||||||
|
|
||||||
b.HasIndex("SeriesId");
|
b.HasIndex("SeriesId");
|
||||||
|
|
||||||
b.ToTable("AppUserRating", (string)null);
|
b.ToTable("AppUserRating");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("API.Entities.AppUserRole", b =>
|
modelBuilder.Entity("API.Entities.AppUserRole", b =>
|
||||||
@ -457,7 +460,7 @@ namespace API.Data.Migrations
|
|||||||
|
|
||||||
b.HasIndex("SeriesId");
|
b.HasIndex("SeriesId");
|
||||||
|
|
||||||
b.ToTable("AppUserTableOfContent", (string)null);
|
b.ToTable("AppUserTableOfContent");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("API.Entities.Chapter", b =>
|
modelBuilder.Entity("API.Entities.Chapter", b =>
|
||||||
@ -567,7 +570,7 @@ namespace API.Data.Migrations
|
|||||||
|
|
||||||
b.HasIndex("VolumeId");
|
b.HasIndex("VolumeId");
|
||||||
|
|
||||||
b.ToTable("Chapter", (string)null);
|
b.ToTable("Chapter");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("API.Entities.CollectionTag", b =>
|
modelBuilder.Entity("API.Entities.CollectionTag", b =>
|
||||||
@ -602,7 +605,7 @@ namespace API.Data.Migrations
|
|||||||
b.HasIndex("Id", "Promoted")
|
b.HasIndex("Id", "Promoted")
|
||||||
.IsUnique();
|
.IsUnique();
|
||||||
|
|
||||||
b.ToTable("CollectionTag", (string)null);
|
b.ToTable("CollectionTag");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("API.Entities.Device", b =>
|
modelBuilder.Entity("API.Entities.Device", b =>
|
||||||
@ -648,7 +651,7 @@ namespace API.Data.Migrations
|
|||||||
|
|
||||||
b.HasIndex("AppUserId");
|
b.HasIndex("AppUserId");
|
||||||
|
|
||||||
b.ToTable("Device", (string)null);
|
b.ToTable("Device");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("API.Entities.FolderPath", b =>
|
modelBuilder.Entity("API.Entities.FolderPath", b =>
|
||||||
@ -670,7 +673,7 @@ namespace API.Data.Migrations
|
|||||||
|
|
||||||
b.HasIndex("LibraryId");
|
b.HasIndex("LibraryId");
|
||||||
|
|
||||||
b.ToTable("FolderPath", (string)null);
|
b.ToTable("FolderPath");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("API.Entities.Genre", b =>
|
modelBuilder.Entity("API.Entities.Genre", b =>
|
||||||
@ -690,7 +693,7 @@ namespace API.Data.Migrations
|
|||||||
b.HasIndex("NormalizedTitle")
|
b.HasIndex("NormalizedTitle")
|
||||||
.IsUnique();
|
.IsUnique();
|
||||||
|
|
||||||
b.ToTable("Genre", (string)null);
|
b.ToTable("Genre");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("API.Entities.Library", b =>
|
modelBuilder.Entity("API.Entities.Library", b =>
|
||||||
@ -748,7 +751,7 @@ namespace API.Data.Migrations
|
|||||||
|
|
||||||
b.HasKey("Id");
|
b.HasKey("Id");
|
||||||
|
|
||||||
b.ToTable("Library", (string)null);
|
b.ToTable("Library");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("API.Entities.MangaFile", b =>
|
modelBuilder.Entity("API.Entities.MangaFile", b =>
|
||||||
@ -797,7 +800,7 @@ namespace API.Data.Migrations
|
|||||||
|
|
||||||
b.HasIndex("ChapterId");
|
b.HasIndex("ChapterId");
|
||||||
|
|
||||||
b.ToTable("MangaFile", (string)null);
|
b.ToTable("MangaFile");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("API.Entities.MediaError", b =>
|
modelBuilder.Entity("API.Entities.MediaError", b =>
|
||||||
@ -832,7 +835,7 @@ namespace API.Data.Migrations
|
|||||||
|
|
||||||
b.HasKey("Id");
|
b.HasKey("Id");
|
||||||
|
|
||||||
b.ToTable("MediaError", (string)null);
|
b.ToTable("MediaError");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b =>
|
modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b =>
|
||||||
@ -933,7 +936,7 @@ namespace API.Data.Migrations
|
|||||||
b.HasIndex("Id", "SeriesId")
|
b.HasIndex("Id", "SeriesId")
|
||||||
.IsUnique();
|
.IsUnique();
|
||||||
|
|
||||||
b.ToTable("SeriesMetadata", (string)null);
|
b.ToTable("SeriesMetadata");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("API.Entities.Metadata.SeriesRelation", b =>
|
modelBuilder.Entity("API.Entities.Metadata.SeriesRelation", b =>
|
||||||
@ -957,7 +960,7 @@ namespace API.Data.Migrations
|
|||||||
|
|
||||||
b.HasIndex("TargetSeriesId");
|
b.HasIndex("TargetSeriesId");
|
||||||
|
|
||||||
b.ToTable("SeriesRelation", (string)null);
|
b.ToTable("SeriesRelation");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("API.Entities.Person", b =>
|
modelBuilder.Entity("API.Entities.Person", b =>
|
||||||
@ -977,7 +980,7 @@ namespace API.Data.Migrations
|
|||||||
|
|
||||||
b.HasKey("Id");
|
b.HasKey("Id");
|
||||||
|
|
||||||
b.ToTable("Person", (string)null);
|
b.ToTable("Person");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("API.Entities.ReadingList", b =>
|
modelBuilder.Entity("API.Entities.ReadingList", b =>
|
||||||
@ -1040,7 +1043,7 @@ namespace API.Data.Migrations
|
|||||||
|
|
||||||
b.HasIndex("AppUserId");
|
b.HasIndex("AppUserId");
|
||||||
|
|
||||||
b.ToTable("ReadingList", (string)null);
|
b.ToTable("ReadingList");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("API.Entities.ReadingListItem", b =>
|
modelBuilder.Entity("API.Entities.ReadingListItem", b =>
|
||||||
@ -1074,7 +1077,7 @@ namespace API.Data.Migrations
|
|||||||
|
|
||||||
b.HasIndex("VolumeId");
|
b.HasIndex("VolumeId");
|
||||||
|
|
||||||
b.ToTable("ReadingListItem", (string)null);
|
b.ToTable("ReadingListItem");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("API.Entities.Scrobble.ScrobbleError", b =>
|
modelBuilder.Entity("API.Entities.Scrobble.ScrobbleError", b =>
|
||||||
@ -1119,7 +1122,7 @@ namespace API.Data.Migrations
|
|||||||
|
|
||||||
b.HasIndex("SeriesId");
|
b.HasIndex("SeriesId");
|
||||||
|
|
||||||
b.ToTable("ScrobbleError", (string)null);
|
b.ToTable("ScrobbleError");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("API.Entities.Scrobble.ScrobbleEvent", b =>
|
modelBuilder.Entity("API.Entities.Scrobble.ScrobbleEvent", b =>
|
||||||
@ -1190,7 +1193,7 @@ namespace API.Data.Migrations
|
|||||||
|
|
||||||
b.HasIndex("SeriesId");
|
b.HasIndex("SeriesId");
|
||||||
|
|
||||||
b.ToTable("ScrobbleEvent", (string)null);
|
b.ToTable("ScrobbleEvent");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("API.Entities.Scrobble.ScrobbleHold", b =>
|
modelBuilder.Entity("API.Entities.Scrobble.ScrobbleHold", b =>
|
||||||
@ -1223,7 +1226,7 @@ namespace API.Data.Migrations
|
|||||||
|
|
||||||
b.HasIndex("SeriesId");
|
b.HasIndex("SeriesId");
|
||||||
|
|
||||||
b.ToTable("ScrobbleHold", (string)null);
|
b.ToTable("ScrobbleHold");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("API.Entities.Series", b =>
|
modelBuilder.Entity("API.Entities.Series", b =>
|
||||||
@ -1319,7 +1322,7 @@ namespace API.Data.Migrations
|
|||||||
|
|
||||||
b.HasIndex("LibraryId");
|
b.HasIndex("LibraryId");
|
||||||
|
|
||||||
b.ToTable("Series", (string)null);
|
b.ToTable("Series");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("API.Entities.ServerSetting", b =>
|
modelBuilder.Entity("API.Entities.ServerSetting", b =>
|
||||||
@ -1336,7 +1339,7 @@ namespace API.Data.Migrations
|
|||||||
|
|
||||||
b.HasKey("Key");
|
b.HasKey("Key");
|
||||||
|
|
||||||
b.ToTable("ServerSetting", (string)null);
|
b.ToTable("ServerSetting");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("API.Entities.ServerStatistics", b =>
|
modelBuilder.Entity("API.Entities.ServerStatistics", b =>
|
||||||
@ -1374,7 +1377,7 @@ namespace API.Data.Migrations
|
|||||||
|
|
||||||
b.HasKey("Id");
|
b.HasKey("Id");
|
||||||
|
|
||||||
b.ToTable("ServerStatistics", (string)null);
|
b.ToTable("ServerStatistics");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("API.Entities.SiteTheme", b =>
|
modelBuilder.Entity("API.Entities.SiteTheme", b =>
|
||||||
@ -1412,7 +1415,7 @@ namespace API.Data.Migrations
|
|||||||
|
|
||||||
b.HasKey("Id");
|
b.HasKey("Id");
|
||||||
|
|
||||||
b.ToTable("SiteTheme", (string)null);
|
b.ToTable("SiteTheme");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("API.Entities.Tag", b =>
|
modelBuilder.Entity("API.Entities.Tag", b =>
|
||||||
@ -1432,7 +1435,7 @@ namespace API.Data.Migrations
|
|||||||
b.HasIndex("NormalizedTitle")
|
b.HasIndex("NormalizedTitle")
|
||||||
.IsUnique();
|
.IsUnique();
|
||||||
|
|
||||||
b.ToTable("Tag", (string)null);
|
b.ToTable("Tag");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("API.Entities.Volume", b =>
|
modelBuilder.Entity("API.Entities.Volume", b =>
|
||||||
@ -1484,7 +1487,7 @@ namespace API.Data.Migrations
|
|||||||
|
|
||||||
b.HasIndex("SeriesId");
|
b.HasIndex("SeriesId");
|
||||||
|
|
||||||
b.ToTable("Volume", (string)null);
|
b.ToTable("Volume");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("AppUserLibrary", b =>
|
modelBuilder.Entity("AppUserLibrary", b =>
|
||||||
@ -1499,7 +1502,7 @@ namespace API.Data.Migrations
|
|||||||
|
|
||||||
b.HasIndex("LibrariesId");
|
b.HasIndex("LibrariesId");
|
||||||
|
|
||||||
b.ToTable("AppUserLibrary", (string)null);
|
b.ToTable("AppUserLibrary");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("ChapterGenre", b =>
|
modelBuilder.Entity("ChapterGenre", b =>
|
||||||
@ -1514,7 +1517,7 @@ namespace API.Data.Migrations
|
|||||||
|
|
||||||
b.HasIndex("GenresId");
|
b.HasIndex("GenresId");
|
||||||
|
|
||||||
b.ToTable("ChapterGenre", (string)null);
|
b.ToTable("ChapterGenre");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("ChapterPerson", b =>
|
modelBuilder.Entity("ChapterPerson", b =>
|
||||||
@ -1529,7 +1532,7 @@ namespace API.Data.Migrations
|
|||||||
|
|
||||||
b.HasIndex("PeopleId");
|
b.HasIndex("PeopleId");
|
||||||
|
|
||||||
b.ToTable("ChapterPerson", (string)null);
|
b.ToTable("ChapterPerson");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("ChapterTag", b =>
|
modelBuilder.Entity("ChapterTag", b =>
|
||||||
@ -1544,7 +1547,7 @@ namespace API.Data.Migrations
|
|||||||
|
|
||||||
b.HasIndex("TagsId");
|
b.HasIndex("TagsId");
|
||||||
|
|
||||||
b.ToTable("ChapterTag", (string)null);
|
b.ToTable("ChapterTag");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("CollectionTagSeriesMetadata", b =>
|
modelBuilder.Entity("CollectionTagSeriesMetadata", b =>
|
||||||
@ -1559,7 +1562,7 @@ namespace API.Data.Migrations
|
|||||||
|
|
||||||
b.HasIndex("SeriesMetadatasId");
|
b.HasIndex("SeriesMetadatasId");
|
||||||
|
|
||||||
b.ToTable("CollectionTagSeriesMetadata", (string)null);
|
b.ToTable("CollectionTagSeriesMetadata");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("GenreSeriesMetadata", b =>
|
modelBuilder.Entity("GenreSeriesMetadata", b =>
|
||||||
@ -1574,7 +1577,7 @@ namespace API.Data.Migrations
|
|||||||
|
|
||||||
b.HasIndex("SeriesMetadatasId");
|
b.HasIndex("SeriesMetadatasId");
|
||||||
|
|
||||||
b.ToTable("GenreSeriesMetadata", (string)null);
|
b.ToTable("GenreSeriesMetadata");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<int>", b =>
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<int>", b =>
|
||||||
@ -1673,7 +1676,7 @@ namespace API.Data.Migrations
|
|||||||
|
|
||||||
b.HasIndex("SeriesMetadatasId");
|
b.HasIndex("SeriesMetadatasId");
|
||||||
|
|
||||||
b.ToTable("PersonSeriesMetadata", (string)null);
|
b.ToTable("PersonSeriesMetadata");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("SeriesMetadataTag", b =>
|
modelBuilder.Entity("SeriesMetadataTag", b =>
|
||||||
@ -1688,7 +1691,7 @@ namespace API.Data.Migrations
|
|||||||
|
|
||||||
b.HasIndex("TagsId");
|
b.HasIndex("TagsId");
|
||||||
|
|
||||||
b.ToTable("SeriesMetadataTag", (string)null);
|
b.ToTable("SeriesMetadataTag");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("API.Entities.AppUserBookmark", b =>
|
modelBuilder.Entity("API.Entities.AppUserBookmark", b =>
|
||||||
|
@ -625,6 +625,7 @@ public class SeriesRepository : ISeriesRepository
|
|||||||
if (rating != null)
|
if (rating != null)
|
||||||
{
|
{
|
||||||
s.UserRating = rating.Rating;
|
s.UserRating = rating.Rating;
|
||||||
|
s.HasUserRated = rating.HasBeenRated;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (userProgress.Count > 0)
|
if (userProgress.Count > 0)
|
||||||
@ -1686,13 +1687,13 @@ public class SeriesRepository : ISeriesRepository
|
|||||||
{
|
{
|
||||||
// If there is 0 or 1 rating and that rating is you, return 0 back
|
// If there is 0 or 1 rating and that rating is you, return 0 back
|
||||||
var countOfRatingsThatAreUser = await _context.AppUserRating
|
var countOfRatingsThatAreUser = await _context.AppUserRating
|
||||||
.Where(r => r.SeriesId == seriesId).CountAsync(u => u.AppUserId == userId);
|
.Where(r => r.SeriesId == seriesId && r.HasBeenRated).CountAsync(u => u.AppUserId == userId);
|
||||||
if (countOfRatingsThatAreUser == 1)
|
if (countOfRatingsThatAreUser == 1)
|
||||||
{
|
{
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
var avg = (await _context.AppUserRating
|
var avg = (await _context.AppUserRating
|
||||||
.Where(r => r.SeriesId == seriesId)
|
.Where(r => r.SeriesId == seriesId && r.HasBeenRated)
|
||||||
.AverageAsync(r => (int?) r.Rating));
|
.AverageAsync(r => (int?) r.Rating));
|
||||||
return avg.HasValue ? (int) (avg.Value * 20) : 0;
|
return avg.HasValue ? (int) (avg.Value * 20) : 0;
|
||||||
}
|
}
|
||||||
@ -1714,7 +1715,7 @@ public class SeriesRepository : ISeriesRepository
|
|||||||
public async Task ClearOnDeckRemoval(int seriesId, int userId)
|
public async Task ClearOnDeckRemoval(int seriesId, int userId)
|
||||||
{
|
{
|
||||||
var existingEntry = await _context.AppUserOnDeckRemoval
|
var existingEntry = await _context.AppUserOnDeckRemoval
|
||||||
.Where(u => u.Id == userId && u.SeriesId == seriesId)
|
.Where(u => u.AppUserId == userId && u.SeriesId == seriesId)
|
||||||
.FirstOrDefaultAsync();
|
.FirstOrDefaultAsync();
|
||||||
if (existingEntry == null) return;
|
if (existingEntry == null) return;
|
||||||
_context.AppUserOnDeckRemoval.Remove(existingEntry);
|
_context.AppUserOnDeckRemoval.Remove(existingEntry);
|
||||||
|
@ -1,13 +1,17 @@
|
|||||||
|
|
||||||
namespace API.Entities;
|
namespace API.Entities;
|
||||||
|
#nullable enable
|
||||||
public class AppUserRating
|
public class AppUserRating
|
||||||
{
|
{
|
||||||
public int Id { get; set; }
|
public int Id { get; set; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A number between 0-5 that represents how good a series is.
|
/// A number between 0-5.0 that represents how good a series is.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int Rating { get; set; }
|
public float Rating { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// If the rating has been explicitly set. Otherwise the 0.0 rating should be ignored as it's not rated
|
||||||
|
/// </summary>
|
||||||
|
public bool HasBeenRated { get; set; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A short summary the user can write when giving their review.
|
/// A short summary the user can write when giving their review.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -17,7 +21,7 @@ public class AppUserRating
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public string? Tagline { get; set; }
|
public string? Tagline { get; set; }
|
||||||
public int SeriesId { get; set; }
|
public int SeriesId { get; set; }
|
||||||
public Series Series { get; set; }
|
public Series Series { get; set; } = null!;
|
||||||
|
|
||||||
|
|
||||||
// Relationships
|
// Relationships
|
||||||
|
@ -39,7 +39,7 @@ public interface IScrobblingService
|
|||||||
{
|
{
|
||||||
Task CheckExternalAccessTokens();
|
Task CheckExternalAccessTokens();
|
||||||
Task<bool> HasTokenExpired(int userId, ScrobbleProvider provider);
|
Task<bool> HasTokenExpired(int userId, ScrobbleProvider provider);
|
||||||
Task ScrobbleRatingUpdate(int userId, int seriesId, int rating);
|
Task ScrobbleRatingUpdate(int userId, int seriesId, float rating);
|
||||||
Task ScrobbleReviewUpdate(int userId, int seriesId, string reviewTitle, string reviewBody);
|
Task ScrobbleReviewUpdate(int userId, int seriesId, string reviewTitle, string reviewBody);
|
||||||
Task ScrobbleReadingUpdate(int userId, int seriesId);
|
Task ScrobbleReadingUpdate(int userId, int seriesId);
|
||||||
Task ScrobbleWantToReadUpdate(int userId, int seriesId, bool onWantToRead);
|
Task ScrobbleWantToReadUpdate(int userId, int seriesId, bool onWantToRead);
|
||||||
@ -223,7 +223,7 @@ public class ScrobblingService : IScrobblingService
|
|||||||
_logger.LogDebug("Added Scrobbling Review update on {SeriesName} with Userid {UserId} ", series.Name, userId);
|
_logger.LogDebug("Added Scrobbling Review update on {SeriesName} with Userid {UserId} ", series.Name, userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task ScrobbleRatingUpdate(int userId, int seriesId, int rating)
|
public async Task ScrobbleRatingUpdate(int userId, int seriesId, float rating)
|
||||||
{
|
{
|
||||||
if (!await _licenseService.HasActiveLicense()) return;
|
if (!await _licenseService.HasActiveLicense()) return;
|
||||||
var token = await GetTokenForProvider(userId, ScrobbleProvider.AniList);
|
var token = await GetTokenForProvider(userId, ScrobbleProvider.AniList);
|
||||||
|
@ -294,7 +294,8 @@ public class SeriesService : ISeriesService
|
|||||||
new AppUserRating();
|
new AppUserRating();
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
userRating.Rating = Math.Clamp(updateSeriesRatingDto.UserRating, 0, 5);
|
userRating.Rating = Math.Clamp(updateSeriesRatingDto.UserRating, 0f, 5f);
|
||||||
|
userRating.HasBeenRated = true;
|
||||||
userRating.SeriesId = updateSeriesRatingDto.SeriesId;
|
userRating.SeriesId = updateSeriesRatingDto.SeriesId;
|
||||||
|
|
||||||
if (userRating.Id == 0)
|
if (userRating.Id == 0)
|
||||||
|
13
UI/Web/package-lock.json
generated
13
UI/Web/package-lock.json
generated
@ -36,6 +36,7 @@
|
|||||||
"ngx-extended-pdf-viewer": "^16.2.16",
|
"ngx-extended-pdf-viewer": "^16.2.16",
|
||||||
"ngx-file-drop": "^16.0.0",
|
"ngx-file-drop": "^16.0.0",
|
||||||
"ngx-slider-v2": "^16.0.2",
|
"ngx-slider-v2": "^16.0.2",
|
||||||
|
"ngx-stars": "^1.6.5",
|
||||||
"ngx-toastr": "^17.0.2",
|
"ngx-toastr": "^17.0.2",
|
||||||
"rxjs": "^7.8.0",
|
"rxjs": "^7.8.0",
|
||||||
"screenfull": "^6.0.2",
|
"screenfull": "^6.0.2",
|
||||||
@ -10564,6 +10565,18 @@
|
|||||||
"@angular/forms": "^16.0.0"
|
"@angular/forms": "^16.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/ngx-stars": {
|
||||||
|
"version": "1.6.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/ngx-stars/-/ngx-stars-1.6.5.tgz",
|
||||||
|
"integrity": "sha512-ZJ2R1XgIkBj5TsHSP8tl3QvbRBCi1awLO03Aod7ffDNG1i785ODw9gYlOAvsIrUmnY9ha1h21tTs5pBWXqA+5Q==",
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "^2.3.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@angular/common": ">=2.0.0",
|
||||||
|
"@angular/core": ">=2.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ngx-toastr": {
|
"node_modules/ngx-toastr": {
|
||||||
"version": "17.0.2",
|
"version": "17.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/ngx-toastr/-/ngx-toastr-17.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/ngx-toastr/-/ngx-toastr-17.0.2.tgz",
|
||||||
|
@ -40,6 +40,7 @@
|
|||||||
"ngx-extended-pdf-viewer": "^16.2.16",
|
"ngx-extended-pdf-viewer": "^16.2.16",
|
||||||
"ngx-file-drop": "^16.0.0",
|
"ngx-file-drop": "^16.0.0",
|
||||||
"ngx-slider-v2": "^16.0.2",
|
"ngx-slider-v2": "^16.0.2",
|
||||||
|
"ngx-stars": "^1.6.5",
|
||||||
"ngx-toastr": "^17.0.2",
|
"ngx-toastr": "^17.0.2",
|
||||||
"rxjs": "^7.8.0",
|
"rxjs": "^7.8.0",
|
||||||
"screenfull": "^6.0.2",
|
"screenfull": "^6.0.2",
|
||||||
|
@ -27,6 +27,7 @@ export interface Series {
|
|||||||
* User's rating (0-5)
|
* User's rating (0-5)
|
||||||
*/
|
*/
|
||||||
userRating: number;
|
userRating: number;
|
||||||
|
hasUserRated: boolean;
|
||||||
libraryId: number;
|
libraryId: number;
|
||||||
/**
|
/**
|
||||||
* DateTime the entity was created
|
* DateTime the entity was created
|
||||||
|
@ -9,16 +9,16 @@
|
|||||||
|
|
||||||
<div class="offcanvas-body pb-3">
|
<div class="offcanvas-body pb-3">
|
||||||
<div class="d-flex">
|
<div class="d-flex">
|
||||||
<ul ngbNav #nav="ngbNav" [(activeId)]="active" class="nav-pills" orientation="vertical" style="max-width: 135px;">
|
<ul ngbNav #nav="ngbNav" [(activeId)]="active" class="nav-pills" orientation="vertical" style="max-width: 135px;">
|
||||||
<li [ngbNavItem]="tabs[TabID.General]">
|
<li [ngbNavItem]="tabs[TabID.General]">
|
||||||
<a ngbNavLink>General</a>
|
<a ngbNavLink>General</a>
|
||||||
<ng-template ngbNavContent>
|
<ng-template ngbNavContent>
|
||||||
<div class="container-fluid" style="overflow: auto">
|
<div class="container-fluid" style="overflow: auto">
|
||||||
|
|
||||||
<div class="row g-0">
|
<div class="row g-0">
|
||||||
<div class="d-none d-md-block col-md-2 col-lg-1">
|
<div class="d-none d-md-block col-md-2 col-lg-1">
|
||||||
<app-image class="me-2" width="74px" [imageUrl]="coverImageUrl"></app-image>
|
<app-image class="me-2" width="74px" [imageUrl]="coverImageUrl"></app-image>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-10 col-lg-11">
|
<div class="col-md-10 col-lg-11">
|
||||||
<ng-container *ngIf="summary.length > 0; else noSummary">
|
<ng-container *ngIf="summary.length > 0; else noSummary">
|
||||||
<app-read-more [text]="summary" [maxLength]="250"></app-read-more>
|
<app-read-more [text]="summary" [maxLength]="250"></app-read-more>
|
||||||
@ -28,8 +28,8 @@
|
|||||||
</ng-template>
|
</ng-template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<app-entity-info-cards [entity]="data"></app-entity-info-cards>
|
<app-entity-info-cards [entity]="data" [libraryId]="libraryId"></app-entity-info-cards>
|
||||||
|
|
||||||
|
|
||||||
<!-- 2 rows to show some tags-->
|
<!-- 2 rows to show some tags-->
|
||||||
@ -41,7 +41,7 @@
|
|||||||
<app-badge-expander [items]="chapterMetadata.writers">
|
<app-badge-expander [items]="chapterMetadata.writers">
|
||||||
<ng-template #badgeExpanderItem let-item let-position="idx">
|
<ng-template #badgeExpanderItem let-item let-position="idx">
|
||||||
<app-person-badge [person]="item"></app-person-badge>
|
<app-person-badge [person]="item"></app-person-badge>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</app-badge-expander>
|
</app-badge-expander>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</div>
|
</div>
|
||||||
@ -51,7 +51,7 @@
|
|||||||
<app-badge-expander [items]="chapterMetadata.genres">
|
<app-badge-expander [items]="chapterMetadata.genres">
|
||||||
<ng-template #badgeExpanderItem let-item let-position="idx">
|
<ng-template #badgeExpanderItem let-item let-position="idx">
|
||||||
<app-tag-badge>{{item.title}}</app-tag-badge>
|
<app-tag-badge>{{item.title}}</app-tag-badge>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</app-badge-expander>
|
</app-badge-expander>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</div>
|
</div>
|
||||||
@ -63,7 +63,7 @@
|
|||||||
<app-badge-expander [items]="chapterMetadata.publishers">
|
<app-badge-expander [items]="chapterMetadata.publishers">
|
||||||
<ng-template #badgeExpanderItem let-item let-position="idx">
|
<ng-template #badgeExpanderItem let-item let-position="idx">
|
||||||
<app-person-badge [person]="item"></app-person-badge>
|
<app-person-badge [person]="item"></app-person-badge>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</app-badge-expander>
|
</app-badge-expander>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</div>
|
</div>
|
||||||
@ -73,7 +73,7 @@
|
|||||||
<app-badge-expander [items]="chapterMetadata.tags">
|
<app-badge-expander [items]="chapterMetadata.tags">
|
||||||
<ng-template #badgeExpanderItem let-item let-position="idx">
|
<ng-template #badgeExpanderItem let-item let-position="idx">
|
||||||
<app-tag-badge>{{item.title}}</app-tag-badge>
|
<app-tag-badge>{{item.title}}</app-tag-badge>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</app-badge-expander>
|
</app-badge-expander>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</div>
|
</div>
|
||||||
@ -98,7 +98,7 @@
|
|||||||
<li [ngbNavItem]="tabs[TabID.Cover]" [disabled]="(isAdmin$ | async) === false">
|
<li [ngbNavItem]="tabs[TabID.Cover]" [disabled]="(isAdmin$ | async) === false">
|
||||||
<a ngbNavLink>{{tabs[TabID.Cover].title}}</a>
|
<a ngbNavLink>{{tabs[TabID.Cover].title}}</a>
|
||||||
<ng-template ngbNavContent>
|
<ng-template ngbNavContent>
|
||||||
<app-cover-image-chooser [(imageUrls)]="imageUrls"
|
<app-cover-image-chooser [(imageUrls)]="imageUrls"
|
||||||
[showReset]="chapter.coverImageLocked"
|
[showReset]="chapter.coverImageLocked"
|
||||||
[showApplyButton]="true"
|
[showApplyButton]="true"
|
||||||
(applyCover)="applyCoverImage($event)"
|
(applyCover)="applyCoverImage($event)"
|
||||||
@ -121,7 +121,7 @@
|
|||||||
<h5 class="mt-0 mb-1">
|
<h5 class="mt-0 mb-1">
|
||||||
<span >
|
<span >
|
||||||
<span>
|
<span>
|
||||||
<app-card-actionables (actionHandler)="performAction($event, chapter)" [actions]="chapterActions"
|
<app-card-actionables (actionHandler)="performAction($event, chapter)" [actions]="chapterActions"
|
||||||
[labelBy]="utilityService.formatChapterName(libraryType, true, true) + formatChapterNumber(chapter)"></app-card-actionables>
|
[labelBy]="utilityService.formatChapterName(libraryType, true, true) + formatChapterNumber(chapter)"></app-card-actionables>
|
||||||
<ng-container *ngIf="chapter.number !== '0'; else specialHeader">
|
<ng-container *ngIf="chapter.number !== '0'; else specialHeader">
|
||||||
{{utilityService.formatChapterName(libraryType, true, false) }} {{formatChapterNumber(chapter)}}
|
{{utilityService.formatChapterName(libraryType, true, false) }} {{formatChapterNumber(chapter)}}
|
||||||
@ -143,7 +143,7 @@
|
|||||||
Pages: {{file.pages | number:''}}
|
Pages: {{file.pages | number:''}}
|
||||||
</div>
|
</div>
|
||||||
<div class="col" *ngIf="data.hasOwnProperty('created')">
|
<div class="col" *ngIf="data.hasOwnProperty('created')">
|
||||||
Added:
|
Added:
|
||||||
<!-- TODO: This data.created can be removed after v0.5.5 release -->
|
<!-- TODO: This data.created can be removed after v0.5.5 release -->
|
||||||
<ng-container *ngIf="file.created === '0001-01-01T00:00:00'; else fileDate">
|
<ng-container *ngIf="file.created === '0001-01-01T00:00:00'; else fileDate">
|
||||||
{{data.created | date: 'short' | defaultDate}}
|
{{data.created | date: 'short' | defaultDate}}
|
||||||
@ -166,4 +166,4 @@
|
|||||||
</ul>
|
</ul>
|
||||||
<div [ngbNavOutlet]="nav" class="tab-content {{utilityService.getActiveBreakpoint() === Breakpoint.Mobile ? 'mt-3' : 'ms-4 flex-fill'}}"></div>
|
<div [ngbNavOutlet]="nav" class="tab-content {{utilityService.getActiveBreakpoint() === Breakpoint.Mobile ? 'mt-3' : 'ms-4 flex-fill'}}"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,107 +1,121 @@
|
|||||||
<div class="row g-0 mt-4 mb-3">
|
|
||||||
<ng-container *ngIf="chapter !== undefined && chapter.releaseDate && (chapter.releaseDate | date: 'shortDate') !== '1/1/01'">
|
|
||||||
<div class="col-auto mb-2">
|
|
||||||
<app-icon-and-title label="Release Date" [clickable]="false" fontClasses="fa-regular fa-calendar" title="Release">
|
|
||||||
{{chapter.releaseDate | date:'shortDate' | defaultDate}}
|
|
||||||
</app-icon-and-title>
|
|
||||||
</div>
|
|
||||||
<div class="vr d-none d-lg-block m-2"></div>
|
|
||||||
</ng-container>
|
|
||||||
|
|
||||||
<ng-container *ngIf="chapter.ageRating !== AgeRating.Unknown">
|
<div class="mt-4 mb-3">
|
||||||
<div class="col-auto mb-2">
|
<div class="row g-0" *ngIf="chapterMetadata ">
|
||||||
<app-icon-and-title label="Age Rating" [clickable]="false" fontClasses="fas fa-eye" title="Age Rating">
|
<!-- Tags and Characters are used a lot of Hentai and Doujinshi type content, so showing in list item has value add on first glance -->
|
||||||
{{chapter.ageRating | ageRating | async}}
|
<app-metadata-detail [tags]="chapterMetadata.tags" [libraryId]="libraryId" [queryParam]="FilterQueryParam.Tags" heading="Tags">
|
||||||
</app-icon-and-title>
|
<ng-template #titleTemplate let-item>{{item.title}}</ng-template>
|
||||||
</div>
|
</app-metadata-detail>
|
||||||
<div class="vr d-none d-lg-block m-2"></div>
|
|
||||||
</ng-container>
|
|
||||||
|
|
||||||
<ng-container *ngIf="totalPages > 0">
|
<app-metadata-detail [tags]="chapterMetadata.characters" [libraryId]="libraryId" [queryParam]="FilterQueryParam.Character" heading="Characters">
|
||||||
<div class="col-auto mb-2">
|
<ng-template #titleTemplate let-item>{{item.title}}</ng-template>
|
||||||
<app-icon-and-title label="Length" [clickable]="false" fontClasses="fa-regular fa-file-lines" title="Pages">
|
</app-metadata-detail>
|
||||||
{{totalPages | compactNumber}} Pages
|
</div>
|
||||||
</app-icon-and-title>
|
|
||||||
</div>
|
|
||||||
<div class="vr d-none d-lg-block m-2"></div>
|
|
||||||
</ng-container>
|
|
||||||
|
|
||||||
<ng-container *ngIf="chapter.files[0].format === MangaFormat.EPUB && totalWordCount > 0">
|
<div class="row g-0">
|
||||||
<div class="col-auto mb-2">
|
<ng-container *ngIf="chapter !== undefined && chapter.releaseDate && (chapter.releaseDate | date: 'shortDate') !== '1/1/01'">
|
||||||
<app-icon-and-title label="Length" [clickable]="false" fontClasses="fa-solid fa-book-open">
|
<div class="col-auto mb-2">
|
||||||
{{totalWordCount | compactNumber}} Words
|
<app-icon-and-title label="Release Date" [clickable]="false" fontClasses="fa-regular fa-calendar" title="Release">
|
||||||
</app-icon-and-title>
|
{{chapter.releaseDate | date:'shortDate' | defaultDate}}
|
||||||
</div>
|
</app-icon-and-title>
|
||||||
<div class="vr d-none d-lg-block m-2"></div>
|
</div>
|
||||||
</ng-container>
|
<div class="vr d-none d-lg-block m-2"></div>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
<ng-container *ngIf="chapter.files[0].format === MangaFormat.EPUB && totalWordCount > 0 || chapter.files[0].format !== MangaFormat.EPUB">
|
<ng-container *ngIf="chapter.ageRating !== AgeRating.Unknown">
|
||||||
<div class="col-auto mb-2">
|
<div class="col-auto mb-2">
|
||||||
<app-icon-and-title label="Read Time" [clickable]="false" fontClasses="fa-regular fa-clock">
|
<app-icon-and-title label="Age Rating" [clickable]="false" fontClasses="fas fa-eye" title="Age Rating">
|
||||||
<ng-container *ngIf="readingTime.maxHours === 0 || readingTime.minHours === 0; else normalReadTime"><1 Hour</ng-container>
|
{{chapter.ageRating | ageRating | async}}
|
||||||
<ng-template #normalReadTime>
|
</app-icon-and-title>
|
||||||
{{readingTime.minHours}}{{readingTime.maxHours !== readingTime.minHours ? ('-' + readingTime.maxHours) : ''}} Hour{{readingTime.minHours > 1 ? 's' : ''}}
|
</div>
|
||||||
</ng-template>
|
<div class="vr d-none d-lg-block m-2"></div>
|
||||||
</app-icon-and-title>
|
</ng-container>
|
||||||
</div>
|
|
||||||
</ng-container>
|
|
||||||
|
|
||||||
<ng-container *ngIf="showExtendedProperties && chapter.created && chapter.created !== '' && (chapter.created | date: 'shortDate') !== '1/1/01'">
|
<ng-container *ngIf="totalPages > 0">
|
||||||
<div class="vr d-none d-lg-block m-2"></div>
|
<div class="col-auto mb-2">
|
||||||
<div class="col-auto">
|
<app-icon-and-title label="Length" [clickable]="false" fontClasses="fa-regular fa-file-lines" title="Pages">
|
||||||
<app-icon-and-title label="Date Added" [clickable]="false" fontClasses="fa-solid fa-file-import" title="Date Added">
|
{{totalPages | compactNumber}} Pages
|
||||||
{{chapter.created | date:'short' | defaultDate}}
|
</app-icon-and-title>
|
||||||
</app-icon-and-title>
|
</div>
|
||||||
</div>
|
<div class="vr d-none d-lg-block m-2"></div>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<ng-container *ngIf="showExtendedProperties && size > 0">
|
<ng-container *ngIf="chapter.files[0].format === MangaFormat.EPUB && totalWordCount > 0">
|
||||||
<div class="vr d-none d-lg-block m-2"></div>
|
<div class="col-auto mb-2">
|
||||||
<div class="col-auto">
|
<app-icon-and-title label="Length" [clickable]="false" fontClasses="fa-solid fa-book-open">
|
||||||
<app-icon-and-title label="Size" [clickable]="false" fontClasses="fa-solid fa-scale-unbalanced" title="ID">
|
{{totalWordCount | compactNumber}} Words
|
||||||
{{size | bytes}}
|
</app-icon-and-title>
|
||||||
</app-icon-and-title>
|
</div>
|
||||||
</div>
|
<div class="vr d-none d-lg-block m-2"></div>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<ng-container *ngIf="showExtendedProperties">
|
<ng-container *ngIf="chapter.files[0].format === MangaFormat.EPUB && totalWordCount > 0 || chapter.files[0].format !== MangaFormat.EPUB">
|
||||||
<div class="vr d-none d-lg-block m-2"></div>
|
<div class="col-auto mb-2">
|
||||||
<div class="col-auto">
|
<app-icon-and-title label="Read Time" [clickable]="false" fontClasses="fa-regular fa-clock">
|
||||||
<app-icon-and-title label="ID" [clickable]="false" fontClasses="fa-solid fa-fingerprint" title="ID">
|
<ng-container *ngIf="readingTime.maxHours === 0 || readingTime.minHours === 0; else normalReadTime"><1 Hour</ng-container>
|
||||||
{{entity.id}}
|
<ng-template #normalReadTime>
|
||||||
</app-icon-and-title>
|
{{readingTime.minHours}}{{readingTime.maxHours !== readingTime.minHours ? ('-' + readingTime.maxHours) : ''}} Hour{{readingTime.minHours > 1 ? 's' : ''}}
|
||||||
</div>
|
</ng-template>
|
||||||
<ng-container *ngIf="WebLinks.length > 0">
|
</app-icon-and-title>
|
||||||
<div class="vr d-none d-lg-block m-2"></div>
|
</div>
|
||||||
<div class="col-auto">
|
</ng-container>
|
||||||
<app-icon-and-title label="Links" [clickable]="false" fontClasses="fa-solid fa-link" title="Links">
|
|
||||||
<a class="me-1" [href]="link | safeHtml" *ngFor="let link of WebLinks" target="_blank" rel="noopener noreferrer" [title]="link">
|
|
||||||
<img width="24" height="24" #img class="lazyload img-placeholder"
|
|
||||||
src="data:image/gif;base64,R0lGODlhAQABAPAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw=="
|
|
||||||
[attr.data-src]="imageService.getWebLinkImage(link)"
|
|
||||||
(error)="imageService.updateErroredWebLinkImage($event)"
|
|
||||||
aria-hidden="true" alt="">
|
|
||||||
</a>
|
|
||||||
</app-icon-and-title>
|
|
||||||
</div>
|
|
||||||
</ng-container>
|
|
||||||
|
|
||||||
<ng-container *ngIf="chapter.isbn.length > 0">
|
<ng-container *ngIf="showExtendedProperties && chapter.created && chapter.created !== '' && (chapter.created | date: 'shortDate') !== '1/1/01'">
|
||||||
<div class="vr d-none d-lg-block m-2"></div>
|
|
||||||
<div class="col-auto">
|
|
||||||
<app-icon-and-title label="ISBN" [clickable]="false" fontClasses="fa-solid fa-barcode" title="ISBN">
|
|
||||||
{{chapter.isbn}}
|
|
||||||
</app-icon-and-title>
|
|
||||||
</div>
|
|
||||||
</ng-container>
|
|
||||||
|
|
||||||
<ng-container *ngIf="(chapter.lastReadingProgress | date: 'shortDate') !== '1/1/01'">
|
|
||||||
<div class="vr d-none d-lg-block m-2"></div>
|
<div class="vr d-none d-lg-block m-2"></div>
|
||||||
<div class="col-auto">
|
<div class="col-auto">
|
||||||
<app-icon-and-title label="Last Read" [clickable]="false" fontClasses="fa-regular fa-clock" [ngbTooltip]="chapter.lastReadingProgress | date: 'medium'">
|
<app-icon-and-title label="Date Added" [clickable]="false" fontClasses="fa-solid fa-file-import" title="Date Added">
|
||||||
{{chapter.lastReadingProgress | date: 'shortDate'}}
|
{{chapter.created | date:'short' | defaultDate}}
|
||||||
</app-icon-and-title>
|
</app-icon-and-title>
|
||||||
</div>
|
</div>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</ng-container>
|
|
||||||
|
<ng-container *ngIf="showExtendedProperties && size > 0">
|
||||||
|
<div class="vr d-none d-lg-block m-2"></div>
|
||||||
|
<div class="col-auto">
|
||||||
|
<app-icon-and-title label="Size" [clickable]="false" fontClasses="fa-solid fa-scale-unbalanced" title="ID">
|
||||||
|
{{size | bytes}}
|
||||||
|
</app-icon-and-title>
|
||||||
|
</div>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container *ngIf="showExtendedProperties">
|
||||||
|
<div class="vr d-none d-lg-block m-2"></div>
|
||||||
|
<div class="col-auto">
|
||||||
|
<app-icon-and-title label="ID" [clickable]="false" fontClasses="fa-solid fa-fingerprint" title="ID">
|
||||||
|
{{entity.id}}
|
||||||
|
</app-icon-and-title>
|
||||||
|
</div>
|
||||||
|
<ng-container *ngIf="WebLinks.length > 0">
|
||||||
|
<div class="vr d-none d-lg-block m-2"></div>
|
||||||
|
<div class="col-auto">
|
||||||
|
<app-icon-and-title label="Links" [clickable]="false" fontClasses="fa-solid fa-link" title="Links">
|
||||||
|
<a class="me-1" [href]="link | safeHtml" *ngFor="let link of WebLinks" target="_blank" rel="noopener noreferrer" [title]="link">
|
||||||
|
<img width="24" height="24" #img class="lazyload img-placeholder"
|
||||||
|
src="data:image/gif;base64,R0lGODlhAQABAPAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw=="
|
||||||
|
[attr.data-src]="imageService.getWebLinkImage(link)"
|
||||||
|
(error)="imageService.updateErroredWebLinkImage($event)"
|
||||||
|
aria-hidden="true" alt="">
|
||||||
|
</a>
|
||||||
|
</app-icon-and-title>
|
||||||
|
</div>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container *ngIf="chapter.isbn.length > 0">
|
||||||
|
<div class="vr d-none d-lg-block m-2"></div>
|
||||||
|
<div class="col-auto">
|
||||||
|
<app-icon-and-title label="ISBN" [clickable]="false" fontClasses="fa-solid fa-barcode" title="ISBN">
|
||||||
|
{{chapter.isbn}}
|
||||||
|
</app-icon-and-title>
|
||||||
|
</div>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container *ngIf="(chapter.lastReadingProgress | date: 'shortDate') !== '1/1/01'">
|
||||||
|
<div class="vr d-none d-lg-block m-2"></div>
|
||||||
|
<div class="col-auto">
|
||||||
|
<app-icon-and-title label="Last Read" [clickable]="false" fontClasses="fa-regular fa-clock" [ngbTooltip]="chapter.lastReadingProgress | date: 'medium'">
|
||||||
|
{{chapter.lastReadingProgress | date: 'shortDate'}}
|
||||||
|
</app-icon-and-title>
|
||||||
|
</div>
|
||||||
|
</ng-container>
|
||||||
|
</ng-container>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -24,11 +24,13 @@ import {BytesPipe} from "../../pipe/bytes.pipe";
|
|||||||
import {CompactNumberPipe} from "../../pipe/compact-number.pipe";
|
import {CompactNumberPipe} from "../../pipe/compact-number.pipe";
|
||||||
import {AgeRatingPipe} from "../../pipe/age-rating.pipe";
|
import {AgeRatingPipe} from "../../pipe/age-rating.pipe";
|
||||||
import {NgbTooltip} from "@ng-bootstrap/ng-bootstrap";
|
import {NgbTooltip} from "@ng-bootstrap/ng-bootstrap";
|
||||||
|
import {MetadataDetailComponent} from "../../series-detail/_components/metadata-detail/metadata-detail.component";
|
||||||
|
import {FilterQueryParam} from "../../shared/_services/filter-utilities.service";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-entity-info-cards',
|
selector: 'app-entity-info-cards',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [CommonModule, IconAndTitleComponent, SafeHtmlPipe, DefaultDatePipe, BytesPipe, CompactNumberPipe, AgeRatingPipe, NgbTooltip],
|
imports: [CommonModule, IconAndTitleComponent, SafeHtmlPipe, DefaultDatePipe, BytesPipe, CompactNumberPipe, AgeRatingPipe, NgbTooltip, MetadataDetailComponent],
|
||||||
templateUrl: './entity-info-cards.component.html',
|
templateUrl: './entity-info-cards.component.html',
|
||||||
styleUrls: ['./entity-info-cards.component.scss'],
|
styleUrls: ['./entity-info-cards.component.scss'],
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush
|
changeDetection: ChangeDetectionStrategy.OnPush
|
||||||
@ -36,6 +38,7 @@ import {NgbTooltip} from "@ng-bootstrap/ng-bootstrap";
|
|||||||
export class EntityInfoCardsComponent implements OnInit {
|
export class EntityInfoCardsComponent implements OnInit {
|
||||||
|
|
||||||
@Input({required: true}) entity!: Volume | Chapter;
|
@Input({required: true}) entity!: Volume | Chapter;
|
||||||
|
@Input({required: true}) libraryId!: number;
|
||||||
/**
|
/**
|
||||||
* This will pull extra information
|
* This will pull extra information
|
||||||
*/
|
*/
|
||||||
@ -75,8 +78,6 @@ export class EntityInfoCardsComponent implements OnInit {
|
|||||||
return this.chapter.webLinks.split(',');
|
return this.chapter.webLinks.split(',');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
constructor(private utilityService: UtilityService, private seriesService: SeriesService, private readonly cdRef: ChangeDetectorRef) {}
|
constructor(private utilityService: UtilityService, private seriesService: SeriesService, private readonly cdRef: ChangeDetectorRef) {}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
@ -127,8 +128,5 @@ export class EntityInfoCardsComponent implements OnInit {
|
|||||||
this.cdRef.markForCheck();
|
this.cdRef.markForCheck();
|
||||||
}
|
}
|
||||||
|
|
||||||
getTimezone(timezone: string): string {
|
protected readonly FilterQueryParam = FilterQueryParam;
|
||||||
const localDate = new Date(timezone);
|
|
||||||
return localDate.toLocaleString('en-US', { timeZoneName: 'short' }).split(' ')[3];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -22,7 +22,7 @@
|
|||||||
<span class="d-none d-sm-inline-block">Read</span>
|
<span class="d-none d-sm-inline-block">Read</span>
|
||||||
</button>
|
</button>
|
||||||
</h5>
|
</h5>
|
||||||
<!-- This isn't perfect, but it might work. TODO: Polish this-->
|
|
||||||
<h6 class="text-muted" [ngClass]="{'subtitle-with-actionables' : actions.length > 0}" *ngIf="Title !== '' && showTitle">{{Title}}</h6>
|
<h6 class="text-muted" [ngClass]="{'subtitle-with-actionables' : actions.length > 0}" *ngIf="Title !== '' && showTitle">{{Title}}</h6>
|
||||||
<ng-container *ngIf="summary.length > 0">
|
<ng-container *ngIf="summary.length > 0">
|
||||||
<div class="mt-2 ps-2">
|
<div class="mt-2 ps-2">
|
||||||
@ -30,7 +30,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<div class="ps-2 d-none d-md-inline-block">
|
<div class="ps-2 d-none d-md-inline-block">
|
||||||
<app-entity-info-cards [entity]="entity" [showExtendedProperties]="false"></app-entity-info-cards>
|
<app-entity-info-cards [entity]="entity" [libraryId]="libraryId" [includeMetadata]="ShowExtended" [showExtendedProperties]="ShowExtended"></app-entity-info-cards>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -5,15 +5,14 @@ import {
|
|||||||
EventEmitter,
|
EventEmitter,
|
||||||
inject,
|
inject,
|
||||||
Input,
|
Input,
|
||||||
OnDestroy,
|
|
||||||
OnInit,
|
OnInit,
|
||||||
Output
|
Output
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { ToastrService } from 'ngx-toastr';
|
import { ToastrService } from 'ngx-toastr';
|
||||||
import { map, Observable, Subject, takeUntil } from 'rxjs';
|
import { map, Observable } from 'rxjs';
|
||||||
import { Download } from 'src/app/shared/_models/download';
|
import { Download } from 'src/app/shared/_models/download';
|
||||||
import { DownloadEvent, DownloadService } from 'src/app/shared/_services/download.service';
|
import { DownloadEvent, DownloadService } from 'src/app/shared/_services/download.service';
|
||||||
import { UtilityService } from 'src/app/shared/_services/utility.service';
|
import {Breakpoint, UtilityService} from 'src/app/shared/_services/utility.service';
|
||||||
import { Chapter } from 'src/app/_models/chapter';
|
import { Chapter } from 'src/app/_models/chapter';
|
||||||
import { LibraryType } from 'src/app/_models/library';
|
import { LibraryType } from 'src/app/_models/library';
|
||||||
import { RelationKind } from 'src/app/_models/series-detail/relation-kind';
|
import { RelationKind } from 'src/app/_models/series-detail/relation-kind';
|
||||||
@ -42,6 +41,7 @@ export class ListItemComponent implements OnInit {
|
|||||||
* Volume or Chapter to render
|
* Volume or Chapter to render
|
||||||
*/
|
*/
|
||||||
@Input({required: true}) entity!: Volume | Chapter;
|
@Input({required: true}) entity!: Volume | Chapter;
|
||||||
|
@Input({required: true}) libraryId!: number;
|
||||||
/**
|
/**
|
||||||
* Image to show
|
* Image to show
|
||||||
*/
|
*/
|
||||||
@ -103,8 +103,14 @@ export class ListItemComponent implements OnInit {
|
|||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get ShowExtended() {
|
||||||
|
return this.utilityService.getActiveBreakpoint() === Breakpoint.Desktop;
|
||||||
|
}
|
||||||
|
|
||||||
constructor(private utilityService: UtilityService, private downloadService: DownloadService,
|
protected readonly Breakpoint = Breakpoint;
|
||||||
|
|
||||||
|
|
||||||
|
constructor(public utilityService: UtilityService, private downloadService: DownloadService,
|
||||||
private toastr: ToastrService, private readonly cdRef: ChangeDetectorRef) { }
|
private toastr: ToastrService, private readonly cdRef: ChangeDetectorRef) { }
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
|
@ -3,9 +3,10 @@
|
|||||||
popoverTitle="Your Rating + Overall" popoverClass="md-popover">
|
popoverTitle="Your Rating + Overall" popoverClass="md-popover">
|
||||||
<span class="badge rounded-pill me-1">
|
<span class="badge rounded-pill me-1">
|
||||||
<img class="me-1" ngSrc="assets/images/logo-32.png" width="24" height="24" alt="">
|
<img class="me-1" ngSrc="assets/images/logo-32.png" width="24" height="24" alt="">
|
||||||
{{userRating * 20}}
|
<ng-container *ngIf="hasUserRated; else notYetRated">{{userRating * 20}}</ng-container>
|
||||||
<ng-container *ngIf="overallRating > 0; else noOverallRating"> + {{overallRating}}%</ng-container>
|
<ng-template #notYetRated>N/A</ng-template>
|
||||||
<ng-template #noOverallRating>%</ng-template>
|
<ng-container *ngIf="overallRating > 0"> + {{overallRating}}</ng-container>
|
||||||
|
<ng-container *ngIf="hasUserRated || overallRating > 0">%</ng-container>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -22,11 +23,9 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ng-template #popContent>
|
<ng-template #popContent>
|
||||||
<ngb-rating class="rating-star" [(rate)]="userRating" (rateChange)="updateRating($event)" [resettable]="false">
|
<ngx-stars [initialStars]="userRating" (ratingOutput)="updateRating($event)"
|
||||||
<ng-template let-fill="fill" let-index="index">
|
[maxStars]="5" [color]="starColor"></ngx-stars>
|
||||||
<span class="star" [class.filled]="(index < userRating) && userRating > 0">★</span>
|
{{userRating * 20}}%
|
||||||
</ng-template>
|
|
||||||
</ngb-rating> {{userRating * 20}}%
|
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
|
||||||
<ng-template #externalPopContent let-rating="rating">
|
<ng-template #externalPopContent let-rating="rating">
|
||||||
|
@ -19,4 +19,19 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.rating-star {
|
||||||
|
i {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
padding-right: 0.1rem;
|
||||||
|
color: #d3d3d3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filled {
|
||||||
|
color: var(--primary-color);
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
::ng-deep .star {
|
||||||
|
background-color: var(--primary-color);
|
||||||
|
}
|
||||||
|
@ -16,11 +16,13 @@ import {LoadingComponent} from "../../../shared/loading/loading.component";
|
|||||||
import {AccountService} from "../../../_services/account.service";
|
import {AccountService} from "../../../_services/account.service";
|
||||||
import {LibraryType} from "../../../_models/library";
|
import {LibraryType} from "../../../_models/library";
|
||||||
import {ProviderNamePipe} from "../../../pipe/provider-name.pipe";
|
import {ProviderNamePipe} from "../../../pipe/provider-name.pipe";
|
||||||
|
import {NgxStarsModule} from "ngx-stars";
|
||||||
|
import {ThemeService} from "../../../_services/theme.service";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-external-rating',
|
selector: 'app-external-rating',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [CommonModule, ProviderImagePipe, NgOptimizedImage, NgbRating, NgbPopover, LoadingComponent, ProviderNamePipe],
|
imports: [CommonModule, ProviderImagePipe, NgOptimizedImage, NgbRating, NgbPopover, LoadingComponent, ProviderNamePipe, NgxStarsModule],
|
||||||
templateUrl: './external-rating.component.html',
|
templateUrl: './external-rating.component.html',
|
||||||
styleUrls: ['./external-rating.component.scss'],
|
styleUrls: ['./external-rating.component.scss'],
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
@ -29,15 +31,19 @@ import {ProviderNamePipe} from "../../../pipe/provider-name.pipe";
|
|||||||
export class ExternalRatingComponent implements OnInit {
|
export class ExternalRatingComponent implements OnInit {
|
||||||
@Input({required: true}) seriesId!: number;
|
@Input({required: true}) seriesId!: number;
|
||||||
@Input({required: true}) userRating!: number;
|
@Input({required: true}) userRating!: number;
|
||||||
|
@Input({required: true}) hasUserRated!: boolean;
|
||||||
@Input({required: true}) libraryType!: LibraryType;
|
@Input({required: true}) libraryType!: LibraryType;
|
||||||
private readonly cdRef = inject(ChangeDetectorRef);
|
private readonly cdRef = inject(ChangeDetectorRef);
|
||||||
private readonly seriesService = inject(SeriesService);
|
private readonly seriesService = inject(SeriesService);
|
||||||
private readonly accountService = inject(AccountService);
|
private readonly accountService = inject(AccountService);
|
||||||
|
private readonly themeService = inject(ThemeService);
|
||||||
|
|
||||||
ratings: Array<Rating> = [];
|
ratings: Array<Rating> = [];
|
||||||
isLoading: boolean = false;
|
isLoading: boolean = false;
|
||||||
overallRating: number = -1;
|
overallRating: number = -1;
|
||||||
|
|
||||||
|
starColor = this.themeService.getCssVariable('--rating-star-color');
|
||||||
|
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
|
|
||||||
@ -58,9 +64,11 @@ export class ExternalRatingComponent implements OnInit {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
updateRating(rating: any) {
|
updateRating(rating: number) {
|
||||||
this.seriesService.updateRating(this.seriesId, rating).subscribe(() => {
|
this.seriesService.updateRating(this.seriesId, rating).subscribe(() => {
|
||||||
this.userRating = rating;
|
this.userRating = rating;
|
||||||
|
this.hasUserRated = true;
|
||||||
|
this.cdRef.markForCheck();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,21 @@
|
|||||||
|
<div class="row g-0 mb-1" *ngIf="tags.length > 0">
|
||||||
|
<div class="col-md-4">
|
||||||
|
<h5>{{heading}}</h5>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-8">
|
||||||
|
<app-badge-expander [items]="tags">
|
||||||
|
<ng-template #badgeExpanderItem let-item let-position="idx">
|
||||||
|
<ng-container *ngIf="itemTemplate; else useTitle">
|
||||||
|
<span (click)="goTo(queryParam, item.id)">
|
||||||
|
<ng-container [ngTemplateOutlet]="itemTemplate" [ngTemplateOutletContext]="{ $implicit: item, idx: position }"></ng-container>
|
||||||
|
</span>
|
||||||
|
</ng-container>
|
||||||
|
<ng-template #useTitle>
|
||||||
|
<app-tag-badge a11y-click="13,32" class="col-auto" (click)="goTo(queryParam, item.id)" [selectionMode]="TagBadgeCursor.Clickable">
|
||||||
|
<ng-container [ngTemplateOutlet]="titleTemplate" [ngTemplateOutletContext]="{ $implicit: item, idx: position }"></ng-container>
|
||||||
|
</app-tag-badge>
|
||||||
|
</ng-template>
|
||||||
|
</ng-template>
|
||||||
|
</app-badge-expander>
|
||||||
|
</div>
|
||||||
|
</div>
|
@ -0,0 +1,37 @@
|
|||||||
|
import {ChangeDetectionStrategy, Component, ContentChild, inject, Input, TemplateRef} from '@angular/core';
|
||||||
|
import {CommonModule} from '@angular/common';
|
||||||
|
import {A11yClickDirective} from "../../../shared/a11y-click.directive";
|
||||||
|
import {BadgeExpanderComponent} from "../../../shared/badge-expander/badge-expander.component";
|
||||||
|
import {TagBadgeComponent, TagBadgeCursor} from "../../../shared/tag-badge/tag-badge.component";
|
||||||
|
import {FilterQueryParam} from "../../../shared/_services/filter-utilities.service";
|
||||||
|
import {Router} from "@angular/router";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-metadata-detail',
|
||||||
|
standalone: true,
|
||||||
|
imports: [CommonModule, A11yClickDirective, BadgeExpanderComponent, TagBadgeComponent],
|
||||||
|
templateUrl: './metadata-detail.component.html',
|
||||||
|
styleUrls: ['./metadata-detail.component.scss'],
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush
|
||||||
|
})
|
||||||
|
export class MetadataDetailComponent {
|
||||||
|
|
||||||
|
@Input({required: true}) tags: Array<any> = [];
|
||||||
|
@Input({required: true}) libraryId!: number;
|
||||||
|
@Input({required: true}) heading!: string;
|
||||||
|
@Input() queryParam: FilterQueryParam = FilterQueryParam.None;
|
||||||
|
@ContentChild('titleTemplate') titleTemplate!: TemplateRef<any>;
|
||||||
|
@ContentChild('itemTemplate') itemTemplate?: TemplateRef<any>;
|
||||||
|
|
||||||
|
private readonly router = inject(Router);
|
||||||
|
protected readonly TagBadgeCursor = TagBadgeCursor;
|
||||||
|
|
||||||
|
|
||||||
|
goTo(queryParamName: FilterQueryParam, filter: any) {
|
||||||
|
if (queryParamName === FilterQueryParam.None) return;
|
||||||
|
let params: any = {};
|
||||||
|
params[queryParamName] = filter;
|
||||||
|
params[FilterQueryParam.Page] = 1;
|
||||||
|
this.router.navigate(['library', this.libraryId], {queryParams: params});
|
||||||
|
}
|
||||||
|
}
|
@ -174,7 +174,7 @@
|
|||||||
<ng-template #storylineListLayout>
|
<ng-template #storylineListLayout>
|
||||||
<ng-container *ngFor="let item of scroll.viewPortItems; let idx = index; trackBy: trackByStoryLineIdentity">
|
<ng-container *ngFor="let item of scroll.viewPortItems; let idx = index; trackBy: trackByStoryLineIdentity">
|
||||||
<ng-container *ngIf="!item.isChapter; else chapterListItem">
|
<ng-container *ngIf="!item.isChapter; else chapterListItem">
|
||||||
<app-list-item [imageUrl]="imageService.getVolumeCoverImage(item.volume.id)"
|
<app-list-item [imageUrl]="imageService.getVolumeCoverImage(item.volume.id)" [libraryId]="libraryId"
|
||||||
[seriesName]="series.name" [entity]="item.volume" *ngIf="item.volume.number !== 0"
|
[seriesName]="series.name" [entity]="item.volume" *ngIf="item.volume.number !== 0"
|
||||||
[actions]="volumeActions" [libraryType]="libraryType" imageWidth="130px" imageHeight=""
|
[actions]="volumeActions" [libraryType]="libraryType" imageWidth="130px" imageHeight=""
|
||||||
[pagesRead]="item.volume.pagesRead" [totalPages]="item.volume.pages" (read)="openVolume(item.volume)"
|
[pagesRead]="item.volume.pagesRead" [totalPages]="item.volume.pages" (read)="openVolume(item.volume)"
|
||||||
@ -185,7 +185,7 @@
|
|||||||
</app-list-item>
|
</app-list-item>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<ng-template #chapterListItem>
|
<ng-template #chapterListItem>
|
||||||
<app-list-item [imageUrl]="imageService.getChapterCoverImage(item.chapter.id)"
|
<app-list-item [imageUrl]="imageService.getChapterCoverImage(item.chapter.id)" [libraryId]="libraryId"
|
||||||
[seriesName]="series.name" [entity]="item.chapter" *ngIf="!item.chapter.isSpecial"
|
[seriesName]="series.name" [entity]="item.chapter" *ngIf="!item.chapter.isSpecial"
|
||||||
[actions]="chapterActions" [libraryType]="libraryType" imageWidth="130px" imageHeight=""
|
[actions]="chapterActions" [libraryType]="libraryType" imageWidth="130px" imageHeight=""
|
||||||
[pagesRead]="item.chapter.pagesRead" [totalPages]="item.chapter.pages" (read)="openChapter(item.chapter)"
|
[pagesRead]="item.chapter.pagesRead" [totalPages]="item.chapter.pages" (read)="openChapter(item.chapter)"
|
||||||
@ -219,7 +219,7 @@
|
|||||||
</ng-container>
|
</ng-container>
|
||||||
<ng-template #volumeListLayout>
|
<ng-template #volumeListLayout>
|
||||||
<ng-container *ngFor="let volume of scroll.viewPortItems; let idx = index; trackBy: trackByVolumeIdentity">
|
<ng-container *ngFor="let volume of scroll.viewPortItems; let idx = index; trackBy: trackByVolumeIdentity">
|
||||||
<app-list-item [imageUrl]="imageService.getVolumeCoverImage(volume.id)"
|
<app-list-item [imageUrl]="imageService.getVolumeCoverImage(volume.id)" [libraryId]="libraryId"
|
||||||
[seriesName]="series.name" [entity]="volume"
|
[seriesName]="series.name" [entity]="volume"
|
||||||
[actions]="volumeActions" [libraryType]="libraryType" imageWidth="130px" imageHeight=""
|
[actions]="volumeActions" [libraryType]="libraryType" imageWidth="130px" imageHeight=""
|
||||||
[pagesRead]="volume.pagesRead" [totalPages]="volume.pages" (read)="openVolume(volume)"
|
[pagesRead]="volume.pagesRead" [totalPages]="volume.pages" (read)="openVolume(volume)"
|
||||||
@ -256,7 +256,7 @@
|
|||||||
</ng-container>
|
</ng-container>
|
||||||
<ng-template #chapterListLayout>
|
<ng-template #chapterListLayout>
|
||||||
<div *ngFor="let chapter of scroll.viewPortItems; let idx = index; trackBy: trackByChapterIdentity">
|
<div *ngFor="let chapter of scroll.viewPortItems; let idx = index; trackBy: trackByChapterIdentity">
|
||||||
<app-list-item [imageUrl]="imageService.getChapterCoverImage(chapter.id)"
|
<app-list-item [imageUrl]="imageService.getChapterCoverImage(chapter.id)" [libraryId]="libraryId"
|
||||||
[seriesName]="series.name" [entity]="chapter" *ngIf="!chapter.isSpecial"
|
[seriesName]="series.name" [entity]="chapter" *ngIf="!chapter.isSpecial"
|
||||||
[actions]="chapterActions" [libraryType]="libraryType" imageWidth="130px" imageHeight=""
|
[actions]="chapterActions" [libraryType]="libraryType" imageWidth="130px" imageHeight=""
|
||||||
[pagesRead]="chapter.pagesRead" [totalPages]="chapter.pages" (read)="openChapter(chapter)"
|
[pagesRead]="chapter.pagesRead" [totalPages]="chapter.pages" (read)="openChapter(chapter)"
|
||||||
@ -290,7 +290,7 @@
|
|||||||
</ng-container>
|
</ng-container>
|
||||||
<ng-template #specialListLayout>
|
<ng-template #specialListLayout>
|
||||||
<ng-container *ngFor="let chapter of scroll.viewPortItems; let idx = index; trackBy: trackByChapterIdentity">
|
<ng-container *ngFor="let chapter of scroll.viewPortItems; let idx = index; trackBy: trackByChapterIdentity">
|
||||||
<app-list-item [imageUrl]="imageService.getChapterCoverImage(chapter.id)"
|
<app-list-item [imageUrl]="imageService.getChapterCoverImage(chapter.id)" [libraryId]="libraryId"
|
||||||
[seriesName]="series.name" [entity]="chapter"
|
[seriesName]="series.name" [entity]="chapter"
|
||||||
[actions]="chapterActions" [libraryType]="libraryType" imageWidth="130px" imageHeight=""
|
[actions]="chapterActions" [libraryType]="libraryType" imageWidth="130px" imageHeight=""
|
||||||
[pagesRead]="chapter.pagesRead" [totalPages]="chapter.pages" (read)="openChapter(chapter)"
|
[pagesRead]="chapter.pagesRead" [totalPages]="chapter.pages" (read)="openChapter(chapter)"
|
||||||
|
@ -2,219 +2,121 @@
|
|||||||
<app-read-more [text]="seriesSummary" [maxLength]="250"></app-read-more>
|
<app-read-more [text]="seriesSummary" [maxLength]="250"></app-read-more>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row g-0 mt-2 mb-2">
|
|
||||||
<div class="col-md-4">
|
<app-metadata-detail [tags]="['']" [libraryId]="series.libraryId" heading="Ratings">
|
||||||
<h5>Ratings</h5>
|
<ng-template #itemTemplate let-item>
|
||||||
</div>
|
<app-external-rating [seriesId]="series.id" [userRating]="series.userRating" [hasUserRated]="series.hasUserRated" [libraryType]="libraryType"></app-external-rating>
|
||||||
<div class="col-md-8">
|
</ng-template>
|
||||||
<app-external-rating [seriesId]="series.id" [userRating]="series.userRating" [libraryType]="libraryType"></app-external-rating>
|
</app-metadata-detail>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
<ng-container *ngIf="WebLinks as links">
|
<ng-container *ngIf="WebLinks as links">
|
||||||
<div class="row g-0 mt-2 mb-2" *ngIf="links.length > 0">
|
<app-metadata-detail [tags]="links" [libraryId]="series.libraryId" heading="Links">
|
||||||
<div class="col-md-4">
|
<ng-template #itemTemplate let-item>
|
||||||
<h5>Links</h5>
|
<a class="col me-1" [href]="link | safeHtml" target="_blank" rel="noopener noreferrer" *ngFor="let link of links" [title]="link">
|
||||||
</div>
|
<img width="24" height="24" class="lazyload img-placeholder"
|
||||||
<div class="col-md-8">
|
[src]="imageService.errorWebLinkImage"
|
||||||
<a class="col me-1" [href]="link | safeHtml" target="_blank" rel="noopener noreferrer" *ngFor="let link of links" [title]="link">
|
[attr.data-src]="imageService.getWebLinkImage(link)"
|
||||||
<img width="24" height="24" class="lazyload img-placeholder"
|
(error)="imageService.updateErroredWebLinkImage($event)"
|
||||||
[src]="imageService.errorWebLinkImage"
|
aria-hidden="true" alt="">
|
||||||
[attr.data-src]="imageService.getWebLinkImage(link)"
|
</a>
|
||||||
(error)="imageService.updateErroredWebLinkImage($event)"
|
</ng-template>
|
||||||
aria-hidden="true" alt="">
|
</app-metadata-detail>
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
|
|
||||||
<div class="row g-0" *ngIf="seriesMetadata.genres && seriesMetadata.genres.length > 0">
|
<app-metadata-detail [tags]="seriesMetadata.genres" [libraryId]="series.libraryId" [queryParam]="FilterQueryParam.Genres" heading="Genres">
|
||||||
<div class="col-md-4">
|
<ng-template #titleTemplate let-item>{{item.title}}</ng-template>
|
||||||
<h5>Genres</h5>
|
</app-metadata-detail>
|
||||||
</div>
|
|
||||||
<div class="col-md-8">
|
<app-metadata-detail [tags]="seriesMetadata.tags" [libraryId]="series.libraryId" [queryParam]="FilterQueryParam.Tags" heading="Tags">
|
||||||
<app-badge-expander [items]="seriesMetadata.genres">
|
<ng-template #titleTemplate let-item>{{item.title}}</ng-template>
|
||||||
<ng-template #badgeExpanderItem let-item let-position="idx">
|
</app-metadata-detail>
|
||||||
<app-tag-badge a11y-click="13,32" class="col-auto" (click)="goTo(FilterQueryParam.Genres, item.id)" [selectionMode]="TagBadgeCursor.Clickable">{{item.title}}</app-tag-badge>
|
|
||||||
</ng-template>
|
<app-metadata-detail [tags]="seriesMetadata.collectionTags" [libraryId]="series.libraryId" heading="Collections">
|
||||||
</app-badge-expander>
|
<ng-template #itemTemplate let-item>
|
||||||
</div>
|
<app-tag-badge a11y-click="13,32" class="col-auto" (click)="navigate('collections', item.id)" [selectionMode]="TagBadgeCursor.Clickable">
|
||||||
</div>
|
{{item.title}}
|
||||||
<div class="row g-0" *ngIf="seriesMetadata.tags && seriesMetadata.tags.length > 0">
|
</app-tag-badge>
|
||||||
<div class="col-md-4">
|
</ng-template>
|
||||||
<h5>Tags</h5>
|
</app-metadata-detail>
|
||||||
</div>
|
|
||||||
<div class="col-md-8">
|
|
||||||
<app-badge-expander [items]="seriesMetadata.tags">
|
<app-metadata-detail [tags]="readingLists" [libraryId]="series.libraryId" heading="Reading Lists">
|
||||||
<ng-template #badgeExpanderItem let-item let-position="idx">
|
<ng-template #itemTemplate let-item>
|
||||||
<app-tag-badge a11y-click="13,32" class="col-auto" (click)="goTo(FilterQueryParam.Tags, item.id)" [selectionMode]="TagBadgeCursor.Clickable">{{item.title}}</app-tag-badge>
|
<app-tag-badge a11y-click="13,32" class="col-auto" (click)="navigate('lists', item.id)" [selectionMode]="TagBadgeCursor.Clickable">
|
||||||
</ng-template>
|
|
||||||
</app-badge-expander>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row g-0 mt-1" *ngIf="seriesMetadata.collectionTags && seriesMetadata.collectionTags.length > 0">
|
|
||||||
<div class="col-md-4">
|
|
||||||
<h5>Collections</h5>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-8">
|
|
||||||
<app-badge-expander [items]="seriesMetadata.collectionTags">
|
|
||||||
<ng-template #badgeExpanderItem let-item let-position="idx">
|
|
||||||
<app-tag-badge a11y-click="13,32" class="col-auto" (click)="navigate('collections', item.id)" [selectionMode]="TagBadgeCursor.Clickable">
|
|
||||||
{{item.title}}
|
|
||||||
</app-tag-badge>
|
|
||||||
</ng-template>
|
|
||||||
</app-badge-expander>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row g-0 mt-1" *ngIf="readingLists && readingLists.length > 0">
|
|
||||||
<div class="col-md-4">
|
|
||||||
<h5>Reading Lists</h5>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-8">
|
|
||||||
<app-badge-expander [items]="readingLists">
|
|
||||||
<ng-template #badgeExpanderItem let-item let-position="idx">
|
|
||||||
<app-tag-badge a11y-click="13,32" class="col-auto" (click)="navigate('lists', item.id)" [selectionMode]="TagBadgeCursor.Clickable">
|
|
||||||
<span *ngIf="item.promoted">
|
<span *ngIf="item.promoted">
|
||||||
<i class="fa fa-angle-double-up" aria-hidden="true"></i>
|
<i class="fa fa-angle-double-up" aria-hidden="true"></i>
|
||||||
<span class="visually-hidden">(promoted)</span>
|
<span class="visually-hidden">(promoted)</span>
|
||||||
</span>
|
</span>
|
||||||
{{item.title}}
|
{{item.title}}
|
||||||
</app-tag-badge>
|
</app-tag-badge>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</app-badge-expander>
|
</app-metadata-detail>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row g-0 mt-1" *ngIf="seriesMetadata.writers && seriesMetadata.writers.length > 0">
|
<app-metadata-detail [tags]="seriesMetadata.writers" [libraryId]="series.libraryId" [queryParam]="FilterQueryParam.Writers" heading="Writers/Authors">
|
||||||
<div class="col-md-4">
|
<ng-template #itemTemplate let-item>
|
||||||
<h5>Writers/Authors</h5>
|
<app-person-badge a11y-click="13,32" class="col-auto" [person]="item"></app-person-badge>
|
||||||
</div>
|
</ng-template>
|
||||||
<div class="col-md-8">
|
</app-metadata-detail>
|
||||||
<app-badge-expander [items]="seriesMetadata.writers">
|
|
||||||
<ng-template #badgeExpanderItem let-item let-position="idx">
|
|
||||||
<app-person-badge a11y-click="13,32" class="col-auto" (click)="goTo(FilterQueryParam.Writers, item.id)" [person]="item"></app-person-badge>
|
|
||||||
</ng-template>
|
|
||||||
</app-badge-expander>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div #collapse="ngbCollapse" [(ngbCollapse)]="isCollapsed" id="extended-series-metadata">
|
<div #collapse="ngbCollapse" [(ngbCollapse)]="isCollapsed" id="extended-series-metadata">
|
||||||
<div class="row g-0 mt-1" *ngIf="seriesMetadata.coverArtists && seriesMetadata.coverArtists.length > 0">
|
<app-metadata-detail [tags]="seriesMetadata.coverArtists" [libraryId]="series.libraryId" [queryParam]="FilterQueryParam.CoverArtists" heading="Cover Artists">
|
||||||
<div class="col-md-4">
|
<ng-template #itemTemplate let-item>
|
||||||
<h5>Cover Artists</h5>
|
<app-person-badge a11y-click="13,32" class="col-auto" [person]="item"></app-person-badge>
|
||||||
</div>
|
</ng-template>
|
||||||
<div class="col-md-8">
|
</app-metadata-detail>
|
||||||
<app-badge-expander [items]="seriesMetadata.coverArtists">
|
|
||||||
<ng-template #badgeExpanderItem let-item let-position="idx">
|
|
||||||
<app-person-badge a11y-click="13,32" class="col-auto" (click)="goTo(FilterQueryParam.CoverArtists, item.id)" [person]="item"></app-person-badge>
|
|
||||||
</ng-template>
|
|
||||||
</app-badge-expander>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row g-0 mt-1" *ngIf="seriesMetadata.characters && seriesMetadata.characters.length > 0">
|
<app-metadata-detail [tags]="seriesMetadata.characters" [libraryId]="series.libraryId" [queryParam]="FilterQueryParam.Character" heading="Characters">
|
||||||
<div class="col-md-4">
|
<ng-template #itemTemplate let-item>
|
||||||
<h5>Characters</h5>
|
<app-person-badge a11y-click="13,32" class="col-auto" [person]="item"></app-person-badge>
|
||||||
</div>
|
</ng-template>
|
||||||
<div class="col-md-8">
|
</app-metadata-detail>
|
||||||
<app-badge-expander [items]="seriesMetadata.characters">
|
|
||||||
<ng-template #badgeExpanderItem let-item let-position="idx">
|
|
||||||
<app-person-badge a11y-click="13,32" class="col-auto" (click)="goTo(FilterQueryParam.Character, item.id)" [person]="item"></app-person-badge>
|
|
||||||
</ng-template>
|
|
||||||
</app-badge-expander>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row g-0 mt-1" *ngIf="seriesMetadata.colorists && seriesMetadata.colorists.length > 0">
|
<app-metadata-detail [tags]="seriesMetadata.colorists" [libraryId]="series.libraryId" [queryParam]="FilterQueryParam.Colorist" heading="Colorists">
|
||||||
<div class="col-md-4">
|
<ng-template #itemTemplate let-item>
|
||||||
<h5>Colorists</h5>
|
<app-person-badge a11y-click="13,32" class="col-auto" [person]="item"></app-person-badge>
|
||||||
</div>
|
</ng-template>
|
||||||
<div class="col-md-8">
|
</app-metadata-detail>
|
||||||
<app-badge-expander [items]="seriesMetadata.colorists">
|
|
||||||
<ng-template #badgeExpanderItem let-item let-position="idx">
|
|
||||||
<app-person-badge a11y-click="13,32" class="col-auto" (click)="goTo(FilterQueryParam.Colorist, item.id)" [person]="item"></app-person-badge>
|
|
||||||
</ng-template>
|
|
||||||
</app-badge-expander>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row g-0 mt-1" *ngIf="seriesMetadata.editors && seriesMetadata.editors.length > 0">
|
<app-metadata-detail [tags]="seriesMetadata.editors" [libraryId]="series.libraryId" [queryParam]="FilterQueryParam.Editor" heading="Editors">
|
||||||
<div class="col-md-4">
|
<ng-template #itemTemplate let-item>
|
||||||
<h5>Editors</h5>
|
<app-person-badge a11y-click="13,32" class="col-auto" [person]="item"></app-person-badge>
|
||||||
</div>
|
</ng-template>
|
||||||
<div class="col-md-8">
|
</app-metadata-detail>
|
||||||
<app-badge-expander [items]="seriesMetadata.editors">
|
|
||||||
<ng-template #badgeExpanderItem let-item let-position="idx">
|
|
||||||
<app-person-badge a11y-click="13,32" class="col-auto" (click)="goTo(FilterQueryParam.Editor, item.id)" [person]="item"></app-person-badge>
|
|
||||||
</ng-template>
|
|
||||||
</app-badge-expander>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row g-0 mt-1" *ngIf="seriesMetadata.inkers && seriesMetadata.inkers.length > 0">
|
<app-metadata-detail [tags]="seriesMetadata.inkers" [libraryId]="series.libraryId" [queryParam]="FilterQueryParam.Inker" heading="Inkers">
|
||||||
<div class="col-md-4">
|
<ng-template #itemTemplate let-item>
|
||||||
<h5>Inkers</h5>
|
<app-person-badge a11y-click="13,32" class="col-auto" [person]="item"></app-person-badge>
|
||||||
</div>
|
</ng-template>
|
||||||
<div class="col-md-8">
|
</app-metadata-detail>
|
||||||
<app-badge-expander [items]="seriesMetadata.inkers">
|
|
||||||
<ng-template #badgeExpanderItem let-item let-position="idx">
|
|
||||||
<app-person-badge a11y-click="13,32" class="col-auto" (click)="goTo(FilterQueryParam.Inker, item.id)" [person]="item"></app-person-badge>
|
|
||||||
</ng-template>
|
|
||||||
</app-badge-expander>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row g-0 mt-1" *ngIf="seriesMetadata.letterers && seriesMetadata.letterers.length > 0">
|
<app-metadata-detail [tags]="seriesMetadata.letterers" [libraryId]="series.libraryId" [queryParam]="FilterQueryParam.Letterer" heading="Letterers">
|
||||||
<div class="col-md-4">
|
<ng-template #itemTemplate let-item>
|
||||||
<h5>Letterers</h5>
|
<app-person-badge a11y-click="13,32" class="col-auto" [person]="item"></app-person-badge>
|
||||||
</div>
|
</ng-template>
|
||||||
<div class="col-md-8">
|
</app-metadata-detail>
|
||||||
<app-badge-expander [items]="seriesMetadata.letterers">
|
|
||||||
<ng-template #badgeExpanderItem let-item let-position="idx">
|
|
||||||
<app-person-badge a11y-click="13,32" class="col-auto" (click)="goTo(FilterQueryParam.Letterer, item.id)" [person]="item"></app-person-badge>
|
|
||||||
</ng-template>
|
|
||||||
</app-badge-expander>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row g-0 mt-1" *ngIf="seriesMetadata.translators && seriesMetadata.translators.length > 0">
|
|
||||||
<div class="col-md-4">
|
|
||||||
<h5>Translators</h5>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-8">
|
|
||||||
<app-badge-expander [items]="seriesMetadata.translators">
|
|
||||||
<ng-template #badgeExpanderItem let-item let-position="idx">
|
|
||||||
<app-person-badge a11y-click="13,32" class="col-auto" (click)="goTo(FilterQueryParam.Translator, item.id)" [person]="item"></app-person-badge>
|
|
||||||
</ng-template>
|
|
||||||
</app-badge-expander>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row g-0 mt-1" *ngIf="seriesMetadata.pencillers && seriesMetadata.pencillers.length > 0">
|
<app-metadata-detail [tags]="seriesMetadata.translators" [libraryId]="series.libraryId" [queryParam]="FilterQueryParam.Translator" heading="Translators">
|
||||||
<div class="col-md-4">
|
<ng-template #itemTemplate let-item>
|
||||||
<h5>Pencillers</h5>
|
<app-person-badge a11y-click="13,32" class="col-auto" [person]="item"></app-person-badge>
|
||||||
</div>
|
</ng-template>
|
||||||
<div class="col-md-8">
|
</app-metadata-detail>
|
||||||
<app-badge-expander [items]="seriesMetadata.pencillers">
|
|
||||||
<ng-template #badgeExpanderItem let-item let-position="idx">
|
<app-metadata-detail [tags]="seriesMetadata.pencillers" [libraryId]="series.libraryId" [queryParam]="FilterQueryParam.Penciller" heading="Pencillers">
|
||||||
<app-person-badge a11y-click="13,32" class="col-auto" (click)="goTo(FilterQueryParam.Penciller, item.id)" [person]="item"></app-person-badge>
|
<ng-template #itemTemplate let-item>
|
||||||
</ng-template>
|
<app-person-badge a11y-click="13,32" class="col-auto" [person]="item"></app-person-badge>
|
||||||
</app-badge-expander>
|
</ng-template>
|
||||||
</div>
|
</app-metadata-detail>
|
||||||
</div>
|
|
||||||
|
<app-metadata-detail [tags]="seriesMetadata.publishers" [libraryId]="series.libraryId" [queryParam]="FilterQueryParam.Publisher" heading="Publishers">
|
||||||
|
<ng-template #itemTemplate let-item>
|
||||||
|
<app-person-badge a11y-click="13,32" class="col-auto" [person]="item"></app-person-badge>
|
||||||
|
</ng-template>
|
||||||
|
</app-metadata-detail>
|
||||||
|
|
||||||
<div class="row g-0 mt-1" *ngIf="seriesMetadata.publishers && seriesMetadata.publishers.length > 0">
|
|
||||||
<div class="col-md-4">
|
|
||||||
<h5>Publishers</h5>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-8">
|
|
||||||
<app-badge-expander [items]="seriesMetadata.publishers">
|
|
||||||
<ng-template #badgeExpanderItem let-item let-position="idx">
|
|
||||||
<app-person-badge a11y-click="13,32" class="col-auto" (click)="goTo(FilterQueryParam.Publisher, item.id)" [person]="item"></app-person-badge>
|
|
||||||
</ng-template>
|
|
||||||
</app-badge-expander>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row g-0">
|
<div class="row g-0">
|
||||||
@ -226,5 +128,4 @@
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- This first row will have random information about the series-->
|
|
||||||
<app-series-info-cards [series]="series" [seriesMetadata]="seriesMetadata" (goTo)="handleGoTo($event)" [hasReadingProgress]="hasReadingProgress"></app-series-info-cards>
|
<app-series-info-cards [series]="series" [seriesMetadata]="seriesMetadata" (goTo)="handleGoTo($event)" [hasReadingProgress]="hasReadingProgress"></app-series-info-cards>
|
||||||
|
@ -19,12 +19,15 @@ import {PersonBadgeComponent} from "../../../shared/person-badge/person-badge.co
|
|||||||
import {NgbCollapse} from "@ng-bootstrap/ng-bootstrap";
|
import {NgbCollapse} from "@ng-bootstrap/ng-bootstrap";
|
||||||
import {SeriesInfoCardsComponent} from "../../../cards/series-info-cards/series-info-cards.component";
|
import {SeriesInfoCardsComponent} from "../../../cards/series-info-cards/series-info-cards.component";
|
||||||
import {LibraryType} from "../../../_models/library";
|
import {LibraryType} from "../../../_models/library";
|
||||||
|
import {MetadataDetailComponent} from "../metadata-detail/metadata-detail.component";
|
||||||
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-series-metadata-detail',
|
selector: 'app-series-metadata-detail',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [CommonModule, TagBadgeComponent, BadgeExpanderComponent, SafeHtmlPipe, ExternalRatingComponent, ReadMoreComponent, A11yClickDirective, PersonBadgeComponent, NgbCollapse, SeriesInfoCardsComponent],
|
imports: [CommonModule, TagBadgeComponent, BadgeExpanderComponent, SafeHtmlPipe, ExternalRatingComponent,
|
||||||
|
ReadMoreComponent, A11yClickDirective, PersonBadgeComponent, NgbCollapse, SeriesInfoCardsComponent,
|
||||||
|
MetadataDetailComponent],
|
||||||
templateUrl: './series-metadata-detail.component.html',
|
templateUrl: './series-metadata-detail.component.html',
|
||||||
styleUrls: ['./series-metadata-detail.component.scss'],
|
styleUrls: ['./series-metadata-detail.component.scss'],
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush
|
changeDetection: ChangeDetectionStrategy.OnPush
|
||||||
@ -78,9 +81,7 @@ export class SeriesMetadataDetailComponent implements OnChanges {
|
|||||||
this.seriesMetadata.translators.length > 0;
|
this.seriesMetadata.translators.length > 0;
|
||||||
|
|
||||||
|
|
||||||
if (this.seriesMetadata !== null) {
|
this.seriesSummary = (this.seriesMetadata?.summary === null ? '' : this.seriesMetadata.summary).replace(/\n/g, '<br>');
|
||||||
this.seriesSummary = (this.seriesMetadata.summary === null ? '' : this.seriesMetadata.summary).replace(/\n/g, '<br>');
|
|
||||||
}
|
|
||||||
this.cdRef.markForCheck();
|
this.cdRef.markForCheck();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -34,7 +34,11 @@ export enum FilterQueryParam {
|
|||||||
/**
|
/**
|
||||||
* This is a pagination control
|
* This is a pagination control
|
||||||
*/
|
*/
|
||||||
Page = 'page'
|
Page = 'page',
|
||||||
|
/**
|
||||||
|
* Special case for the UI. Does not trigger filtering
|
||||||
|
*/
|
||||||
|
None = 'none'
|
||||||
}
|
}
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
@ -46,19 +50,19 @@ export class FilterUtilitiesService {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates the window location with a custom url based on filter and pagination objects
|
* Updates the window location with a custom url based on filter and pagination objects
|
||||||
* @param pagination
|
* @param pagination
|
||||||
* @param filter
|
* @param filter
|
||||||
*/
|
*/
|
||||||
updateUrlFromFilter(pagination: Pagination, filter: SeriesFilter | undefined) {
|
updateUrlFromFilter(pagination: Pagination, filter: SeriesFilter | undefined) {
|
||||||
const params = '?page=' + pagination.currentPage;
|
const params = '?page=' + pagination.currentPage;
|
||||||
|
|
||||||
const url = this.urlFromFilter(window.location.href.split('?')[0] + params, filter);
|
const url = this.urlFromFilter(window.location.href.split('?')[0] + params, filter);
|
||||||
window.history.replaceState(window.location.href, '', this.replacePaginationOnUrl(url, pagination));
|
window.history.replaceState(window.location.href, '', this.replacePaginationOnUrl(url, pagination));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Patches the page query param in the window location.
|
* Patches the page query param in the window location.
|
||||||
* @param pagination
|
* @param pagination
|
||||||
*/
|
*/
|
||||||
updateUrlFromPagination(pagination: Pagination) {
|
updateUrlFromPagination(pagination: Pagination) {
|
||||||
window.history.replaceState(window.location.href, '', this.replacePaginationOnUrl(window.location.href, pagination));
|
window.history.replaceState(window.location.href, '', this.replacePaginationOnUrl(window.location.href, pagination));
|
||||||
@ -127,7 +131,7 @@ export class FilterUtilitiesService {
|
|||||||
if (filter.seriesNameQuery !== '') {
|
if (filter.seriesNameQuery !== '') {
|
||||||
params += `&${FilterQueryParam.Name}=${encodeURIComponent(filter.seriesNameQuery)}`;
|
params += `&${FilterQueryParam.Name}=${encodeURIComponent(filter.seriesNameQuery)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return currentUrl + params;
|
return currentUrl + params;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -262,7 +266,7 @@ export class FilterUtilitiesService {
|
|||||||
anyChanged = true;
|
anyChanged = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Rating, seriesName,
|
// Rating, seriesName,
|
||||||
const rating = snapshot.queryParamMap.get(FilterQueryParam.Rating);
|
const rating = snapshot.queryParamMap.get(FilterQueryParam.Rating);
|
||||||
if (rating !== undefined && rating !== null && parseInt(rating, 10) > 0) {
|
if (rating !== undefined && rating !== null && parseInt(rating, 10) > 0) {
|
||||||
filter.rating = parseInt(rating, 10);
|
filter.rating = parseInt(rating, 10);
|
||||||
@ -301,7 +305,7 @@ export class FilterUtilitiesService {
|
|||||||
filter.seriesNameQuery = decodeURIComponent(searchNameQuery);
|
filter.seriesNameQuery = decodeURIComponent(searchNameQuery);
|
||||||
anyChanged = true;
|
anyChanged = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
return [filter, false]; // anyChanged. Testing out if having a filter active but keep drawer closed by default works better
|
return [filter, false]; // anyChanged. Testing out if having a filter active but keep drawer closed by default works better
|
||||||
}
|
}
|
||||||
|
@ -252,4 +252,7 @@
|
|||||||
--review-spoiler-bg-color: var(--primary-color);
|
--review-spoiler-bg-color: var(--primary-color);
|
||||||
--review-spoiler-text-color: var(--body-text-color);
|
--review-spoiler-text-color: var(--body-text-color);
|
||||||
|
|
||||||
|
/** Rating Star Color **/
|
||||||
|
--rating-star-color: var(--primary-color);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
22
openapi.json
22
openapi.json
@ -11306,9 +11306,13 @@
|
|||||||
"format": "int32"
|
"format": "int32"
|
||||||
},
|
},
|
||||||
"rating": {
|
"rating": {
|
||||||
"type": "integer",
|
"type": "number",
|
||||||
"description": "A number between 0-5 that represents how good a series is.",
|
"description": "A number between 0-5.0 that represents how good a series is.",
|
||||||
"format": "int32"
|
"format": "float"
|
||||||
|
},
|
||||||
|
"hasBeenRated": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "If the rating has been explicitly set. Otherwise the 0.0 rating should be ignored as it's not rated"
|
||||||
},
|
},
|
||||||
"review": {
|
"review": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
@ -15362,9 +15366,13 @@
|
|||||||
"format": "date-time"
|
"format": "date-time"
|
||||||
},
|
},
|
||||||
"userRating": {
|
"userRating": {
|
||||||
"type": "integer",
|
"type": "number",
|
||||||
"description": "Rating from logged in user. Calculated at API-time.",
|
"description": "Rating from logged in user. Calculated at API-time.",
|
||||||
"format": "int32"
|
"format": "float"
|
||||||
|
},
|
||||||
|
"hasUserRated": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "If the user has set the rating or not"
|
||||||
},
|
},
|
||||||
"format": {
|
"format": {
|
||||||
"enum": [
|
"enum": [
|
||||||
@ -17112,8 +17120,8 @@
|
|||||||
"format": "int32"
|
"format": "int32"
|
||||||
},
|
},
|
||||||
"userRating": {
|
"userRating": {
|
||||||
"type": "integer",
|
"type": "number",
|
||||||
"format": "int32"
|
"format": "float"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
|
Loading…
x
Reference in New Issue
Block a user