Skip to content

Commit 96fa1aa

Browse files
authored
Merge pull request #18 from mx1up/feature/cli_options
Feature/cli options
2 parents 347433a + a8daf0a commit 96fa1aa

File tree

10 files changed

+271
-69
lines changed

10 files changed

+271
-69
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
# Changelog
22

3+
## 0.4.0 - 2025-11-11
4+
5+
* support --noempty, --cache and --symlinks cli option
6+
* added preferences screen
7+
38
## 0.3.2 - 2025-11-11
49

510
* print app version on startup

lib/domain/fdupes_bloc.dart

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,12 @@ part 'fdupes_state.dart';
1414
class FdupesBloc extends Bloc<FdupesEvent, FdupesState> {
1515
final List<Directory>? initialDirs;
1616
String? fdupesLocation;
17+
final SharedPreferences sharedPreferences;
1718

18-
FdupesBloc({this.initialDirs}) : super(FdupesStateInitial(initialDirs)) {
19+
FdupesBloc({
20+
this.initialDirs,
21+
required this.sharedPreferences,
22+
}) : super(FdupesStateInitial(initialDirs)) {
1923
on<FdupesEventCheckFdupesAvailability>(_onCheckFdupesAvailability);
2024
on<FdupesEventSelectFdupesLocation>(_onSelectFdupesLocation);
2125
on<FdupesEventDirsSelected>(_onDirsSelected);
@@ -62,8 +66,8 @@ class FdupesBloc extends Bloc<FdupesEvent, FdupesState> {
6266
return;
6367
}
6468
add(FdupesEventCheckFdupesAvailability());
65-
}
66-
else emit(FdupesStateFdupesNotFound(statusMsg: 'Not a valid fdupes binary.'));
69+
} else
70+
emit(FdupesStateFdupesNotFound(statusMsg: 'Not a valid fdupes binary.'));
6771
}
6872

6973
Future<bool> validFdupesLocation(String path) async {
@@ -84,7 +88,16 @@ class FdupesBloc extends Bloc<FdupesEvent, FdupesState> {
8488
if (s is FdupesStateResult) {
8589
emit(s.copyWith(loading: true));
8690
}
87-
final dupes = await findDupes(event.dirs, emit: emit);
91+
final skipEmpty = sharedPreferences.getBool('noempty') ?? false;
92+
final useCache = sharedPreferences.getBool('usecache') ?? false;
93+
final followSymlinks = sharedPreferences.getBool('followsymlinks') ?? false;
94+
final dupes = await findDupes(
95+
event.dirs,
96+
emit: emit,
97+
skipEmpty: skipEmpty,
98+
useCache: useCache,
99+
followSymlinks: followSymlinks,
100+
);
88101

89102
emit(FdupesStateResult(dirs: event.dirs, dupeGroups: dupes));
90103
}
@@ -156,10 +169,24 @@ class FdupesBloc extends Bloc<FdupesEvent, FdupesState> {
156169
}
157170
}
158171

