Fix localization imports and linting issues

This commit is contained in:
laurenspriem
2025-09-02 10:06:23 +05:30
parent 52e3f22abf
commit 5446f8dd68
4 changed files with 268 additions and 251 deletions

View File

@@ -1,5 +1,4 @@
import 'package:photos/models/file/file.dart';
import 'package:photos/models/similar_files.dart';
enum SwipeDecision { keep, delete, undecided }

View File

@@ -5,15 +5,16 @@ import 'package:flutter_animate/flutter_animate.dart';
import 'package:flutter_card_swiper/flutter_card_swiper.dart';
import 'package:logging/logging.dart';
import 'package:photos/core/configuration.dart';
import 'package:photos/ente_theme_data.dart';
import 'package:photos/generated/l10n.dart';
import 'package:photos/models/file/file.dart';
import 'package:photos/models/similar_files.dart';
import 'package:photos/services/collections_service.dart';
import 'package:photos/theme/ente_theme.dart';
import 'package:photos/ui/pages/library_culling/models/swipe_culling_state.dart';
import 'package:photos/ui/pages/library_culling/widgets/swipeable_photo_card.dart';
import 'package:photos/ui/pages/library_culling/widgets/group_carousel.dart';
import 'package:photos/ui/pages/library_culling/widgets/group_summary_popup.dart';
import 'package:photos/ui/pages/library_culling/widgets/swipeable_photo_card.dart';
import 'package:photos/utils/delete_file_util.dart';
import 'package:photos/utils/dialog_util.dart';
import 'package:photos/utils/standalone/data.dart';
@@ -22,28 +23,28 @@ class SwipeCullingPage extends StatefulWidget {
final List<SimilarFiles> similarFiles;
const SwipeCullingPage({
Key? key,
super.key,
required this.similarFiles,
}) : super(key: key);
});
@override
State<SwipeCullingPage> createState() => _SwipeCullingPageState();
}
class _SwipeCullingPageState extends State<SwipeCullingPage>
class _SwipeCullingPageState extends State<SwipeCullingPage>
with TickerProviderStateMixin {
final _logger = Logger("SwipeCullingPage");
late List<SimilarFiles> groups;
int currentGroupIndex = 0;
int currentImageIndex = 0;
Map<EnteFile, SwipeDecision> decisions = {};
Map<int, List<SwipeAction>> groupHistories = {};
List<SwipeAction> fullHistory = [];
final CardSwiperController controller = CardSwiperController();
late ValueNotifier<String> _deleteProgress;
// Animation controllers for celebrations
late AnimationController _celebrationController;
late AnimationController _progressRingController;
@@ -63,7 +64,7 @@ class _SwipeCullingPageState extends State<SwipeCullingPage>
);
_initializeGroups();
}
@override
void dispose() {
_deleteProgress.dispose();
@@ -78,7 +79,7 @@ class _SwipeCullingPageState extends State<SwipeCullingPage>
groups = widget.similarFiles
.where((g) => g.files.length > 1 && g.files.length < 50)
.toList();
// Initialize all as undecided
for (final group in groups) {
for (final file in group.files) {
@@ -111,11 +112,11 @@ class _SwipeCullingPageState extends State<SwipeCullingPage>
if (groupIndex >= groups.length) {
return GroupProgress(totalImages: 0, reviewedImages: 0, deletionCount: 0);
}
final group = groups[groupIndex];
int reviewed = 0;
int toDelete = 0;
for (final file in group.files) {
final decision = decisions[file] ?? SwipeDecision.undecided;
if (decision != SwipeDecision.undecided) {
@@ -125,7 +126,7 @@ class _SwipeCullingPageState extends State<SwipeCullingPage>
}
}
}
return GroupProgress(
totalImages: group.files.length,
reviewedImages: reviewed,
@@ -142,14 +143,14 @@ class _SwipeCullingPageState extends State<SwipeCullingPage>
setState(() {
decisions[file] = decision;
final action = SwipeAction(
file: file,
decision: decision,
timestamp: DateTime.now(),
groupIndex: currentGroupIndex,
);
groupHistories[currentGroupIndex]?.add(action);
fullHistory.add(action);
@@ -159,7 +160,7 @@ class _SwipeCullingPageState extends State<SwipeCullingPage>
} else {
// Group complete - check if all images marked for deletion
final groupProgress = getGroupProgress(currentGroupIndex);
if (groupProgress.deletionCount == groupProgress.totalImages &&
if (groupProgress.deletionCount == groupProgress.totalImages &&
groupProgress.totalImages > 0) {
_showAllInGroupDeletionDialog();
} else {
@@ -171,20 +172,20 @@ class _SwipeCullingPageState extends State<SwipeCullingPage>
void _handleGroupCompletion() async {
if (_showingCelebration) return;
// Haptic feedback
HapticFeedback.mediumImpact();
setState(() {
_showingCelebration = true;
});
// Start progress ring animation
_progressRingController.forward();
// Wait for progress ring to complete or user to skip
await Future.delayed(const Duration(seconds: 2));
// Quick celebration based on group size
final groupSize = currentGroupFiles.length;
if (groupSize <= 5) {
@@ -194,10 +195,10 @@ class _SwipeCullingPageState extends State<SwipeCullingPage>
} else {
_celebrationController.duration = const Duration(milliseconds: 800);
}
_celebrationController.forward();
await Future.delayed(const Duration(milliseconds: 300));
// Move to next group or show completion
if (currentGroupIndex < groups.length - 1) {
setState(() {
@@ -214,15 +215,15 @@ class _SwipeCullingPageState extends State<SwipeCullingPage>
void _showAllInGroupDeletionDialog() {
final groupSize = currentGroupFiles.length;
showDialog(
context: context,
builder: (context) {
final theme = getEnteColorScheme(context);
return AlertDialog(
title: Text(S.of(context).deleteAllInGroup),
title: Text(AppLocalizations.of(context).deleteAllInGroup),
content: Text(
S.of(context).allImagesMarkedForDeletion(groupSize),
AppLocalizations.of(context).allImagesMarkedForDeletion(groupSize),
),
actions: [
TextButton(
@@ -237,7 +238,7 @@ class _SwipeCullingPageState extends State<SwipeCullingPage>
groupHistories[currentGroupIndex]?.clear();
});
},
child: Text(S.of(context).reviewAgain),
child: Text(AppLocalizations.of(context).reviewAgain),
),
TextButton(
onPressed: () {
@@ -247,7 +248,7 @@ class _SwipeCullingPageState extends State<SwipeCullingPage>
style: TextButton.styleFrom(
foregroundColor: theme.warning700,
),
child: Text(S.of(context).delete),
child: Text(AppLocalizations.of(context).delete),
),
],
);
@@ -258,27 +259,27 @@ class _SwipeCullingPageState extends State<SwipeCullingPage>
void _showCompletionDialog() {
final filesToDelete = <EnteFile>{};
int totalSize = 0;
for (final entry in decisions.entries) {
if (entry.value == SwipeDecision.delete) {
filesToDelete.add(entry.key);
totalSize += entry.key.fileSize ?? 0;
}
}
if (filesToDelete.isEmpty) {
Navigator.of(context).pop(0);
return;
}
showChoiceDialog(
context,
title: S.of(context).deletePhotos,
body: S.of(context).deletePhotosBody(
filesToDelete.length.toString(),
formatBytes(totalSize),
),
firstButtonLabel: S.of(context).delete,
title: AppLocalizations.of(context).deletePhotos,
body: AppLocalizations.of(context).deletePhotosBody(
filesToDelete.length.toString(),
formatBytes(totalSize),
),
firstButtonLabel: AppLocalizations.of(context).delete,
isCritical: true,
firstButtonOnTap: () async {
try {
@@ -303,7 +304,7 @@ class _SwipeCullingPageState extends State<SwipeCullingPage>
final lastAction = groupHistories[currentGroupIndex]!.removeLast();
fullHistory.removeLast();
decisions[lastAction.file] = SwipeDecision.undecided;
// Move back to the undone image
final fileIndex = currentGroupFiles.indexOf(lastAction.file);
if (fileIndex != -1) {
@@ -322,11 +323,11 @@ class _SwipeCullingPageState extends State<SwipeCullingPage>
void _switchToGroup(int groupIndex) {
if (groupIndex < 0 || groupIndex >= groups.length) return;
setState(() {
currentGroupIndex = groupIndex;
currentImageIndex = 0;
// Find first undecided image in the group
final files = groups[groupIndex].files;
for (int i = 0; i < files.length; i++) {
@@ -340,9 +341,9 @@ class _SwipeCullingPageState extends State<SwipeCullingPage>
void _showGroupSummaryPopup(int groupIndex) {
if (groupIndex < 0 || groupIndex >= groups.length) return;
final group = groups[groupIndex];
showDialog(
context: context,
builder: (context) => GroupSummaryPopup(
@@ -357,7 +358,8 @@ class _SwipeCullingPageState extends State<SwipeCullingPage>
// Clear group history
groupHistories[groupIndex]?.clear();
// Remove from full history
fullHistory.removeWhere((action) => action.groupIndex == groupIndex);
fullHistory
.removeWhere((action) => action.groupIndex == groupIndex);
});
Navigator.of(context).pop();
_switchToGroup(groupIndex);
@@ -370,11 +372,11 @@ class _SwipeCullingPageState extends State<SwipeCullingPage>
filesToDelete.add(file);
}
}
if (filesToDelete.isNotEmpty) {
Navigator.of(context).pop();
await _deleteFilesLogic(filesToDelete, true);
// Remove this group from the list if all deleted
if (filesToDelete.length == group.files.length) {
setState(() {
@@ -398,10 +400,10 @@ class _SwipeCullingPageState extends State<SwipeCullingPage>
if (filesToDelete.isEmpty) {
return;
}
final Map<int, Set<EnteFile>> collectionToFilesToAddMap = {};
final allDeleteFiles = <EnteFile>{};
for (final group in groups) {
final groupDeleteFiles = <EnteFile>{};
for (final file in filesToDelete) {
@@ -410,11 +412,13 @@ class _SwipeCullingPageState extends State<SwipeCullingPage>
allDeleteFiles.add(file);
}
}
if (groupDeleteFiles.isNotEmpty && createSymlink) {
final filesToKeep = group.files.where((f) => !groupDeleteFiles.contains(f)).toSet();
final collectionIDs = filesToKeep.map((file) => file.collectionID).toSet();
final filesToKeep =
group.files.where((f) => !groupDeleteFiles.contains(f)).toSet();
final collectionIDs =
filesToKeep.map((file) => file.collectionID).toSet();
for (final deletedFile in groupDeleteFiles) {
final collectionID = deletedFile.collectionID;
if (collectionIDs.contains(collectionID) || collectionID == null) {
@@ -427,7 +431,7 @@ class _SwipeCullingPageState extends State<SwipeCullingPage>
}
}
}
final int collectionCnt = collectionToFilesToAddMap.keys.length;
if (createSymlink) {
final userID = Configuration.instance.getUserID();
@@ -441,9 +445,10 @@ class _SwipeCullingPageState extends State<SwipeCullingPage>
final double percentage = (progress / collectionCnt) * 100;
_deleteProgress.value = '${percentage.toStringAsFixed(1)}%';
}
// Check permission before attempting to add symlinks
final collection = CollectionsService.instance.getCollectionByID(collectionID);
final collection =
CollectionsService.instance.getCollectionByID(collectionID);
if (collection != null && collection.canAutoAdd(userID!)) {
await CollectionsService.instance.addSilentlyToCollection(
collectionID,
@@ -456,13 +461,13 @@ class _SwipeCullingPageState extends State<SwipeCullingPage>
}
}
}
if (collectionCnt > 2) {
_deleteProgress.value = "";
}
await deleteFilesFromRemoteOnly(context, allDeleteFiles.toList());
// Show congratulations if more than 100 files deleted
if (allDeleteFiles.length > 100 && mounted) {
final int totalSize = allDeleteFiles.fold<int>(
@@ -472,24 +477,23 @@ class _SwipeCullingPageState extends State<SwipeCullingPage>
_showCongratulationsDialog(allDeleteFiles.length, totalSize);
}
}
void _showCongratulationsDialog(int deletedCount, int totalSize) {
showDialog(
context: context,
builder: (context) {
final theme = getEnteColorScheme(context);
return AlertDialog(
title: Text(S.of(context).congratulations),
title: Text(AppLocalizations.of(context).congratulations),
content: Text(
S.of(context).deletedPhotosWithSize(
deletedCount.toString(),
formatBytes(totalSize),
),
AppLocalizations.of(context).deletedPhotosWithSize(
deletedCount.toString(),
formatBytes(totalSize),
),
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text(S.of(context).ok),
child: Text(AppLocalizations.of(context).ok),
),
],
);
@@ -500,14 +504,14 @@ class _SwipeCullingPageState extends State<SwipeCullingPage>
@override
Widget build(BuildContext context) {
final theme = getEnteColorScheme(context);
if (groups.isEmpty) {
return Scaffold(
appBar: AppBar(
title: Text(S.of(context).review),
title: Text(AppLocalizations.of(context).review),
),
body: Center(
child: Text(S.of(context).noImagesSelected),
child: Text(AppLocalizations.of(context).noImagesSelected),
),
);
}
@@ -515,190 +519,203 @@ class _SwipeCullingPageState extends State<SwipeCullingPage>
return Stack(
children: [
Scaffold(
appBar: AppBar(
leading: IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () {
if (totalDeletionCount > 0) {
// TODO: Show exit confirmation if there are pending deletions
}
Navigator.of(context).pop();
},
),
title: Text(
"${currentImageIndex + 1} of ${currentGroupFiles.length}",
style: Theme.of(context).textTheme.bodyMedium,
),
actions: [
if (totalDeletionCount > 0)
TextButton(
onPressed: _showCompletionDialog,
child: Text(
"Delete ($totalDeletionCount)",
style: TextStyle(color: theme.warning700),
),
appBar: AppBar(
leading: IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () {
if (totalDeletionCount > 0) {
// TODO: Show exit confirmation if there are pending deletions
}
Navigator.of(context).pop();
},
),
],
),
body: Column(
children: [
// Group carousel at top
if (groups.length > 1)
Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: GroupCarousel(
groups: groups,
currentGroupIndex: currentGroupIndex,
onGroupSelected: _switchToGroup,
onGroupLongPress: _showGroupSummaryPopup,
progressMap: progressMap,
),
title: Text(
"${currentImageIndex + 1} of ${currentGroupFiles.length}",
style: Theme.of(context).textTheme.bodyMedium,
),
Expanded(
child: currentFile != null
? Stack(
alignment: Alignment.center,
children: [
CardSwiper(
controller: controller,
cardsCount: currentGroupFiles.length - currentImageIndex,
numberOfCardsDisplayed: 1,
backCardOffset: const Offset(0, 0),
padding: const EdgeInsets.all(24.0),
cardBuilder: (context, index, percentThresholdX, percentThresholdY) {
final fileIndex = currentImageIndex + index;
if (fileIndex >= currentGroupFiles.length) {
return const SizedBox.shrink();
}
final file = currentGroupFiles[fileIndex];
final isFirst = fileIndex == 0;
// Calculate swipe progress for overlay effects
final swipeProgress = percentThresholdX / 100;
final isSwipingLeft = swipeProgress < -0.1;
final isSwipingRight = swipeProgress > 0.1;
return SwipeablePhotoCard(
file: file,
showBestPictureBadge: isFirst,
swipeProgress: swipeProgress,
isSwipingLeft: isSwipingLeft,
isSwipingRight: isSwipingRight,
);
},
onSwipe: (previousIndex, currentIndex, direction) {
final decision = direction == CardSwiperDirection.left
? SwipeDecision.delete
: SwipeDecision.keep;
// Handle the swipe decision
_handleSwipeDecision(decision);
return true;
},
onEnd: () {
// All cards in current group have been swiped
// This is handled in _handleSwipeDecision when reaching last card
},
isDisabled: false,
threshold: 50,
actions: [
if (totalDeletionCount > 0)
TextButton(
onPressed: _showCompletionDialog,
child: Text(
"Delete ($totalDeletionCount)",
style: TextStyle(color: theme.warning700),
),
// Celebration overlay
if (_showingCelebration)
AnimatedBuilder(
animation: _progressRingController,
builder: (context, child) {
return Container(
color: Colors.black.withOpacity(0.3),
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Stack(
alignment: Alignment.center,
),
],
),
body: Column(
children: [
// Group carousel at top
if (groups.length > 1)
Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: GroupCarousel(
groups: groups,
currentGroupIndex: currentGroupIndex,
onGroupSelected: _switchToGroup,
onGroupLongPress: _showGroupSummaryPopup,
progressMap: progressMap,
),
),
Expanded(
child: currentFile != null
? Stack(
alignment: Alignment.center,
children: [
CardSwiper(
controller: controller,
cardsCount:
currentGroupFiles.length - currentImageIndex,
numberOfCardsDisplayed: 1,
backCardOffset: const Offset(0, 0),
padding: const EdgeInsets.all(24.0),
cardBuilder: (context, index, percentThresholdX,
percentThresholdY) {
final fileIndex = currentImageIndex + index;
if (fileIndex >= currentGroupFiles.length) {
return const SizedBox.shrink();
}
final file = currentGroupFiles[fileIndex];
final isFirst = fileIndex == 0;
// Calculate swipe progress for overlay effects
final swipeProgress = percentThresholdX / 100;
final isSwipingLeft = swipeProgress < -0.1;
final isSwipingRight = swipeProgress > 0.1;
return SwipeablePhotoCard(
file: file,
showBestPictureBadge: isFirst,
swipeProgress: swipeProgress,
isSwipingLeft: isSwipingLeft,
isSwipingRight: isSwipingRight,
);
},
onSwipe: (previousIndex, currentIndex, direction) {
final decision =
direction == CardSwiperDirection.left
? SwipeDecision.delete
: SwipeDecision.keep;
// Handle the swipe decision
_handleSwipeDecision(decision);
return true;
},
onEnd: () {
// All cards in current group have been swiped
// This is handled in _handleSwipeDecision when reaching last card
},
isDisabled: false,
threshold: 50,
),
// Celebration overlay
if (_showingCelebration)
AnimatedBuilder(
animation: _progressRingController,
builder: (context, child) {
return Container(
color: Colors.black.withValues(alpha: 0.3),
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// Progress ring
SizedBox(
width: 100,
height: 100,
child: CircularProgressIndicator(
value: _progressRingController.value,
strokeWidth: 4,
valueColor: AlwaysStoppedAnimation(
theme.primary500,
Stack(
alignment: Alignment.center,
children: [
// Progress ring
SizedBox(
width: 100,
height: 100,
child: CircularProgressIndicator(
value: _progressRingController
.value,
strokeWidth: 4,
valueColor:
AlwaysStoppedAnimation(
theme.primary500,
),
),
),
),
// Checkmark or celebration icon
Icon(
Icons.check_circle_outline,
size: 60,
color: theme.primary500,
)
.animate(
controller:
_celebrationController)
.scaleXY(
begin: 0.8,
end: 1.2,
curve: Curves.elasticOut,
)
.fadeIn(),
],
),
// Checkmark or celebration icon
Icon(
Icons.check_circle_outline,
size: 60,
color: theme.primary500,
const SizedBox(height: 16),
Text(
AppLocalizations.of(context).groupComplete,
style: Theme.of(context)
.textTheme
.headlineSmall
?.copyWith(
color: Colors.white,
),
)
.animate(controller: _celebrationController)
.scale(
begin: 0.8,
end: 1.2,
curve: Curves.elasticOut,
)
.fadeIn(),
.animate(
controller:
_celebrationController)
.fadeIn(delay: 200.ms),
],
),
const SizedBox(height: 16),
Text(
S.of(context).groupComplete,
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
color: Colors.white,
),
)
.animate(controller: _celebrationController)
.fadeIn(delay: 200.ms),
],
),
),
);
},
),
],
)
: Center(
child: Text(S.of(context).noImagesSelected),
),
),
);
},
),
],
)
: Center(
child: Text(AppLocalizations.of(context).noImagesSelected),
),
),
// Action buttons at bottom
Padding(
padding: const EdgeInsets.all(16.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
IconButton(
onPressed: currentFile != null
? () => controller.swipe(CardSwiperDirection.left)
: null,
icon: Icon(Icons.delete_outline, color: theme.warning700),
iconSize: 32,
),
IconButton(
onPressed: _handleUndo,
icon: const Icon(Icons.undo),
iconSize: 32,
),
IconButton(
onPressed: currentFile != null
? () => controller.swipe(CardSwiperDirection.right)
: null,
icon: Icon(Icons.thumb_up_outlined,
color: theme.primary700),
iconSize: 32,
),
],
),
),
],
),
// Action buttons at bottom
Padding(
padding: const EdgeInsets.all(16.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
IconButton(
onPressed: currentFile != null
? () => controller.swipe(CardSwiperDirection.left)
: null,
icon: Icon(Icons.delete_outline, color: theme.warning700),
iconSize: 32,
),
IconButton(
onPressed: _handleUndo,
icon: const Icon(Icons.undo),
iconSize: 32,
),
IconButton(
onPressed: currentFile != null
? () => controller.swipe(CardSwiperDirection.right)
: null,
icon: Icon(Icons.thumb_up_outlined, color: theme.primary700),
iconSize: 32,
),
],
),
),
],
),
),
),
// Progress overlay during deletion
ValueListenableBuilder(
valueListenable: _deleteProgress,
@@ -706,12 +723,13 @@ class _SwipeCullingPageState extends State<SwipeCullingPage>
if (value.isEmpty) {
return const SizedBox.shrink();
}
return Container(
color: theme.backgroundBase.withOpacity(0.8),
color: theme.backgroundBase.withValues(alpha: 0.8),
child: Center(
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
padding:
const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
decoration: BoxDecoration(
color: theme.backgroundElevated,
borderRadius: BorderRadius.circular(8),
@@ -749,4 +767,4 @@ class _SwipeCullingPageState extends State<SwipeCullingPage>
],
);
}
}
}

View File

@@ -1,7 +1,7 @@
import 'package:flutter/material.dart';
import 'package:photos/generated/l10n.dart';
import 'package:photos/models/file/file.dart';
import 'package:photos/models/similar_files.dart';
import 'package:photos/generated/l10n.dart';
import 'package:photos/theme/ente_theme.dart';
import 'package:photos/ui/pages/library_culling/models/swipe_culling_state.dart';
import 'package:photos/ui/viewer/file/thumbnail_widget.dart';
@@ -69,7 +69,7 @@ class GroupSummaryPopup extends StatelessWidget {
Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Text(
S.of(context).storageToBeFreed(formatBytes(totalSize)),
AppLocalizations.of(context).storageToBeFreed(formatBytes(totalSize)),
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: theme.warning700,
),
@@ -174,7 +174,7 @@ class GroupSummaryPopup extends StatelessWidget {
Expanded(
child: OutlinedButton(
onPressed: onUndoAll,
child: Text(S.of(context).undoAll),
child: Text(AppLocalizations.of(context).undoAll),
),
),
const SizedBox(width: 12),
@@ -185,7 +185,7 @@ class GroupSummaryPopup extends StatelessWidget {
style: ElevatedButton.styleFrom(
backgroundColor: theme.warning700,
),
child: Text(S.of(context).deleteThese),
child: Text(AppLocalizations.of(context).deleteThese),
),
),
],

View File

@@ -25,8 +25,8 @@ import "package:photos/ui/viewer/gallery/empty_state.dart";
import "package:photos/utils/delete_file_util.dart";
import "package:photos/utils/dialog_util.dart";
import "package:photos/utils/navigation_util.dart";
import "package:photos/utils/standalone/data.dart";
import 'package:photos/ui/pages/library_culling/swipe_culling_page.dart';
import "package:photos/utils/standalone/data.dart";
enum SimilarImagesPageState {
setup,