refactor: use custom rating bar & provider

This commit is contained in:
Yaros
2025-12-10 19:13:13 +01:00
parent 46e2d6e71e
commit 63c1c9e376
5 changed files with 99 additions and 23 deletions

View File

@@ -4,7 +4,6 @@ import 'package:auto_route/auto_route.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_rating_bar/flutter_rating_bar.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/constants/enums.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
@@ -17,6 +16,7 @@ import 'package:immich_mobile/presentation/widgets/album/album_tile.dart';
import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.state.dart'; import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.state.dart';
import 'package:immich_mobile/presentation/widgets/asset_viewer/bottom_sheet/sheet_location_details.widget.dart'; import 'package:immich_mobile/presentation/widgets/asset_viewer/bottom_sheet/sheet_location_details.widget.dart';
import 'package:immich_mobile/presentation/widgets/asset_viewer/bottom_sheet/sheet_people_details.widget.dart'; import 'package:immich_mobile/presentation/widgets/asset_viewer/bottom_sheet/sheet_people_details.widget.dart';
import 'package:immich_mobile/presentation/widgets/asset_viewer/rating_bar.widget.dart';
import 'package:immich_mobile/presentation/widgets/asset_viewer/sheet_tile.widget.dart'; import 'package:immich_mobile/presentation/widgets/asset_viewer/sheet_tile.widget.dart';
import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart'; import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart';
import 'package:immich_mobile/providers/infrastructure/action.provider.dart'; import 'package:immich_mobile/providers/infrastructure/action.provider.dart';
@@ -56,12 +56,6 @@ class AssetDetailBottomSheet extends ConsumerWidget {
final currentAlbum = ref.watch(currentRemoteAlbumProvider); final currentAlbum = ref.watch(currentRemoteAlbumProvider);
final isArchived = asset is RemoteAsset && asset.visibility == AssetVisibility.archive; final isArchived = asset is RemoteAsset && asset.visibility == AssetVisibility.archive;
final advancedTroubleshooting = ref.watch(settingsProvider.notifier).get(Setting.advancedTroubleshooting); 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( final buttonContext = ActionButtonContext(
asset: asset, asset: asset,
@@ -79,7 +73,7 @@ class AssetDetailBottomSheet extends ConsumerWidget {
return BaseBottomSheet( return BaseBottomSheet(
actions: actions, actions: actions,
slivers: [_AssetDetailBottomSheet(isRatingEnabled: isRatingEnabled)], slivers: [const _AssetDetailBottomSheet()],
controller: controller, controller: controller,
initialChildSize: initialChildSize, initialChildSize: initialChildSize,
minChildSize: 0.1, minChildSize: 0.1,
@@ -93,9 +87,7 @@ class AssetDetailBottomSheet extends ConsumerWidget {
} }
class _AssetDetailBottomSheet extends ConsumerWidget { class _AssetDetailBottomSheet extends ConsumerWidget {
final bool isRatingEnabled; const _AssetDetailBottomSheet();
const _AssetDetailBottomSheet({required this.isRatingEnabled});
String _getDateTime(BuildContext ctx, BaseAsset asset, ExifInfo? exifInfo) { String _getDateTime(BuildContext ctx, BaseAsset asset, ExifInfo? exifInfo) {
DateTime dateTime = asset.createdAt.toLocal(); DateTime dateTime = asset.createdAt.toLocal();
@@ -243,6 +235,9 @@ class _AssetDetailBottomSheet extends ConsumerWidget {
final cameraTitle = _getCameraInfoTitle(exifInfo); final cameraTitle = _getCameraInfoTitle(exifInfo);
final lensTitle = exifInfo?.lens != null && exifInfo!.lens!.isNotEmpty ? exifInfo.lens : null; final lensTitle = exifInfo?.lens != null && exifInfo!.lens!.isNotEmpty ? exifInfo.lens : null;
final isOwner = ref.watch(currentUserProvider)?.id == (asset is RemoteAsset ? asset.ownerId : null); final isOwner = ref.watch(currentUserProvider)?.id == (asset is RemoteAsset ? asset.ownerId : null);
final isRatingEnabled = ref
.watch(userMetadataPreferencesProvider(ref.watch(currentUserProvider)?.id ?? ''))
.maybeWhen(data: (prefs) => prefs?.ratingsEnabled ?? false, orElse: () => false);
// Build file info tile based on asset type // Build file info tile based on asset type
Widget buildFileInfoTile() { Widget buildFileInfoTile() {
@@ -350,11 +345,11 @@ class _AssetDetailBottomSheet extends ConsumerWidget {
), ),
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
RatingBar.builder( RatingBar(
initialRating: exifInfo?.rating?.toDouble() ?? 0, initialRating: exifInfo?.rating?.toDouble() ?? 0,
itemBuilder: (context, _) => Icon(Icons.star, color: context.themeData.colorScheme.primary), filledColor: context.themeData.colorScheme.primary,
unfilledColor: context.themeData.colorScheme.onSurface.withAlpha(100),
itemSize: 32, itemSize: 32,
glow: false,
onRatingUpdate: (rating) async { onRatingUpdate: (rating) async {
await ref.read(actionProvider.notifier).updateRating(ActionSource.viewer, rating.round()); await ref.read(actionProvider.notifier).updateRating(ActionSource.viewer, rating.round());
}, },

View File

@@ -0,0 +1,84 @@
import 'package:flutter/material.dart';
class RatingBar extends StatefulWidget {
final double initialRating;
final int itemCount;
final double itemSize;
final Color filledColor;
final Color unfilledColor;
final ValueChanged<int>? onRatingUpdate;
final Widget? itemBuilder;
const RatingBar({
super.key,
this.initialRating = 0.0,
this.itemCount = 5,
this.itemSize = 40.0,
this.filledColor = Colors.amber,
this.unfilledColor = Colors.grey,
this.onRatingUpdate,
this.itemBuilder,
});
@override
State<RatingBar> createState() => _RatingBarState();
}
class _RatingBarState extends State<RatingBar> {
late double _currentRating;
@override
void initState() {
super.initState();
_currentRating = widget.initialRating;
}
void _updateRating(Offset localPosition, bool isRTL, {bool isTap = false}) {
final totalWidth = widget.itemCount * widget.itemSize;
double dx = localPosition.dx;
if (isRTL) dx = totalWidth - dx;
double newRating;
if (dx <= 0) {
newRating = 0;
} else if (dx >= totalWidth) {
newRating = widget.itemCount.toDouble();
} else {
int tappedIndex = (dx ~/ widget.itemSize).clamp(0, widget.itemCount - 1);
newRating = tappedIndex + 1.0;
if (isTap && newRating == _currentRating && _currentRating != 0) {
newRating = 0;
}
}
if (_currentRating != newRating) {
setState(() {
_currentRating = newRating;
});
widget.onRatingUpdate?.call(newRating.round());
}
}
@override
Widget build(BuildContext context) {
final isRTL = Directionality.of(context) == TextDirection.rtl;
return GestureDetector(
behavior: HitTestBehavior.opaque,
onTapDown: (details) => _updateRating(details.localPosition, isRTL, isTap: true),
onPanUpdate: (details) => _updateRating(details.localPosition, isRTL, isTap: false),
child: Row(
mainAxisSize: MainAxisSize.min,
textDirection: isRTL ? TextDirection.rtl : TextDirection.ltr,
children: List.generate(widget.itemCount, (index) {
bool filled = _currentRating > index;
return widget.itemBuilder ??
Icon(Icons.star, size: widget.itemSize, color: filled ? widget.filledColor : widget.unfilledColor);
}),
),
);
}
}

View File

@@ -11,3 +11,9 @@ final userMetadataProvider = FutureProvider.family<List<UserMetadata>, String>((
final repository = ref.watch(userMetadataRepository); final repository = ref.watch(userMetadataRepository);
return repository.getUserMetadata(userId); return repository.getUserMetadata(userId);
}); });
final userMetadataPreferencesProvider = FutureProvider.family<Preferences?, String>((ref, String userId) async {
final metadataList = await ref.watch(userMetadataProvider(userId).future);
final metadataWithPrefs = metadataList.firstWhere((meta) => meta.preferences != null);
return metadataWithPrefs.preferences;
});

View File

@@ -665,14 +665,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.0.27" 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: flutter_riverpod:
dependency: transitive dependency: transitive
description: description:

View File

@@ -33,7 +33,6 @@ dependencies:
flutter_displaymode: ^0.7.0 flutter_displaymode: ^0.7.0
flutter_hooks: ^0.21.3+1 flutter_hooks: ^0.21.3+1
flutter_local_notifications: ^17.2.1+2 flutter_local_notifications: ^17.2.1+2
flutter_rating_bar: ^4.0.1
flutter_secure_storage: ^9.2.4 flutter_secure_storage: ^9.2.4
flutter_svg: ^2.2.1 flutter_svg: ^2.2.1
flutter_udid: ^4.0.0 flutter_udid: ^4.0.0