mirror of
https://github.com/immich-app/immich.git
synced 2025-12-12 07:41:02 -08:00
feat(mobile): star rating
This commit is contained in:
@@ -6,6 +6,7 @@ class ExifInfo {
|
||||
final String? orientation;
|
||||
final String? timeZone;
|
||||
final DateTime? dateTimeOriginal;
|
||||
final int? rating;
|
||||
|
||||
// GPS
|
||||
final double? latitude;
|
||||
@@ -46,6 +47,7 @@ class ExifInfo {
|
||||
this.orientation,
|
||||
this.timeZone,
|
||||
this.dateTimeOriginal,
|
||||
this.rating,
|
||||
this.isFlipped = false,
|
||||
this.latitude,
|
||||
this.longitude,
|
||||
@@ -71,6 +73,7 @@ class ExifInfo {
|
||||
other.orientation == orientation &&
|
||||
other.timeZone == timeZone &&
|
||||
other.dateTimeOriginal == dateTimeOriginal &&
|
||||
other.rating == rating &&
|
||||
other.latitude == latitude &&
|
||||
other.longitude == longitude &&
|
||||
other.city == city &&
|
||||
@@ -94,6 +97,7 @@ class ExifInfo {
|
||||
isFlipped.hashCode ^
|
||||
timeZone.hashCode ^
|
||||
dateTimeOriginal.hashCode ^
|
||||
rating.hashCode ^
|
||||
latitude.hashCode ^
|
||||
longitude.hashCode ^
|
||||
city.hashCode ^
|
||||
@@ -118,6 +122,7 @@ orientation: ${orientation ?? 'NA'},
|
||||
isFlipped: $isFlipped,
|
||||
timeZone: ${timeZone ?? 'NA'},
|
||||
dateTimeOriginal: ${dateTimeOriginal ?? 'NA'},
|
||||
rating: ${rating ?? 'NA'},
|
||||
latitude: ${latitude ?? 'NA'},
|
||||
longitude: ${longitude ?? 'NA'},
|
||||
city: ${city ?? 'NA'},
|
||||
@@ -140,6 +145,7 @@ exposureSeconds: ${exposureSeconds ?? 'NA'},
|
||||
String? orientation,
|
||||
String? timeZone,
|
||||
DateTime? dateTimeOriginal,
|
||||
int? rating,
|
||||
double? latitude,
|
||||
double? longitude,
|
||||
String? city,
|
||||
@@ -161,6 +167,7 @@ exposureSeconds: ${exposureSeconds ?? 'NA'},
|
||||
orientation: orientation ?? this.orientation,
|
||||
timeZone: timeZone ?? this.timeZone,
|
||||
dateTimeOriginal: dateTimeOriginal ?? this.dateTimeOriginal,
|
||||
rating: rating ?? this.rating,
|
||||
isFlipped: isFlipped ?? this.isFlipped,
|
||||
latitude: latitude ?? this.latitude,
|
||||
longitude: longitude ?? this.longitude,
|
||||
|
||||
@@ -151,6 +151,7 @@ extension RemoteExifEntityDataDomainEx on RemoteExifEntityData {
|
||||
domain.ExifInfo toDto() => domain.ExifInfo(
|
||||
fileSize: fileSize,
|
||||
dateTimeOriginal: dateTimeOriginal,
|
||||
rating: rating,
|
||||
timeZone: timeZone,
|
||||
make: make,
|
||||
model: model,
|
||||
|
||||
@@ -255,6 +255,12 @@ class RemoteAssetRepository extends DriftDatabaseRepository {
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> updateRating(String assetId, int rating) async {
|
||||
await (_db.remoteExifEntity.update()..where((row) => row.assetId.equals(assetId))).write(
|
||||
RemoteExifEntityCompanion(rating: Value(rating)),
|
||||
);
|
||||
}
|
||||
|
||||
Future<int> getCount() {
|
||||
return _db.managers.remoteAssetEntity.count();
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import 'package:auto_route/auto_route.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_rating_bar/flutter_rating_bar.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/constants/enums.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
@@ -23,6 +24,7 @@ import 'package:immich_mobile/providers/infrastructure/album.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/current_album.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/setting.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/user_metadata.provider.dart';
|
||||
import 'package:immich_mobile/providers/routes.provider.dart';
|
||||
import 'package:immich_mobile/providers/server_info.provider.dart';
|
||||
import 'package:immich_mobile/providers/user.provider.dart';
|
||||
@@ -54,6 +56,12 @@ class AssetDetailBottomSheet extends ConsumerWidget {
|
||||
final currentAlbum = ref.watch(currentRemoteAlbumProvider);
|
||||
final isArchived = asset is RemoteAsset && asset.visibility == AssetVisibility.archive;
|
||||
final advancedTroubleshooting = ref.watch(settingsProvider.notifier).get(Setting.advancedTroubleshooting);
|
||||
final isRatingEnabled = ref
|
||||
.watch(userMetadataProvider(ref.watch(currentUserProvider)?.id ?? ''))
|
||||
.maybeWhen(
|
||||
data: (metadataList) => metadataList.any((meta) => meta.preferences?.ratingsEnabled ?? false),
|
||||
orElse: () => false,
|
||||
);
|
||||
|
||||
final buttonContext = ActionButtonContext(
|
||||
asset: asset,
|
||||
@@ -71,7 +79,7 @@ class AssetDetailBottomSheet extends ConsumerWidget {
|
||||
|
||||
return BaseBottomSheet(
|
||||
actions: actions,
|
||||
slivers: const [_AssetDetailBottomSheet()],
|
||||
slivers: [_AssetDetailBottomSheet(isRatingEnabled: isRatingEnabled)],
|
||||
controller: controller,
|
||||
initialChildSize: initialChildSize,
|
||||
minChildSize: 0.1,
|
||||
@@ -85,7 +93,9 @@ class AssetDetailBottomSheet extends ConsumerWidget {
|
||||
}
|
||||
|
||||
class _AssetDetailBottomSheet extends ConsumerWidget {
|
||||
const _AssetDetailBottomSheet();
|
||||
final bool isRatingEnabled;
|
||||
|
||||
const _AssetDetailBottomSheet({required this.isRatingEnabled});
|
||||
|
||||
String _getDateTime(BuildContext ctx, BaseAsset asset, ExifInfo? exifInfo) {
|
||||
DateTime dateTime = asset.createdAt.toLocal();
|
||||
@@ -323,6 +333,36 @@ class _AssetDetailBottomSheet extends ConsumerWidget {
|
||||
),
|
||||
),
|
||||
],
|
||||
// Rating bar
|
||||
if (isRatingEnabled) ...[
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 16.0),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'rating'.t(context: context).toUpperCase(),
|
||||
style: context.textTheme.labelMedium?.copyWith(
|
||||
color: context.textTheme.labelMedium?.color?.withAlpha(200),
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
RatingBar.builder(
|
||||
initialRating: exifInfo?.rating?.toDouble() ?? 0,
|
||||
itemBuilder: (context, _) => Icon(Icons.star, color: context.themeData.colorScheme.primary),
|
||||
itemSize: 32,
|
||||
glow: false,
|
||||
onRatingUpdate: (rating) async {
|
||||
await ref.read(actionProvider.notifier).updateRating(ActionSource.viewer, rating.round());
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
// Appears in (Albums)
|
||||
Padding(padding: const EdgeInsets.only(top: 16.0), child: _buildAppearsInList(ref, context)),
|
||||
// padding at the bottom to avoid cut-off
|
||||
|
||||
@@ -357,6 +357,22 @@ class ActionNotifier extends Notifier<void> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<ActionResult> updateRating(ActionSource source, int rating) async {
|
||||
final ids = _getRemoteIdsForSource(source);
|
||||
if (ids.length != 1) {
|
||||
_logger.warning('updateRating called with multiple assets, expected single asset');
|
||||
return ActionResult(count: ids.length, success: false, error: 'Expected single asset for rating update');
|
||||
}
|
||||
|
||||
try {
|
||||
final isUpdated = await _service.updateRating(ids.first, rating);
|
||||
return ActionResult(count: 1, success: isUpdated);
|
||||
} catch (error, stack) {
|
||||
_logger.severe('Failed to update rating for asset', error, stack);
|
||||
return ActionResult(count: 1, success: false, error: error.toString());
|
||||
}
|
||||
}
|
||||
|
||||
Future<ActionResult> stack(String userId, ActionSource source) async {
|
||||
final ids = _getOwnedRemoteIdsForSource(source);
|
||||
try {
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/domain/models/user_metadata.model.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/user_metadata.repository.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
|
||||
|
||||
final userMetadataRepository = Provider<DriftUserMetadataRepository>(
|
||||
(ref) => DriftUserMetadataRepository(ref.watch(driftProvider)),
|
||||
);
|
||||
|
||||
final userMetadataProvider = FutureProvider.family<List<UserMetadata>, String>((ref, String userId) async {
|
||||
final repository = ref.watch(userMetadataRepository);
|
||||
return repository.getUserMetadata(userId);
|
||||
});
|
||||
|
||||
@@ -101,6 +101,10 @@ class AssetApiRepository extends ApiRepository {
|
||||
Future<void> updateDescription(String assetId, String description) {
|
||||
return _api.updateAsset(assetId, UpdateAssetDto(description: description));
|
||||
}
|
||||
|
||||
Future<void> updateRating(String assetId, int rating) {
|
||||
return _api.updateAsset(assetId, UpdateAssetDto(rating: rating));
|
||||
}
|
||||
}
|
||||
|
||||
extension on StackResponseDto {
|
||||
|
||||
@@ -225,6 +225,14 @@ class ActionService {
|
||||
return true;
|
||||
}
|
||||
|
||||
Future<bool> updateRating(String assetId, int rating) async {
|
||||
// update remote first, then local to ensure consistency
|
||||
await _assetApiRepository.updateRating(assetId, rating);
|
||||
await _remoteAssetRepository.updateRating(assetId, rating);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
Future<void> stack(String userId, List<String> remoteIds) async {
|
||||
final stack = await _assetApiRepository.stack(remoteIds);
|
||||
await _remoteAssetRepository.stack(userId, stack);
|
||||
|
||||
@@ -665,6 +665,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.27"
|
||||
flutter_rating_bar:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_rating_bar
|
||||
sha256: d2af03469eac832c591a1eba47c91ecc871fe5708e69967073c043b2d775ed93
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.0.1"
|
||||
flutter_riverpod:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
||||
@@ -33,6 +33,7 @@ dependencies:
|
||||
flutter_displaymode: ^0.7.0
|
||||
flutter_hooks: ^0.21.3+1
|
||||
flutter_local_notifications: ^17.2.1+2
|
||||
flutter_rating_bar: ^4.0.1
|
||||
flutter_secure_storage: ^9.2.4
|
||||
flutter_svg: ^2.2.1
|
||||
flutter_udid: ^4.0.0
|
||||
|
||||
Reference in New Issue
Block a user