Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions VocaDb.Migrations/Migrations.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,26 @@

namespace VocaDb.Migrations;

[Migration(2026_05_14_1633)]
public class ChangeRatingColumnsToDecimal : Migration
{
public override void Up()
{
// Per-user rating: allow one decimal place (e.g. 0.5)
Alter.Column("Rating").OnTable(TableNames.AlbumsForUsers).AsDecimal(3, 1).NotNullable().WithDefaultValue(0.0m);

// Album aggregate total: allow one decimal place and larger range for totals
Alter.Column("RatingTotal").OnTable(TableNames.Albums).AsDecimal(9, 1).NotNullable().WithDefaultValue(0.0m);
}

public override void Down()
{
// Revert to previous integer columns
Alter.Column("Rating").OnTable(TableNames.AlbumsForUsers).AsInt32().NotNullable().WithDefaultValue(0);
Alter.Column("RatingTotal").OnTable(TableNames.Albums).AsInt32().NotNullable().WithDefaultValue(0);
}
}

[Migration(2026_02_23_0000)]
public class ExpandAdditionalNamesString : Migration
{
Expand Down
2 changes: 1 addition & 1 deletion VocaDbModel/DataContracts/Albums/AlbumDetailsContract.cs
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ public class SharedAlbumStatsContract
public AlbumReviewContract LatestReview { get; init; }

[DataMember]
public int LatestReviewRatingScore { get; init; }
public double LatestReviewRatingScore { get; init; }

[DataMember]
public int OwnedCount { get; init; }
Expand Down
2 changes: 1 addition & 1 deletion VocaDbModel/DataContracts/Users/AlbumForUserContract.cs
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ public AlbumForUserContract(
public PurchaseStatus PurchaseStatus { get; init; }

[DataMember]
public int Rating { get; init; }
public double Rating { get; init; }

// Note: only needed for album collection. True if public collection or viewer is the user himself.
public bool ShouldShowCollectionStatus { get; init; }
Expand Down
4 changes: 2 additions & 2 deletions VocaDbModel/DataContracts/Users/AlbumForUserForApiContract.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ public AlbumForUserForApiContract(
Album = albumForUser != null
? new AlbumForApiContract(albumForUser.Album, null, languagePreference, permissionContext, thumbPersister, fields, SongOptionalFields.None)
: null;
Rating = albumForUser?.Rating ?? 0;
Rating = albumForUser?.Rating ?? 0.0;

if (shouldShowCollectionStatus)
{
Expand Down Expand Up @@ -61,7 +61,7 @@ public AlbumForUserForApiContract(
/// Given rating, generally from 0 to 5.
/// </summary>
[DataMember]
public int Rating { get; init; }
public double Rating { get; init; }

[DataMember]
public UserForApiContract User { get; init; }
Expand Down
8 changes: 4 additions & 4 deletions VocaDbModel/Database/Queries/TagQueries.cs
Original file line number Diff line number Diff line change
Expand Up @@ -402,7 +402,7 @@ private async Task<TagStatsForApiContract> GetStatsAsync(IDatabaseContext<Tag> c
return await _cache.GetOrInsertAsync(key, CachePolicy.AbsoluteExpiration(hours: 1), async () =>
{
var artists = await GetTopUsagesAndCountAsync<ArtistTagUsage, Artist, int>(ctx, tagId, t => !t.Entry.Deleted, t => t.Entry.Id, t => t.Entry);
var albums = await GetTopUsagesAndCountAsync<AlbumTagUsage, Album, int>(ctx, tagId, t => !t.Entry.Deleted, t => t.Entry.RatingTotal, t => t.Entry);
var albums = await GetTopUsagesAndCountAsync<AlbumTagUsage, Album, double>(ctx, tagId, t => !t.Entry.Deleted, t => t.Entry.RatingTotal, t => t.Entry);
var songLists = await GetTopUsagesAndCountAsync<SongListTagUsage, SongList, int>(ctx, tagId, t => !t.Entry.Deleted, t => t.Entry.Id, t => t.Entry);
var songs = await GetTopUsagesAndCountAsync<SongTagUsage, Song, int, SongType>(ctx, tagId, EntryType.Song, (query, etm) => query.WhereHasTypeOrTag(etm));
var eventSeries = await GetTopUsagesAndCountAsync<EventSeriesTagUsage, ReleaseEventSeries, int>(ctx, tagId, t => !t.Entry.Deleted, t => t.Entry.Id, t => t.Entry, maxCount: 6);
Expand Down Expand Up @@ -720,7 +720,7 @@ public void Merge(int sourceId, int targetId)
}

diff.Description.Set(target.Description.CopyIfEmpty(source.Description));

// Targets
if (!target.NewTargets.SequenceEqual(source.NewTargets))
{
Expand Down Expand Up @@ -917,9 +917,9 @@ public TagBaseContract Update(TagForEditForApiContract contract, UploadedFileCon
if (tag.HideFromSuggestions != contract.HideFromSuggestions)
diff.HideFromSuggestions.Set();

if (!tag.NewTargets.SequenceEqual(contract.NewTargets))
if (!tag.NewTargets.SequenceEqual(contract.NewTargets))
diff.Targets.Set();

if (tag.Targets != (TagTargetTypes)contract.Targets)
diff.Targets.Set();

Expand Down
2 changes: 1 addition & 1 deletion VocaDbModel/Database/Queries/UserQueries.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1903,7 +1903,7 @@ public void UpdateAlbumForUser(
int albumId,
PurchaseStatus status,
MediaType mediaType,
int rating
double rating
)
{
PermissionContext.VerifyPermission(PermissionToken.EditProfile);
Expand Down
4 changes: 2 additions & 2 deletions VocaDbModel/Domain/Albums/Album.cs
Original file line number Diff line number Diff line change
Expand Up @@ -371,7 +371,7 @@ public virtual double RatingAverage

public virtual int RatingCount { get; set; }

public virtual int RatingTotal { get; set; }
public virtual double RatingTotal { get; set; }

public virtual IList<AlbumReview> AllReviews
{
Expand Down Expand Up @@ -943,7 +943,7 @@ public virtual void UpdateRatingTotals()
{
RatingCount = UserCollections.Where(a => a.Rating != AlbumForUser.NotRated).Count();
RatingTotal = UserCollections.Sum(a => a.Rating);
RatingAverageInt = RatingCount > 0 ? (RatingTotal * 100 / RatingCount) : 0;
RatingAverageInt = RatingCount > 0 ? (int)(RatingTotal * 100 / RatingCount) : 0;
}

public virtual Object TagSubtype()
Expand Down
6 changes: 3 additions & 3 deletions VocaDbModel/Domain/Users/AlbumForUser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ namespace VocaDb.Model.Domain.Users;

public class AlbumForUser : IAlbumLink, IEntryWithIntId
{
public const int NotRated = 0;
public const double NotRated = 0.0;

private Album _album;
private User _user;
Expand All @@ -18,7 +18,7 @@ public AlbumForUser()
PurchaseStatus = PurchaseStatus.Owned;
}

public AlbumForUser(User user, Album album, PurchaseStatus status, MediaType mediaType, int rating)
public AlbumForUser(User user, Album album, PurchaseStatus status, MediaType mediaType, double rating)
: this()
{
User = user;
Expand Down Expand Up @@ -47,7 +47,7 @@ public virtual Album Album
/// <summary>
/// Rating score, 0-5 (0 = no rating).
/// </summary>
public virtual int Rating { get; set; }
public virtual double Rating { get; set; }

public virtual User User
{
Expand Down
2 changes: 1 addition & 1 deletion VocaDbModel/Domain/Users/User.cs
Original file line number Diff line number Diff line change
Expand Up @@ -517,7 +517,7 @@ public virtual IList<UserWebLink> WebLinks
/// <param name="mediaType">Media type.</param>
/// <param name="rating">Rating.</param>
/// <returns>Album link. Cannot be null.</returns>
public virtual AlbumForUser AddAlbum(Album album, PurchaseStatus status, MediaType mediaType, int rating)
public virtual AlbumForUser AddAlbum(Album album, PurchaseStatus status, MediaType mediaType, double rating)
{
ParamIs.NotNull(() => album);

Expand Down
2 changes: 1 addition & 1 deletion VocaDbWeb/Controllers/Api/UserApiController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -668,7 +668,7 @@ public ActionResult RemoveEventTagUsage(long tagUsageId)
/// </remarks>
[HttpPost("current/albums/{albumId:int}")]
[Authorize]
public string PostAlbumStatus(int albumId, PurchaseStatus collectionStatus, MediaType mediaType, int rating)
public string PostAlbumStatus(int albumId, PurchaseStatus collectionStatus, MediaType mediaType, double rating)
{
_queries.UpdateAlbumForUser(_permissionContext.LoggedUserId, albumId, collectionStatus, mediaType, rating);
return "OK";
Expand Down
2 changes: 1 addition & 1 deletion VocaDbWeb/Controllers/UserController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -567,7 +567,7 @@ public async Task<ActionResult> ResetPassword(ResetPassword model)
}

[HttpPost]
public void UpdateAlbumForUser(int albumid, PurchaseStatus collectionStatus, MediaType mediaType, int rating)
public void UpdateAlbumForUser(int albumid, PurchaseStatus collectionStatus, MediaType mediaType, double rating)
{
Data.UpdateAlbumForUser(LoggedUserId, albumid, collectionStatus, mediaType, rating);
}
Expand Down
6 changes: 3 additions & 3 deletions VocaDbWeb/Models/AlbumModels.cs
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ public AlbumDetails(AlbumDetailsContract contract, IUserPermissionContext permis

public ArtistForAlbumContract[] Circles { get; set; }

public int CollectionRating { get; set; }
public double CollectionRating { get; set; }

public int CommentCount { get; set; }

Expand Down Expand Up @@ -167,7 +167,7 @@ public AlbumDetails(AlbumDetailsContract contract, IUserPermissionContext permis

public AlbumReviewContract LatestReview { get; set; }

public int LatestReviewRatingScore { get; set; }
public double LatestReviewRatingScore { get; set; }

public AlbumContract MergedTo { get; set; }

Expand Down Expand Up @@ -276,4 +276,4 @@ public AlbumDetailsAjax(AlbumDetails model)
public string PersonalDescriptionText { get; set; }

public TagUsageForApiContract[] TagUsages { get; set; }
}
}
111 changes: 111 additions & 0 deletions VocaDbWeb/Scripts/Components/Shared/Partials/Shared/RatingPicker.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import React from 'react';

interface RatingPickerProps {
value: number;
onChange: (value: number) => void;
max?: number;
}

interface RatingStarProps {
fill: number;
index: number;
onClick: (index: number, event: React.MouseEvent<HTMLSpanElement>) => void;
onMouseMove: (
index: number,
event: React.MouseEvent<HTMLSpanElement>,
) => void;
}

export const RatingStar = ({
fill,
index,
onClick,
onMouseMove,
}: RatingStarProps): React.ReactElement => {
return (
<span
className="rating"
style={{
position: 'relative',
display: 'inline-block',
width: 20,
height: 20,
cursor: 'pointer',
}}
onClick={(event): void => onClick(index, event)}
onMouseMove={(event): void => onMouseMove(index, event)}
>
<img
src="/Content/star_disabled.png"
style={{
position: 'absolute',
left: 0,
top: 0,
width: '100%',
}}
alt=""
/>
<div
style={{
position: 'absolute',
left: 0,
top: 0,
width: `${fill * 100}%`,
height: 20,
overflow: 'hidden',
}}
>
<img
src="/Content/star.png"
style={{
width: 20,
height: 20,
maxWidth: 'none',
}}
alt="*"
/>
</div>
</span>
);
};

export const RatingPicker = ({
value,
onChange,
max = 5,
}: RatingPickerProps): React.ReactElement => {
const [hoverValue, setHoverValue] = React.useState<number | null>(null);
const displayValue = hoverValue ?? value;

const handleClick = (index: number, event: React.MouseEvent<HTMLSpanElement>): void => {
const rect = event.currentTarget.getBoundingClientRect();
const x = event.clientX - rect.left;
const nextValue = Math.min(max, Math.max(0.5, index + (x < rect.width / 2 ? 0.5 : 1)));
onChange(nextValue);
};

const handleMouseMove = (index: number, event: React.MouseEvent<HTMLSpanElement>): void => {
const rect = event.currentTarget.getBoundingClientRect();
const x = event.clientX - rect.left;
const nextValue = Math.min(max, Math.max(0.5, index + (x < rect.width / 2 ? 0.5 : 1)));
setHoverValue(nextValue);
};

return (
<span onMouseLeave={(): void => setHoverValue(null)} style={{ display: 'inline-flex' }}>
{Array.from({ length: max }, (_, index) => {
const fill = Math.max(0, Math.min(1, displayValue - index));
return (
<RatingStar
key={index}
fill={fill}
index={index}
onClick={handleClick}
onMouseMove={handleMouseMove}
/>
);
})}
</span>
);
};

Loading