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>
|
||||
/// Rating from logged in user. Calculated at API-time.
|
||||
/// </summary>
|
||||
public int UserRating { get; set; }
|
||||
public MangaFormat Format { get; set; }
|
||||
public float UserRating { 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 bool NameLocked { get; set; }
|
||||
|
@ -1,9 +1,7 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace API.DTOs;
|
||||
namespace API.DTOs;
|
||||
|
||||
public class UpdateSeriesRatingDto
|
||||
{
|
||||
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)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder.HasAnnotation("ProductVersion", "7.0.8");
|
||||
modelBuilder.HasAnnotation("ProductVersion", "7.0.9");
|
||||
|
||||
modelBuilder.Entity("API.Entities.AppRole", b =>
|
||||
{
|
||||
@ -180,7 +180,7 @@ namespace API.Data.Migrations
|
||||
|
||||
b.HasIndex("AppUserId");
|
||||
|
||||
b.ToTable("AppUserBookmark", (string)null);
|
||||
b.ToTable("AppUserBookmark");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.AppUserOnDeckRemoval", b =>
|
||||
@ -201,7 +201,7 @@ namespace API.Data.Migrations
|
||||
|
||||
b.HasIndex("SeriesId");
|
||||
|
||||
b.ToTable("AppUserOnDeckRemoval", (string)null);
|
||||
b.ToTable("AppUserOnDeckRemoval");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.AppUserPreferences", b =>
|
||||
@ -309,7 +309,7 @@ namespace API.Data.Migrations
|
||||
|
||||
b.HasIndex("ThemeId");
|
||||
|
||||
b.ToTable("AppUserPreferences", (string)null);
|
||||
b.ToTable("AppUserPreferences");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.AppUserProgress", b =>
|
||||
@ -359,7 +359,7 @@ namespace API.Data.Migrations
|
||||
|
||||
b.HasIndex("SeriesId");
|
||||
|
||||
b.ToTable("AppUserProgresses", (string)null);
|
||||
b.ToTable("AppUserProgresses");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.AppUserRating", b =>
|
||||
@ -371,9 +371,12 @@ namespace API.Data.Migrations
|
||||
b.Property<int>("AppUserId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("Rating")
|
||||
b.Property<bool>("HasBeenRated")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<float>("Rating")
|
||||
.HasColumnType("REAL");
|
||||
|
||||
b.Property<string>("Review")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
@ -389,7 +392,7 @@ namespace API.Data.Migrations
|
||||
|
||||
b.HasIndex("SeriesId");
|
||||
|
||||
b.ToTable("AppUserRating", (string)null);
|
||||
b.ToTable("AppUserRating");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.AppUserRole", b =>
|
||||
@ -457,7 +460,7 @@ namespace API.Data.Migrations
|
||||
|
||||
b.HasIndex("SeriesId");
|
||||
|
||||
b.ToTable("AppUserTableOfContent", (string)null);
|
||||
b.ToTable("AppUserTableOfContent");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.Chapter", b =>
|
||||
@ -567,7 +570,7 @@ namespace API.Data.Migrations
|
||||
|
||||
b.HasIndex("VolumeId");
|
||||
|
||||
b.ToTable("Chapter", (string)null);
|
||||
b.ToTable("Chapter");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.CollectionTag", b =>
|
||||
@ -602,7 +605,7 @@ namespace API.Data.Migrations
|
||||
b.HasIndex("Id", "Promoted")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("CollectionTag", (string)null);
|
||||
b.ToTable("CollectionTag");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.Device", b =>
|
||||
@ -648,7 +651,7 @@ namespace API.Data.Migrations
|
||||
|
||||
b.HasIndex("AppUserId");
|
||||
|
||||
b.ToTable("Device", (string)null);
|
||||
b.ToTable("Device");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.FolderPath", b =>
|
||||
@ -670,7 +673,7 @@ namespace API.Data.Migrations
|
||||
|
||||
b.HasIndex("LibraryId");
|
||||
|
||||
b.ToTable("FolderPath", (string)null);
|
||||
b.ToTable("FolderPath");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.Genre", b =>
|
||||
@ -690,7 +693,7 @@ namespace API.Data.Migrations
|
||||
b.HasIndex("NormalizedTitle")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Genre", (string)null);
|
||||
b.ToTable("Genre");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.Library", b =>
|
||||
@ -748,7 +751,7 @@ namespace API.Data.Migrations
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Library", (string)null);
|
||||
b.ToTable("Library");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.MangaFile", b =>
|
||||
@ -797,7 +800,7 @@ namespace API.Data.Migrations
|
||||
|
||||
b.HasIndex("ChapterId");
|
||||
|
||||
b.ToTable("MangaFile", (string)null);
|
||||
b.ToTable("MangaFile");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.MediaError", b =>
|
||||
@ -832,7 +835,7 @@ namespace API.Data.Migrations
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("MediaError", (string)null);
|
||||
b.ToTable("MediaError");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b =>
|
||||
@ -933,7 +936,7 @@ namespace API.Data.Migrations
|
||||
b.HasIndex("Id", "SeriesId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("SeriesMetadata", (string)null);
|
||||
b.ToTable("SeriesMetadata");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.Metadata.SeriesRelation", b =>
|
||||
@ -957,7 +960,7 @@ namespace API.Data.Migrations
|
||||
|
||||
b.HasIndex("TargetSeriesId");
|
||||
|
||||
b.ToTable("SeriesRelation", (string)null);
|
||||
b.ToTable("SeriesRelation");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.Person", b =>
|
||||
@ -977,7 +980,7 @@ namespace API.Data.Migrations
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Person", (string)null);
|
||||
b.ToTable("Person");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.ReadingList", b =>
|
||||
@ -1040,7 +1043,7 @@ namespace API.Data.Migrations
|
||||
|
||||
b.HasIndex("AppUserId");
|
||||
|
||||
b.ToTable("ReadingList", (string)null);
|
||||
b.ToTable("ReadingList");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.ReadingListItem", b =>
|
||||
@ -1074,7 +1077,7 @@ namespace API.Data.Migrations
|
||||
|
||||
b.HasIndex("VolumeId");
|
||||
|
||||
b.ToTable("ReadingListItem", (string)null);
|
||||
b.ToTable("ReadingListItem");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.Scrobble.ScrobbleError", b =>
|
||||
@ -1119,7 +1122,7 @@ namespace API.Data.Migrations
|
||||
|
||||
b.HasIndex("SeriesId");
|
||||
|
||||
b.ToTable("ScrobbleError", (string)null);
|
||||
b.ToTable("ScrobbleError");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.Scrobble.ScrobbleEvent", b =>
|
||||
@ -1190,7 +1193,7 @@ namespace API.Data.Migrations
|
||||
|
||||
b.HasIndex("SeriesId");
|
||||
|
||||
b.ToTable("ScrobbleEvent", (string)null);
|
||||
b.ToTable("ScrobbleEvent");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.Scrobble.ScrobbleHold", b =>
|
||||
@ -1223,7 +1226,7 @@ namespace API.Data.Migrations
|
||||
|
||||
b.HasIndex("SeriesId");
|
||||
|
||||
b.ToTable("ScrobbleHold", (string)null);
|
||||
b.ToTable("ScrobbleHold");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.Series", b =>
|
||||
@ -1319,7 +1322,7 @@ namespace API.Data.Migrations
|
||||
|
||||
b.HasIndex("LibraryId");
|
||||
|
||||
b.ToTable("Series", (string)null);
|
||||
b.ToTable("Series");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.ServerSetting", b =>
|
||||
@ -1336,7 +1339,7 @@ namespace API.Data.Migrations
|
||||
|
||||
b.HasKey("Key");
|
||||
|
||||
b.ToTable("ServerSetting", (string)null);
|
||||
b.ToTable("ServerSetting");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.ServerStatistics", b =>
|
||||
@ -1374,7 +1377,7 @@ namespace API.Data.Migrations
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("ServerStatistics", (string)null);
|
||||
b.ToTable("ServerStatistics");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.SiteTheme", b =>
|
||||
@ -1412,7 +1415,7 @@ namespace API.Data.Migrations
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("SiteTheme", (string)null);
|
||||
b.ToTable("SiteTheme");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.Tag", b =>
|
||||
@ -1432,7 +1435,7 @@ namespace API.Data.Migrations
|
||||
b.HasIndex("NormalizedTitle")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Tag", (string)null);
|
||||
b.ToTable("Tag");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.Volume", b =>
|
||||
@ -1484,7 +1487,7 @@ namespace API.Data.Migrations
|
||||
|
||||
b.HasIndex("SeriesId");
|
||||
|
||||
b.ToTable("Volume", (string)null);
|
||||
b.ToTable("Volume");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AppUserLibrary", b =>
|
||||
@ -1499,7 +1502,7 @@ namespace API.Data.Migrations
|
||||
|
||||
b.HasIndex("LibrariesId");
|
||||
|
||||
b.ToTable("AppUserLibrary", (string)null);
|
||||
b.ToTable("AppUserLibrary");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ChapterGenre", b =>
|
||||
@ -1514,7 +1517,7 @@ namespace API.Data.Migrations
|
||||
|
||||
b.HasIndex("GenresId");
|
||||
|
||||
b.ToTable("ChapterGenre", (string)null);
|
||||
b.ToTable("ChapterGenre");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ChapterPerson", b =>
|
||||
@ -1529,7 +1532,7 @@ namespace API.Data.Migrations
|
||||
|
||||
b.HasIndex("PeopleId");
|
||||
|
||||
b.ToTable("ChapterPerson", (string)null);
|
||||
b.ToTable("ChapterPerson");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ChapterTag", b =>
|
||||
@ -1544,7 +1547,7 @@ namespace API.Data.Migrations
|
||||
|
||||
b.HasIndex("TagsId");
|
||||
|
||||
b.ToTable("ChapterTag", (string)null);
|
||||
b.ToTable("ChapterTag");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CollectionTagSeriesMetadata", b =>
|
||||
@ -1559,7 +1562,7 @@ namespace API.Data.Migrations
|
||||
|
||||
b.HasIndex("SeriesMetadatasId");
|
||||
|
||||
b.ToTable("CollectionTagSeriesMetadata", (string)null);
|
||||
b.ToTable("CollectionTagSeriesMetadata");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("GenreSeriesMetadata", b =>
|
||||
@ -1574,7 +1577,7 @@ namespace API.Data.Migrations
|
||||
|
||||
b.HasIndex("SeriesMetadatasId");
|
||||
|
||||
b.ToTable("GenreSeriesMetadata", (string)null);
|
||||
b.ToTable("GenreSeriesMetadata");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<int>", b =>
|
||||
@ -1673,7 +1676,7 @@ namespace API.Data.Migrations
|
||||
|
||||
b.HasIndex("SeriesMetadatasId");
|
||||
|
||||
b.ToTable("PersonSeriesMetadata", (string)null);
|
||||
b.ToTable("PersonSeriesMetadata");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("SeriesMetadataTag", b =>
|
||||
@ -1688,7 +1691,7 @@ namespace API.Data.Migrations
|
||||
|
||||
b.HasIndex("TagsId");
|
||||
|
||||
b.ToTable("SeriesMetadataTag", (string)null);
|
||||
b.ToTable("SeriesMetadataTag");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.AppUserBookmark", b =>
|
||||
|
@ -625,6 +625,7 @@ public class SeriesRepository : ISeriesRepository
|
||||
if (rating != null)
|
||||
{
|
||||
s.UserRating = rating.Rating;
|
||||
s.HasUserRated = rating.HasBeenRated;
|
||||
}
|
||||
|
||||
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
|
||||
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)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
var avg = (await _context.AppUserRating
|
||||
.Where(r => r.SeriesId == seriesId)
|
||||
.Where(r => r.SeriesId == seriesId && r.HasBeenRated)
|
||||
.AverageAsync(r => (int?) r.Rating));
|
||||
return avg.HasValue ? (int) (avg.Value * 20) : 0;
|
||||
}
|
||||
@ -1714,7 +1715,7 @@ public class SeriesRepository : ISeriesRepository
|
||||
public async Task ClearOnDeckRemoval(int seriesId, int userId)
|
||||
{
|
||||
var existingEntry = await _context.AppUserOnDeckRemoval
|
||||
.Where(u => u.Id == userId && u.SeriesId == seriesId)
|
||||
.Where(u => u.AppUserId == userId && u.SeriesId == seriesId)
|
||||
.FirstOrDefaultAsync();
|
||||
if (existingEntry == null) return;
|
||||
_context.AppUserOnDeckRemoval.Remove(existingEntry);
|
||||
|
@ -1,13 +1,17 @@
|
||||
|
||||
namespace API.Entities;
|
||||
|
||||
#nullable enable
|
||||
public class AppUserRating
|
||||
{
|
||||
public int Id { get; set; }
|
||||
/// <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>
|
||||
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>
|
||||
/// A short summary the user can write when giving their review.
|
||||
/// </summary>
|
||||
@ -17,7 +21,7 @@ public class AppUserRating
|
||||
/// </summary>
|
||||
public string? Tagline { get; set; }
|
||||
public int SeriesId { get; set; }
|
||||
public Series Series { get; set; }
|
||||
public Series Series { get; set; } = null!;
|
||||
|
||||
|
||||
// Relationships
|
||||
|
@ -39,7 +39,7 @@ public interface IScrobblingService
|
||||
{
|
||||
Task CheckExternalAccessTokens();
|
||||
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 ScrobbleReadingUpdate(int userId, int seriesId);
|
||||
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);
|
||||
}
|
||||
|
||||
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;
|
||||
var token = await GetTokenForProvider(userId, ScrobbleProvider.AniList);
|
||||
|
@ -294,7 +294,8 @@ public class SeriesService : ISeriesService
|
||||
new AppUserRating();
|
||||
try
|
||||
{
|
||||
userRating.Rating = Math.Clamp(updateSeriesRatingDto.UserRating, 0, 5);
|
||||
userRating.Rating = Math.Clamp(updateSeriesRatingDto.UserRating, 0f, 5f);
|
||||
userRating.HasBeenRated = true;
|
||||
userRating.SeriesId = updateSeriesRatingDto.SeriesId;
|
||||
|
||||
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-file-drop": "^16.0.0",
|
||||
"ngx-slider-v2": "^16.0.2",
|
||||
"ngx-stars": "^1.6.5",
|
||||
"ngx-toastr": "^17.0.2",
|
||||
"rxjs": "^7.8.0",
|
||||
"screenfull": "^6.0.2",
|
||||
@ -10564,6 +10565,18 @@
|
||||
"@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": {
|
||||
"version": "17.0.2",
|
||||
"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-file-drop": "^16.0.0",
|
||||
"ngx-slider-v2": "^16.0.2",
|
||||
"ngx-stars": "^1.6.5",
|
||||
"ngx-toastr": "^17.0.2",
|
||||
"rxjs": "^7.8.0",
|
||||
"screenfull": "^6.0.2",
|
||||
|
@ -27,6 +27,7 @@ export interface Series {
|
||||
* User's rating (0-5)
|
||||
*/
|
||||
userRating: number;
|
||||
hasUserRated: boolean;
|
||||
libraryId: number;
|
||||
/**
|
||||
* DateTime the entity was created
|
||||
|
@ -29,7 +29,7 @@
|
||||
</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-->
|
||||
|
@ -1,4 +1,17 @@
|
||||
<div class="row g-0 mt-4 mb-3">
|
||||
|
||||
<div class="mt-4 mb-3">
|
||||
<div class="row g-0" *ngIf="chapterMetadata ">
|
||||
<!-- Tags and Characters are used a lot of Hentai and Doujinshi type content, so showing in list item has value add on first glance -->
|
||||
<app-metadata-detail [tags]="chapterMetadata.tags" [libraryId]="libraryId" [queryParam]="FilterQueryParam.Tags" heading="Tags">
|
||||
<ng-template #titleTemplate let-item>{{item.title}}</ng-template>
|
||||
</app-metadata-detail>
|
||||
|
||||
<app-metadata-detail [tags]="chapterMetadata.characters" [libraryId]="libraryId" [queryParam]="FilterQueryParam.Character" heading="Characters">
|
||||
<ng-template #titleTemplate let-item>{{item.title}}</ng-template>
|
||||
</app-metadata-detail>
|
||||
</div>
|
||||
|
||||
<div class="row g-0">
|
||||
<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">
|
||||
@ -104,4 +117,5 @@
|
||||
</div>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -24,11 +24,13 @@ import {BytesPipe} from "../../pipe/bytes.pipe";
|
||||
import {CompactNumberPipe} from "../../pipe/compact-number.pipe";
|
||||
import {AgeRatingPipe} from "../../pipe/age-rating.pipe";
|
||||
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({
|
||||
selector: 'app-entity-info-cards',
|
||||
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',
|
||||
styleUrls: ['./entity-info-cards.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
@ -36,6 +38,7 @@ import {NgbTooltip} from "@ng-bootstrap/ng-bootstrap";
|
||||
export class EntityInfoCardsComponent implements OnInit {
|
||||
|
||||
@Input({required: true}) entity!: Volume | Chapter;
|
||||
@Input({required: true}) libraryId!: number;
|
||||
/**
|
||||
* This will pull extra information
|
||||
*/
|
||||
@ -75,8 +78,6 @@ export class EntityInfoCardsComponent implements OnInit {
|
||||
return this.chapter.webLinks.split(',');
|
||||
}
|
||||
|
||||
|
||||
|
||||
constructor(private utilityService: UtilityService, private seriesService: SeriesService, private readonly cdRef: ChangeDetectorRef) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
@ -127,8 +128,5 @@ export class EntityInfoCardsComponent implements OnInit {
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
getTimezone(timezone: string): string {
|
||||
const localDate = new Date(timezone);
|
||||
return localDate.toLocaleString('en-US', { timeZoneName: 'short' }).split(' ')[3];
|
||||
}
|
||||
protected readonly FilterQueryParam = FilterQueryParam;
|
||||
}
|
||||
|
@ -22,7 +22,7 @@
|
||||
<span class="d-none d-sm-inline-block">Read</span>
|
||||
</button>
|
||||
</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>
|
||||
<ng-container *ngIf="summary.length > 0">
|
||||
<div class="mt-2 ps-2">
|
||||
@ -30,7 +30,7 @@
|
||||
</div>
|
||||
</ng-container>
|
||||
<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>
|
||||
|
@ -5,15 +5,14 @@ import {
|
||||
EventEmitter,
|
||||
inject,
|
||||
Input,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
Output
|
||||
} from '@angular/core';
|
||||
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 { 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 { LibraryType } from 'src/app/_models/library';
|
||||
import { RelationKind } from 'src/app/_models/series-detail/relation-kind';
|
||||
@ -42,6 +41,7 @@ export class ListItemComponent implements OnInit {
|
||||
* Volume or Chapter to render
|
||||
*/
|
||||
@Input({required: true}) entity!: Volume | Chapter;
|
||||
@Input({required: true}) libraryId!: number;
|
||||
/**
|
||||
* Image to show
|
||||
*/
|
||||
@ -103,8 +103,14 @@ export class ListItemComponent implements OnInit {
|
||||
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) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
|
@ -3,9 +3,10 @@
|
||||
popoverTitle="Your Rating + Overall" popoverClass="md-popover">
|
||||
<span class="badge rounded-pill me-1">
|
||||
<img class="me-1" ngSrc="assets/images/logo-32.png" width="24" height="24" alt="">
|
||||
{{userRating * 20}}
|
||||
<ng-container *ngIf="overallRating > 0; else noOverallRating"> + {{overallRating}}%</ng-container>
|
||||
<ng-template #noOverallRating>%</ng-template>
|
||||
<ng-container *ngIf="hasUserRated; else notYetRated">{{userRating * 20}}</ng-container>
|
||||
<ng-template #notYetRated>N/A</ng-template>
|
||||
<ng-container *ngIf="overallRating > 0"> + {{overallRating}}</ng-container>
|
||||
<ng-container *ngIf="hasUserRated || overallRating > 0">%</ng-container>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@ -22,11 +23,9 @@
|
||||
</div>
|
||||
|
||||
<ng-template #popContent>
|
||||
<ngb-rating class="rating-star" [(rate)]="userRating" (rateChange)="updateRating($event)" [resettable]="false">
|
||||
<ng-template let-fill="fill" let-index="index">
|
||||
<span class="star" [class.filled]="(index < userRating) && userRating > 0">★</span>
|
||||
</ng-template>
|
||||
</ngb-rating> {{userRating * 20}}%
|
||||
<ngx-stars [initialStars]="userRating" (ratingOutput)="updateRating($event)"
|
||||
[maxStars]="5" [color]="starColor"></ngx-stars>
|
||||
{{userRating * 20}}%
|
||||
</ng-template>
|
||||
|
||||
<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 {LibraryType} from "../../../_models/library";
|
||||
import {ProviderNamePipe} from "../../../pipe/provider-name.pipe";
|
||||
import {NgxStarsModule} from "ngx-stars";
|
||||
import {ThemeService} from "../../../_services/theme.service";
|
||||
|
||||
@Component({
|
||||
selector: 'app-external-rating',
|
||||
standalone: true,
|
||||
imports: [CommonModule, ProviderImagePipe, NgOptimizedImage, NgbRating, NgbPopover, LoadingComponent, ProviderNamePipe],
|
||||
imports: [CommonModule, ProviderImagePipe, NgOptimizedImage, NgbRating, NgbPopover, LoadingComponent, ProviderNamePipe, NgxStarsModule],
|
||||
templateUrl: './external-rating.component.html',
|
||||
styleUrls: ['./external-rating.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
@ -29,15 +31,19 @@ import {ProviderNamePipe} from "../../../pipe/provider-name.pipe";
|
||||
export class ExternalRatingComponent implements OnInit {
|
||||
@Input({required: true}) seriesId!: number;
|
||||
@Input({required: true}) userRating!: number;
|
||||
@Input({required: true}) hasUserRated!: boolean;
|
||||
@Input({required: true}) libraryType!: LibraryType;
|
||||
private readonly cdRef = inject(ChangeDetectorRef);
|
||||
private readonly seriesService = inject(SeriesService);
|
||||
private readonly accountService = inject(AccountService);
|
||||
private readonly themeService = inject(ThemeService);
|
||||
|
||||
ratings: Array<Rating> = [];
|
||||
isLoading: boolean = false;
|
||||
overallRating: number = -1;
|
||||
|
||||
starColor = this.themeService.getCssVariable('--rating-star-color');
|
||||
|
||||
|
||||
ngOnInit() {
|
||||
|
||||
@ -58,9 +64,11 @@ export class ExternalRatingComponent implements OnInit {
|
||||
});
|
||||
}
|
||||
|
||||
updateRating(rating: any) {
|
||||
updateRating(rating: number) {
|
||||
this.seriesService.updateRating(this.seriesId, rating).subscribe(() => {
|
||||
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-container *ngFor="let item of scroll.viewPortItems; let idx = index; trackBy: trackByStoryLineIdentity">
|
||||
<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"
|
||||
[actions]="volumeActions" [libraryType]="libraryType" imageWidth="130px" imageHeight=""
|
||||
[pagesRead]="item.volume.pagesRead" [totalPages]="item.volume.pages" (read)="openVolume(item.volume)"
|
||||
@ -185,7 +185,7 @@
|
||||
</app-list-item>
|
||||
</ng-container>
|
||||
<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"
|
||||
[actions]="chapterActions" [libraryType]="libraryType" imageWidth="130px" imageHeight=""
|
||||
[pagesRead]="item.chapter.pagesRead" [totalPages]="item.chapter.pages" (read)="openChapter(item.chapter)"
|
||||
@ -219,7 +219,7 @@
|
||||
</ng-container>
|
||||
<ng-template #volumeListLayout>
|
||||
<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"
|
||||
[actions]="volumeActions" [libraryType]="libraryType" imageWidth="130px" imageHeight=""
|
||||
[pagesRead]="volume.pagesRead" [totalPages]="volume.pages" (read)="openVolume(volume)"
|
||||
@ -256,7 +256,7 @@
|
||||
</ng-container>
|
||||
<ng-template #chapterListLayout>
|
||||
<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"
|
||||
[actions]="chapterActions" [libraryType]="libraryType" imageWidth="130px" imageHeight=""
|
||||
[pagesRead]="chapter.pagesRead" [totalPages]="chapter.pages" (read)="openChapter(chapter)"
|
||||
@ -290,7 +290,7 @@
|
||||
</ng-container>
|
||||
<ng-template #specialListLayout>
|
||||
<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"
|
||||
[actions]="chapterActions" [libraryType]="libraryType" imageWidth="130px" imageHeight=""
|
||||
[pagesRead]="chapter.pagesRead" [totalPages]="chapter.pages" (read)="openChapter(chapter)"
|
||||
|
@ -2,22 +2,17 @@
|
||||
<app-read-more [text]="seriesSummary" [maxLength]="250"></app-read-more>
|
||||
</div>
|
||||
|
||||
<div class="row g-0 mt-2 mb-2">
|
||||
<div class="col-md-4">
|
||||
<h5>Ratings</h5>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<app-external-rating [seriesId]="series.id" [userRating]="series.userRating" [libraryType]="libraryType"></app-external-rating>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<app-metadata-detail [tags]="['']" [libraryId]="series.libraryId" heading="Ratings">
|
||||
<ng-template #itemTemplate let-item>
|
||||
<app-external-rating [seriesId]="series.id" [userRating]="series.userRating" [hasUserRated]="series.hasUserRated" [libraryType]="libraryType"></app-external-rating>
|
||||
</ng-template>
|
||||
</app-metadata-detail>
|
||||
|
||||
|
||||
<ng-container *ngIf="WebLinks as links">
|
||||
<div class="row g-0 mt-2 mb-2" *ngIf="links.length > 0">
|
||||
<div class="col-md-4">
|
||||
<h5>Links</h5>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<app-metadata-detail [tags]="links" [libraryId]="series.libraryId" heading="Links">
|
||||
<ng-template #itemTemplate let-item>
|
||||
<a class="col me-1" [href]="link | safeHtml" target="_blank" rel="noopener noreferrer" *ngFor="let link of links" [title]="link">
|
||||
<img width="24" height="24" class="lazyload img-placeholder"
|
||||
[src]="imageService.errorWebLinkImage"
|
||||
@ -25,56 +20,30 @@
|
||||
(error)="imageService.updateErroredWebLinkImage($event)"
|
||||
aria-hidden="true" alt="">
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
</app-metadata-detail>
|
||||
</ng-container>
|
||||
|
||||
|
||||
<div class="row g-0" *ngIf="seriesMetadata.genres && seriesMetadata.genres.length > 0">
|
||||
<div class="col-md-4">
|
||||
<h5>Genres</h5>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<app-badge-expander [items]="seriesMetadata.genres">
|
||||
<ng-template #badgeExpanderItem let-item let-position="idx">
|
||||
<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-badge-expander>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row g-0" *ngIf="seriesMetadata.tags && seriesMetadata.tags.length > 0">
|
||||
<div class="col-md-4">
|
||||
<h5>Tags</h5>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<app-badge-expander [items]="seriesMetadata.tags">
|
||||
<ng-template #badgeExpanderItem let-item let-position="idx">
|
||||
<app-tag-badge a11y-click="13,32" class="col-auto" (click)="goTo(FilterQueryParam.Tags, 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="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-metadata-detail [tags]="seriesMetadata.genres" [libraryId]="series.libraryId" [queryParam]="FilterQueryParam.Genres" heading="Genres">
|
||||
<ng-template #titleTemplate let-item>{{item.title}}</ng-template>
|
||||
</app-metadata-detail>
|
||||
|
||||
<app-metadata-detail [tags]="seriesMetadata.tags" [libraryId]="series.libraryId" [queryParam]="FilterQueryParam.Tags" heading="Tags">
|
||||
<ng-template #titleTemplate let-item>{{item.title}}</ng-template>
|
||||
</app-metadata-detail>
|
||||
|
||||
<app-metadata-detail [tags]="seriesMetadata.collectionTags" [libraryId]="series.libraryId" heading="Collections">
|
||||
<ng-template #itemTemplate let-item>
|
||||
<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-metadata-detail>
|
||||
|
||||
|
||||
<app-metadata-detail [tags]="readingLists" [libraryId]="series.libraryId" heading="Reading Lists">
|
||||
<ng-template #itemTemplate let-item>
|
||||
<app-tag-badge a11y-click="13,32" class="col-auto" (click)="navigate('lists', item.id)" [selectionMode]="TagBadgeCursor.Clickable">
|
||||
<span *ngIf="item.promoted">
|
||||
<i class="fa fa-angle-double-up" aria-hidden="true"></i>
|
||||
@ -83,138 +52,71 @@
|
||||
{{item.title}}
|
||||
</app-tag-badge>
|
||||
</ng-template>
|
||||
</app-badge-expander>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row g-0 mt-1" *ngIf="seriesMetadata.writers && seriesMetadata.writers.length > 0">
|
||||
<div class="col-md-4">
|
||||
<h5>Writers/Authors</h5>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<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>
|
||||
</app-metadata-detail>
|
||||
|
||||
|
||||
<app-metadata-detail [tags]="seriesMetadata.writers" [libraryId]="series.libraryId" [queryParam]="FilterQueryParam.Writers" heading="Writers/Authors">
|
||||
<ng-template #itemTemplate let-item>
|
||||
<app-person-badge a11y-click="13,32" class="col-auto" [person]="item"></app-person-badge>
|
||||
</ng-template>
|
||||
</app-badge-expander>
|
||||
</div>
|
||||
</div>
|
||||
</app-metadata-detail>
|
||||
|
||||
|
||||
<div #collapse="ngbCollapse" [(ngbCollapse)]="isCollapsed" id="extended-series-metadata">
|
||||
<div class="row g-0 mt-1" *ngIf="seriesMetadata.coverArtists && seriesMetadata.coverArtists.length > 0">
|
||||
<div class="col-md-4">
|
||||
<h5>Cover Artists</h5>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<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>
|
||||
<app-metadata-detail [tags]="seriesMetadata.coverArtists" [libraryId]="series.libraryId" [queryParam]="FilterQueryParam.CoverArtists" heading="Cover Artists">
|
||||
<ng-template #itemTemplate let-item>
|
||||
<app-person-badge a11y-click="13,32" class="col-auto" [person]="item"></app-person-badge>
|
||||
</ng-template>
|
||||
</app-badge-expander>
|
||||
</div>
|
||||
</div>
|
||||
</app-metadata-detail>
|
||||
|
||||
<div class="row g-0 mt-1" *ngIf="seriesMetadata.characters && seriesMetadata.characters.length > 0">
|
||||
<div class="col-md-4">
|
||||
<h5>Characters</h5>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<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>
|
||||
<app-metadata-detail [tags]="seriesMetadata.characters" [libraryId]="series.libraryId" [queryParam]="FilterQueryParam.Character" heading="Characters">
|
||||
<ng-template #itemTemplate let-item>
|
||||
<app-person-badge a11y-click="13,32" class="col-auto" [person]="item"></app-person-badge>
|
||||
</ng-template>
|
||||
</app-badge-expander>
|
||||
</div>
|
||||
</div>
|
||||
</app-metadata-detail>
|
||||
|
||||
<div class="row g-0 mt-1" *ngIf="seriesMetadata.colorists && seriesMetadata.colorists.length > 0">
|
||||
<div class="col-md-4">
|
||||
<h5>Colorists</h5>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<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>
|
||||
<app-metadata-detail [tags]="seriesMetadata.colorists" [libraryId]="series.libraryId" [queryParam]="FilterQueryParam.Colorist" heading="Colorists">
|
||||
<ng-template #itemTemplate let-item>
|
||||
<app-person-badge a11y-click="13,32" class="col-auto" [person]="item"></app-person-badge>
|
||||
</ng-template>
|
||||
</app-badge-expander>
|
||||
</div>
|
||||
</div>
|
||||
</app-metadata-detail>
|
||||
|
||||
<div class="row g-0 mt-1" *ngIf="seriesMetadata.editors && seriesMetadata.editors.length > 0">
|
||||
<div class="col-md-4">
|
||||
<h5>Editors</h5>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<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>
|
||||
<app-metadata-detail [tags]="seriesMetadata.editors" [libraryId]="series.libraryId" [queryParam]="FilterQueryParam.Editor" heading="Editors">
|
||||
<ng-template #itemTemplate let-item>
|
||||
<app-person-badge a11y-click="13,32" class="col-auto" [person]="item"></app-person-badge>
|
||||
</ng-template>
|
||||
</app-badge-expander>
|
||||
</div>
|
||||
</div>
|
||||
</app-metadata-detail>
|
||||
|
||||
<div class="row g-0 mt-1" *ngIf="seriesMetadata.inkers && seriesMetadata.inkers.length > 0">
|
||||
<div class="col-md-4">
|
||||
<h5>Inkers</h5>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<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>
|
||||
<app-metadata-detail [tags]="seriesMetadata.inkers" [libraryId]="series.libraryId" [queryParam]="FilterQueryParam.Inker" heading="Inkers">
|
||||
<ng-template #itemTemplate let-item>
|
||||
<app-person-badge a11y-click="13,32" class="col-auto" [person]="item"></app-person-badge>
|
||||
</ng-template>
|
||||
</app-badge-expander>
|
||||
</div>
|
||||
</div>
|
||||
</app-metadata-detail>
|
||||
|
||||
<div class="row g-0 mt-1" *ngIf="seriesMetadata.letterers && seriesMetadata.letterers.length > 0">
|
||||
<div class="col-md-4">
|
||||
<h5>Letterers</h5>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<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>
|
||||
<app-metadata-detail [tags]="seriesMetadata.letterers" [libraryId]="series.libraryId" [queryParam]="FilterQueryParam.Letterer" heading="Letterers">
|
||||
<ng-template #itemTemplate let-item>
|
||||
<app-person-badge a11y-click="13,32" class="col-auto" [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>
|
||||
</app-metadata-detail>
|
||||
|
||||
<div class="row g-0 mt-1" *ngIf="seriesMetadata.pencillers && seriesMetadata.pencillers.length > 0">
|
||||
<div class="col-md-4">
|
||||
<h5>Pencillers</h5>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<app-badge-expander [items]="seriesMetadata.pencillers">
|
||||
<ng-template #badgeExpanderItem let-item let-position="idx">
|
||||
<app-person-badge a11y-click="13,32" class="col-auto" (click)="goTo(FilterQueryParam.Penciller, item.id)" [person]="item"></app-person-badge>
|
||||
<app-metadata-detail [tags]="seriesMetadata.translators" [libraryId]="series.libraryId" [queryParam]="FilterQueryParam.Translator" heading="Translators">
|
||||
<ng-template #itemTemplate let-item>
|
||||
<app-person-badge a11y-click="13,32" class="col-auto" [person]="item"></app-person-badge>
|
||||
</ng-template>
|
||||
</app-badge-expander>
|
||||
</div>
|
||||
</div>
|
||||
</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>
|
||||
<app-metadata-detail [tags]="seriesMetadata.pencillers" [libraryId]="series.libraryId" [queryParam]="FilterQueryParam.Penciller" heading="Pencillers">
|
||||
<ng-template #itemTemplate let-item>
|
||||
<app-person-badge a11y-click="13,32" class="col-auto" [person]="item"></app-person-badge>
|
||||
</ng-template>
|
||||
</app-badge-expander>
|
||||
</div>
|
||||
</div>
|
||||
</app-metadata-detail>
|
||||
|
||||
<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>
|
||||
|
||||
<div class="row g-0">
|
||||
@ -226,5 +128,4 @@
|
||||
</a>
|
||||
</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>
|
||||
|
@ -19,12 +19,15 @@ import {PersonBadgeComponent} from "../../../shared/person-badge/person-badge.co
|
||||
import {NgbCollapse} from "@ng-bootstrap/ng-bootstrap";
|
||||
import {SeriesInfoCardsComponent} from "../../../cards/series-info-cards/series-info-cards.component";
|
||||
import {LibraryType} from "../../../_models/library";
|
||||
import {MetadataDetailComponent} from "../metadata-detail/metadata-detail.component";
|
||||
|
||||
|
||||
@Component({
|
||||
selector: 'app-series-metadata-detail',
|
||||
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',
|
||||
styleUrls: ['./series-metadata-detail.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
@ -78,9 +81,7 @@ export class SeriesMetadataDetailComponent implements OnChanges {
|
||||
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();
|
||||
}
|
||||
|
||||
|
@ -34,7 +34,11 @@ export enum FilterQueryParam {
|
||||
/**
|
||||
* This is a pagination control
|
||||
*/
|
||||
Page = 'page'
|
||||
Page = 'page',
|
||||
/**
|
||||
* Special case for the UI. Does not trigger filtering
|
||||
*/
|
||||
None = 'none'
|
||||
}
|
||||
|
||||
@Injectable({
|
||||
|
@ -252,4 +252,7 @@
|
||||
--review-spoiler-bg-color: var(--primary-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"
|
||||
},
|
||||
"rating": {
|
||||
"type": "integer",
|
||||
"description": "A number between 0-5 that represents how good a series is.",
|
||||
"format": "int32"
|
||||
"type": "number",
|
||||
"description": "A number between 0-5.0 that represents how good a series is.",
|
||||
"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": {
|
||||
"type": "string",
|
||||
@ -15362,9 +15366,13 @@
|
||||
"format": "date-time"
|
||||
},
|
||||
"userRating": {
|
||||
"type": "integer",
|
||||
"type": "number",
|
||||
"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": {
|
||||
"enum": [
|
||||
@ -17112,8 +17120,8 @@
|
||||
"format": "int32"
|
||||
},
|
||||
"userRating": {
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
"type": "number",
|
||||
"format": "float"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
|
Loading…
x
Reference in New Issue
Block a user