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
4 changes: 4 additions & 0 deletions lib/data/models/aggregated_item.dart
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,10 @@ class AggregatedItem {

bool get isFavorite => _userData?['IsFavorite'] as bool? ?? false;

double? get personalRating => (_userData?['Rating'] as num?)?.toDouble();

bool? get personalRatingLikes => _userData?['Likes'] as bool?;

int? get playbackPositionTicks => _toInt(_userData?['PlaybackPositionTicks']);

Duration? get playbackPosition => playbackPositionTicks != null
Expand Down
11 changes: 11 additions & 0 deletions lib/data/offline/offline_stub_apis.dart
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,9 @@ class OfflineUserLibraryApi implements UserLibraryApi {

OfflineUserLibraryApi(this._catalog);

@override
bool get supportsNumericUserRatings => false;

@override
Future<Map<String, dynamic>> getItem(String itemId) async {
final entry = _catalog.byId(itemId);
Expand Down Expand Up @@ -85,6 +88,14 @@ class OfflineUserLibraryApi implements UserLibraryApi {
throw offlineUnavailable('/UserItems/$itemId/Rating');
}

@override
Future<void> updateNumericUserRating(
String itemId, {
required double rating,
}) async {
throw offlineUnavailable('/UserItems/$itemId/UserData');
}

@override
Future<void> deleteUserRating(String itemId) async {
throw offlineUnavailable('/UserItems/$itemId/Rating');
Expand Down
7 changes: 7 additions & 0 deletions lib/data/repositories/item_mutation_repository.dart
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,13 @@ class ItemMutationRepository {
await _client.userLibraryApi.updateUserRating(itemId, likes: likes);
}

Future<void> setNumericRating(String itemId, {required double rating}) async {
await _client.userLibraryApi.updateNumericUserRating(
itemId,
rating: rating,
);
}

Future<void> clearRating(String itemId) async {
await _client.userLibraryApi.deleteUserRating(itemId);
}
Expand Down
72 changes: 72 additions & 0 deletions lib/data/viewmodels/item_detail_view_model.dart
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,11 @@ class ItemDetailViewModel extends ChangeNotifier {

ImageApi get imageApi => _client.imageApi;
String get baseUrl => _client.baseUrl;
bool get supportsNumericUserRatings =>
_client.userLibraryApi.supportsNumericUserRatings;

bool _isRatingMutationInProgress = false;
bool get isRatingMutationInProgress => _isRatingMutationInProgress;

bool get canManagePlaylistTracks =>
_item?.type == 'Playlist' &&
Expand Down Expand Up @@ -1097,6 +1102,73 @@ class ItemDetailViewModel extends ChangeNotifier {
}
}

Future<void> setThumbRating(bool likes) async {
return _mutateRating(
{'Rating': likes ? 10.0 : 1.0, 'Likes': likes},
() => _mutations.setRating(itemId, likes: likes),
);
}

Future<void> setNumericRating(double rating) async {
if (_isRatingMutationInProgress) return;
if (!rating.isFinite || rating < 0 || rating > 10) {
throw ArgumentError.value(rating, 'rating', 'must be between 0 and 10');
}
return _mutateRating(
{'Rating': rating, 'Likes': rating >= 6.5},
() => _mutations.setNumericRating(itemId, rating: rating),
);
}

Future<void> clearRating() async {
return _mutateRating(
{'Rating': null, 'Likes': null},
() => _mutations.clearRating(itemId),
);
}

Future<void> _mutateRating(
Map<String, dynamic> userDataPatch,
Future<void> Function() mutation,
) async {
if (_isRatingMutationInProgress) return;
final previousUserData = _copyUserData();
_isRatingMutationInProgress = true;
try {
_applyOptimisticUpdate(userDataPatch);
await mutation();
await _reload();
} catch (_) {
_restoreUserData(previousUserData);
rethrow;
} finally {
_isRatingMutationInProgress = false;
notifyListeners();
}
}

Map<String, dynamic>? _copyUserData() {
final userData = _item?.rawData['UserData'];
return userData is Map ? Map<String, dynamic>.from(userData) : null;
}

void _restoreUserData(Map<String, dynamic>? userData) {
final item = _item;
if (item == null) return;
final updatedRaw = Map<String, dynamic>.from(item.rawData);
if (userData == null) {
updatedRaw.remove('UserData');
} else {
updatedRaw['UserData'] = userData;
}
_item = AggregatedItem(
id: item.id,
serverId: item.serverId,
rawData: updatedRaw,
);
notifyListeners();
}

void _applyOptimisticUpdate(Map<String, dynamic> userDataPatch) {
final item = _item;
if (item == null) return;
Expand Down
60 changes: 59 additions & 1 deletion lib/l10n/app_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -8772,5 +8772,63 @@
"imdbMostPopularMovies": "IMDb Most Popular Movies",
"imdbMostPopularTvShows": "IMDb Most Popular TV Shows",
"imdbLowestRatedMovies": "IMDb Lowest Rated Movies",
"imdbTopEnglishMovies": "IMDb Top Rated English Movies"
"imdbTopEnglishMovies": "IMDb Top Rated English Movies",
"personalRatingStyle": "Personal rating style",
"@personalRatingStyle": {
"description": "Selects how personal ratings are displayed and entered on media details."
},
"personalRatingThumbs": "Like / dislike",
"@personalRatingThumbs": {
"description": "Personal rating style using like and dislike actions."
},
"personalRatingStars": "5 stars",
"@personalRatingStars": {
"description": "Personal rating style using a five-star scale."
},
"personalRatingNumeric": "Numeric score out of 10",
"@personalRatingNumeric": {
"description": "Personal rating style using a numeric score out of ten."
},
"rate": "Rate",
"@rate": {
"description": "Action that opens the personal rating control."
},
"like": "Like",
"@like": {
"description": "Personal rating action for a positive rating."
},
"dislike": "Dislike",
"@dislike": {
"description": "Personal rating action for a negative rating."
},
"personalRatingClear": "Clear rating",
"@personalRatingClear": {
"description": "Action that removes the personal rating from the media item."
},
"personalRatingRated": "Rated",
"@personalRatingRated": {
"description": "Tooltip for a media item that has a numeric personal rating."
},
"personalRatingSaveFailed": "Could not save rating",
"@personalRatingSaveFailed": {
"description": "Error shown when saving a personal rating fails."
},
"increase": "Increase",
"@increase": {
"description": "Action that increases a numeric personal rating."
},
"decrease": "Decrease",
"@decrease": {
"description": "Action that decreases a numeric personal rating."
},
"personalRatingOutOfTen": "{rating} / 10",
"@personalRatingOutOfTen": {
"description": "Numeric personal rating displayed on a scale out of ten.",
"placeholders": { "rating": { "type": "String" } }
},
"personalRatingOutOfFive": "{rating} / 5",
"@personalRatingOutOfFive": {
"description": "Personal rating displayed on a five-star scale.",
"placeholders": { "rating": { "type": "String" } }
}
}
84 changes: 84 additions & 0 deletions lib/l10n/app_localizations.dart
Original file line number Diff line number Diff line change
Expand Up @@ -17595,6 +17595,90 @@ abstract class AppLocalizations {
/// In en, this message translates to:
/// **'IMDb Top Rated English Movies'**
String get imdbTopEnglishMovies;

/// Selects how personal ratings are displayed and entered on media details.
///
/// In en, this message translates to:
/// **'Personal rating style'**
String get personalRatingStyle;

/// Personal rating style using like and dislike actions.
///
/// In en, this message translates to:
/// **'Like / dislike'**
String get personalRatingThumbs;

/// Personal rating style using a five-star scale.
///
/// In en, this message translates to:
/// **'5 stars'**
String get personalRatingStars;

/// Personal rating style using a numeric score out of ten.
///
/// In en, this message translates to:
/// **'Numeric score out of 10'**
String get personalRatingNumeric;

/// Action that opens the personal rating control.
///
/// In en, this message translates to:
/// **'Rate'**
String get rate;

/// Personal rating action for a positive rating.
///
/// In en, this message translates to:
/// **'Like'**
String get like;

/// Personal rating action for a negative rating.
///
/// In en, this message translates to:
/// **'Dislike'**
String get dislike;

/// Action that removes the personal rating from the media item.
///
/// In en, this message translates to:
/// **'Clear rating'**
String get personalRatingClear;

/// Tooltip for a media item that has a numeric personal rating.
///
/// In en, this message translates to:
/// **'Rated'**
String get personalRatingRated;

/// Error shown when saving a personal rating fails.
///
/// In en, this message translates to:
/// **'Could not save rating'**
String get personalRatingSaveFailed;

/// Action that increases a numeric personal rating.
///
/// In en, this message translates to:
/// **'Increase'**
String get increase;

/// Action that decreases a numeric personal rating.
///
/// In en, this message translates to:
/// **'Decrease'**
String get decrease;

/// Numeric personal rating displayed on a scale out of ten.
///
/// In en, this message translates to:
/// **'{rating} / 10'**
String personalRatingOutOfTen(String rating);

/// Personal rating displayed on a five-star scale.
///
/// In en, this message translates to:
/// **'{rating} / 5'**
String personalRatingOutOfFive(String rating);
}

class _AppLocalizationsDelegate
Expand Down
46 changes: 46 additions & 0 deletions lib/l10n/app_localizations_af.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9897,4 +9897,50 @@ class AppLocalizationsAf extends AppLocalizations {

@override
String get imdbTopEnglishMovies => 'IMDb Top Rated English Movies';

@override
String get personalRatingStyle => 'Personal rating style';

@override
String get personalRatingThumbs => 'Like / dislike';

@override
String get personalRatingStars => '5 stars';

@override
String get personalRatingNumeric => 'Numeric score out of 10';

@override
String get rate => 'Rate';

@override
String get like => 'Like';

@override
String get dislike => 'Dislike';

@override
String get personalRatingClear => 'Clear rating';

@override
String get personalRatingRated => 'Rated';

@override
String get personalRatingSaveFailed => 'Could not save rating';

@override
String get increase => 'Increase';

@override
String get decrease => 'Decrease';

@override
String personalRatingOutOfTen(String rating) {
return '$rating / 10';
}

@override
String personalRatingOutOfFive(String rating) {
return '$rating / 5';
}
}
46 changes: 46 additions & 0 deletions lib/l10n/app_localizations_ar.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9843,4 +9843,50 @@ class AppLocalizationsAr extends AppLocalizations {

@override
String get imdbTopEnglishMovies => 'IMDb Top Rated English Movies';

@override
String get personalRatingStyle => 'Personal rating style';

@override
String get personalRatingThumbs => 'Like / dislike';

@override
String get personalRatingStars => '5 stars';

@override
String get personalRatingNumeric => 'Numeric score out of 10';

@override
String get rate => 'Rate';

@override
String get like => 'Like';

@override
String get dislike => 'Dislike';

@override
String get personalRatingClear => 'Clear rating';

@override
String get personalRatingRated => 'Rated';

@override
String get personalRatingSaveFailed => 'Could not save rating';

@override
String get increase => 'Increase';

@override
String get decrease => 'Decrease';

@override
String personalRatingOutOfTen(String rating) {
return '$rating / 10';
}

@override
String personalRatingOutOfFive(String rating) {
return '$rating / 5';
}
}
Loading
Loading