159-
Future<List<List<String>>> findDupes(List<Directory> dirs, {required Emitter<FdupesState> emit}) async {
172+
Future<List<List<String>>> findDupes(
173+
List<Directory> dirs, {
174+
required Emitter<FdupesState> emit,
175+
required bool skipEmpty,
176+
required bool useCache,
177+
required bool followSymlinks,
178+
}) async {
160179
print("finding dupes in dirs $dirs");
180+
final args = [
181+
'-r',
182+
if (skipEmpty) '--noempty',
183+
if (useCache) '--usecache',
184+
if (followSymlinks) '--symlinks',
185+
...dirs.map((d) => d.path),
186+
];
187+
print('cmd line: $fdupesLocation $args');
188+
Process process = await Process.start(fdupesLocation!, args);
161189
List<List<String>> dupes = [];
162-
Process process = await Process.start(fdupesLocation!, ['-r', ...dirs.map((d) => d.path)]);
163190
// stdout.addStream(process.stdout);
164191
final regex = RegExp(r'\[(\d+)/(\d+)\]');
165192
final stderrBC = process.stderr.asBroadcastStream();

lib/main.dart

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import 'package:fdupes_gui/theme.dart';
77
import 'package:flutter/material.dart';
88
import 'package:flutter_bloc/flutter_bloc.dart';
99
import 'package:package_info_plus/package_info_plus.dart';
10+
import 'package:shared_preferences/shared_preferences.dart';
1011

1112
class MyBlocObserver extends BlocObserver {
1213
@override
@@ -49,18 +50,31 @@ Future<void> main(List<String> args) async {
4950

5051
Bloc.observer = MyBlocObserver();
5152

52-
runApp(MyApp(initialDirs));
53+
WidgetsFlutterBinding.ensureInitialized();
54+
final sharedPreferences = await SharedPreferences.getInstance();
55+
56+
runApp(MyApp(
57+
initialDirs,
58+
sharedPreferences: sharedPreferences,
59+
));
5360
}
5461

5562
class MyApp extends StatelessWidget {
5663
final List<Directory>? initialDirs;
64+
final SharedPreferences sharedPreferences;
5765

58-
MyApp(this.initialDirs);
66+
MyApp(
67+
this.initialDirs, {
68+
required this.sharedPreferences,
69+
});
5970

6071
@override
6172
Widget build(BuildContext context) {
6273
return BlocProvider<FdupesBloc>(
63-
create: (context) => FdupesBloc(initialDirs: initialDirs),
74+
create: (context) => FdupesBloc(
75+
initialDirs: initialDirs,
76+
sharedPreferences: sharedPreferences,
77+
),
6478
child: AdaptiveTheme(
6579
// debugShowFloatingThemeButton: true,
6680
light: FdupesTheme.light(),
@@ -70,7 +84,11 @@ class MyApp extends StatelessWidget {
7084
title: 'Fdupes gui',
7185
theme: theme,
7286
darkTheme: darkTheme,
73-
home: Material(child: DupeScreen()),
87+
home: Material(
88+
child: DupeScreen(
89+
sharedPreferences: sharedPreferences,
90+
),
91+
),
7492
),
7593
),
7694
);

lib/presentation/about_dialog.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
22
import 'package:package_info_plus/package_info_plus.dart';
33
import 'package:url_launcher/link.dart';
44

5-
Future<void> showAboutDialoog(
5+
Future<void> showAppAboutDialog(
66
BuildContext context,
77
) async {
88
final appInfo = await PackageInfo.fromPlatform();

lib/presentation/dupe_screen.dart

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,43 @@ import 'package:fdupes_gui/core/util.dart' as util;
22
import 'package:fdupes_gui/domain/fdupes_bloc.dart';
33
import 'package:fdupes_gui/presentation/dupes_body.dart';
44
import 'package:fdupes_gui/presentation/dupes_top_bar.dart';
5+
import 'package:fdupes_gui/presentation/prefs_dialog.dart';
56
import 'package:fdupes_gui/presentation/select_folder_dialog.dart';
67
import 'package:file_selector_platform_interface/file_selector_platform_interface.dart';
78
import 'package:flutter/material.dart';
89
import 'package:flutter_bloc/flutter_bloc.dart';
10+
import 'package:shared_preferences/shared_preferences.dart';
911

1012
class DupeScreen extends StatelessWidget {
13+
final SharedPreferences sharedPreferences;
14+
15+
DupeScreen({super.key, required this.sharedPreferences});
16+
1117
@override
1218
Widget build(BuildContext context) {
1319
return BlocBuilder<FdupesBloc, FdupesState>(
1420
builder: (context, state) {
1521
if (state is FdupesStateInitial) {
16-
return Center(
17-
child: ElevatedButton(
18-
child: Text('Select folder'),
19-
onPressed: () => showSelectFolderDialog(context, initialDir: null, currentDirs: []),
20-
),
22+
return Stack(
23+
alignment: Alignment.center,
24+
children: [
25+
ElevatedButton(
26+
child: Text('Select folder'),
27+
onPressed: () => showSelectFolderDialog(context, initialDir: null, currentDirs: []),
28+
),
29+
Positioned(
30+
right: 16,
31+
top: 16,
32+
child: Tooltip(
33+
message: 'Preferences',
34+
child: ElevatedButton(
35+
child: Icon(Icons.settings),
36+
style: ElevatedButton.styleFrom(shape: CircleBorder()),
37+
onPressed: () => showPreferencesDialog(context, sharedPreferences),
38+
),
39+
),
40+
),
41+
],
2142
);
2243
}
2344
if (state is FdupesStateFdupesNotFound) {
@@ -72,7 +93,10 @@ class DupeScreen extends StatelessWidget {
7293
padding: EdgeInsets.all(8),
7394
child: Column(
7495
children: <Widget>[
75-
DupesTopBar(baseDirs: state.dirs),
96+
DupesTopBar(
97+
baseDirs: state.dirs,
98+
sharedPreferences: sharedPreferences,
99+
),
76100
SizedBox(height: 8),
77101
if (state.dupeGroups.isEmpty)
78102
Text('no dupes found')

lib/presentation/dupes_top_bar.dart

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,20 @@ import 'dart:io';
33
import 'package:fdupes_gui/domain/fdupes_bloc.dart';
44
import 'package:fdupes_gui/presentation/about_dialog.dart';
55
import 'package:fdupes_gui/presentation/base_dirs.dart';
6+
import 'package:fdupes_gui/presentation/prefs_dialog.dart';
67
import 'package:fdupes_gui/presentation/select_folder_dialog.dart';
78
import 'package:flutter/material.dart';
89
import 'package:flutter_bloc/flutter_bloc.dart';
10+
import 'package:shared_preferences/shared_preferences.dart';
911

1012
class DupesTopBar extends StatelessWidget {
1113
final List<Directory> baseDirs;
14+
final SharedPreferences sharedPreferences;
1215

1316
DupesTopBar({
1417
super.key,
1518
required this.baseDirs,
19+
required this.sharedPreferences,
1620
});
1721

1822
@override
@@ -44,12 +48,24 @@ class DupesTopBar extends StatelessWidget {
4448
Expanded(
4549
child: BaseDirs(baseDirs: baseDirs),
4650
),
47-
Tooltip(
48-
message: 'Find duplicates',
49-
child: ElevatedButton(
50-
child: Icon(Icons.info_outline),
51-
onPressed: () => showAboutDialoog(context),
52-
),
51+
Column(
52+
children: [
53+
Tooltip(
54+
message: 'About this app',
55+
child: ElevatedButton(
56+
child: Icon(Icons.info_outline),
57+
onPressed: () => showAppAboutDialog(context),
58+
),
59+
),
60+
SizedBox(height: 8),
61+
Tooltip(
62+
message: 'Preferences',
63+
child: ElevatedButton(
64+
child: Icon(Icons.settings),
65+
onPressed: () => showPreferencesDialog(context, sharedPreferences),
66+
),
67+
),
68+
],
5369
),
5470
],
5571
);

lib/presentation/prefs_dialog.dart

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import 'package:flutter/material.dart';
2+
import 'package:shared_preferences/shared_preferences.dart';
3+
4+
Future<void> showPreferencesDialog(
5+
BuildContext context,
6+
SharedPreferences sharedPreferences,
7+
) async {
8+
showDialog(
9+
context: context,
10+
barrierDismissible: true,
11+
builder: (context) => PreferencesDialog(sharedPreferences),
12+
);
13+
}
14+
15+
class PreferencesDialog extends StatefulWidget {
16+
final SharedPreferences sharedPreferences;
17+
18+
PreferencesDialog(this.sharedPreferences);
19+
20+
@override
21+
State<PreferencesDialog> createState() => _PreferencesDialogState();
22+
}
23+
24+
class _PreferencesDialogState extends State<PreferencesDialog> {
25+
/// use local state instead of directly accessing shared preferences since we write the settings asynchronously without waiting
26+
late bool skipEmpty;
27+
late bool useCache;
28+
late bool followSymlinks;
29+
30+
@override
31+
void initState() {
32+
super.initState();
33+
skipEmpty = widget.sharedPreferences.getBool('noempty') ?? false;
34+
useCache = widget.sharedPreferences.getBool('usecache') ?? false;
35+
followSymlinks = widget.sharedPreferences.getBool('followsymlinks') ?? false;
36+
}
37+
38+
@override
39+
Widget build(BuildContext context) {
40+
return AlertDialog(
41+
title: Text('Preferences'),
42+
content: Column(
43+
mainAxisSize: MainAxisSize.min,
44+
children: [
45+
SwitchListTile(
46+
title: const Text('Skip empty files', softWrap: false),
47+
value: skipEmpty,
48+
onChanged: (value) {
49+
setState(() {
50+
skipEmpty = value;
51+
widget.sharedPreferences.setBool('noempty', value);
52+
});
53+
},
54+
),
55+
SwitchListTile(
56+
title: const Text('Use cache'),
57+
subtitle: const Text('fdupes 2.3.0+', softWrap: false),
58+
value: useCache,
59+
onChanged: (value) {
60+
setState(() {
61+
useCache = value;
62+
widget.sharedPreferences.setBool('usecache', value);
63+
});
64+
}),
65+
SwitchListTile(
66+
title: const Text('Follow symlinks'),
67+
value: followSymlinks,
68+
onChanged: (value) {
69+
setState(() {
70+
followSymlinks = value;
71+
widget.sharedPreferences.setBool('followsymlinks', value);
72+
});
73+
}),
74+
],
75+
),
76+
);
77+
}
78+
}

0 commit comments

Comments
 (0)