Skip to content
Merged
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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# Changelog

## 0.4.0 - 2025-11-11

* support --noempty, --cache and --symlinks cli option
* added preferences screen

## 0.3.2 - 2025-11-11

* print app version on startup
Expand Down
39 changes: 33 additions & 6 deletions lib/domain/fdupes_bloc.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,12 @@ part 'fdupes_state.dart';
class FdupesBloc extends Bloc<FdupesEvent, FdupesState> {
final List<Directory>? initialDirs;
String? fdupesLocation;
final SharedPreferences sharedPreferences;

FdupesBloc({this.initialDirs}) : super(FdupesStateInitial(initialDirs)) {
FdupesBloc({
this.initialDirs,
required this.sharedPreferences,
}) : super(FdupesStateInitial(initialDirs)) {
on<FdupesEventCheckFdupesAvailability>(_onCheckFdupesAvailability);
on<FdupesEventSelectFdupesLocation>(_onSelectFdupesLocation);
on<FdupesEventDirsSelected>(_onDirsSelected);
Expand Down Expand Up @@ -62,8 +66,8 @@ class FdupesBloc extends Bloc<FdupesEvent, FdupesState> {
return;
}
add(FdupesEventCheckFdupesAvailability());
}
else emit(FdupesStateFdupesNotFound(statusMsg: 'Not a valid fdupes binary.'));
} else
emit(FdupesStateFdupesNotFound(statusMsg: 'Not a valid fdupes binary.'));
}

Future<bool> validFdupesLocation(String path) async {
Expand All @@ -84,7 +88,16 @@ class FdupesBloc extends Bloc<FdupesEvent, FdupesState> {
if (s is FdupesStateResult) {
emit(s.copyWith(loading: true));
}
final dupes = await findDupes(event.dirs, emit: emit);
final skipEmpty = sharedPreferences.getBool('noempty') ?? false;
final useCache = sharedPreferences.getBool('usecache') ?? false;
final followSymlinks = sharedPreferences.getBool('followsymlinks') ?? false;
final dupes = await findDupes(
event.dirs,
emit: emit,
skipEmpty: skipEmpty,
useCache: useCache,
followSymlinks: followSymlinks,
);

emit(FdupesStateResult(dirs: event.dirs, dupeGroups: dupes));
}
Expand Down Expand Up @@ -156,10 +169,24 @@ class FdupesBloc extends Bloc<FdupesEvent, FdupesState> {
}
}

Future<List<List<String>>> findDupes(List<Directory> dirs, {required Emitter<FdupesState> emit}) async {
Future<List<List<String>>> findDupes(
List<Directory> dirs, {
required Emitter<FdupesState> emit,
required bool skipEmpty,
required bool useCache,
required bool followSymlinks,
}) async {
print("finding dupes in dirs $dirs");
final args = [
'-r',
if (skipEmpty) '--noempty',
if (useCache) '--usecache',
if (followSymlinks) '--symlinks',
...dirs.map((d) => d.path),
];
print('cmd line: $fdupesLocation $args');
Process process = await Process.start(fdupesLocation!, args);
List<List<String>> dupes = [];
Process process = await Process.start(fdupesLocation!, ['-r', ...dirs.map((d) => d.path)]);
// stdout.addStream(process.stdout);
final regex = RegExp(r'\[(\d+)/(\d+)\]');
final stderrBC = process.stderr.asBroadcastStream();
Expand Down
26 changes: 22 additions & 4 deletions lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import 'package:fdupes_gui/theme.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:shared_preferences/shared_preferences.dart';

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

Bloc.observer = MyBlocObserver();

runApp(MyApp(initialDirs));
WidgetsFlutterBinding.ensureInitialized();
final sharedPreferences = await SharedPreferences.getInstance();

runApp(MyApp(
initialDirs,
sharedPreferences: sharedPreferences,
));
}

class MyApp extends StatelessWidget {
final List<Directory>? initialDirs;
final SharedPreferences sharedPreferences;

MyApp(this.initialDirs);
MyApp(
this.initialDirs, {
required this.sharedPreferences,
});

@override
Widget build(BuildContext context) {
return BlocProvider<FdupesBloc>(
create: (context) => FdupesBloc(initialDirs: initialDirs),
create: (context) => FdupesBloc(
initialDirs: initialDirs,
sharedPreferences: sharedPreferences,
),
child: AdaptiveTheme(
// debugShowFloatingThemeButton: true,
light: FdupesTheme.light(),
Expand All @@ -70,7 +84,11 @@ class MyApp extends StatelessWidget {
title: 'Fdupes gui',
theme: theme,
darkTheme: darkTheme,
home: Material(child: DupeScreen()),
home: Material(
child: DupeScreen(
sharedPreferences: sharedPreferences,
),
),
),
),
);
Expand Down
2 changes: 1 addition & 1 deletion lib/presentation/about_dialog.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:url_launcher/link.dart';

Future<void> showAboutDialoog(
Future<void> showAppAboutDialog(
BuildContext context,
) async {
final appInfo = await PackageInfo.fromPlatform();
Expand Down
36 changes: 30 additions & 6 deletions lib/presentation/dupe_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,43 @@ import 'package:fdupes_gui/core/util.dart' as util;
import 'package:fdupes_gui/domain/fdupes_bloc.dart';
import 'package:fdupes_gui/presentation/dupes_body.dart';
import 'package:fdupes_gui/presentation/dupes_top_bar.dart';
import 'package:fdupes_gui/presentation/prefs_dialog.dart';
import 'package:fdupes_gui/presentation/select_folder_dialog.dart';
import 'package:file_selector_platform_interface/file_selector_platform_interface.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:shared_preferences/shared_preferences.dart';

class DupeScreen extends StatelessWidget {
final SharedPreferences sharedPreferences;

DupeScreen({super.key, required this.sharedPreferences});

@override
Widget build(BuildContext context) {
return BlocBuilder<FdupesBloc, FdupesState>(
builder: (context, state) {
if (state is FdupesStateInitial) {
return Center(
child: ElevatedButton(
child: Text('Select folder'),
onPressed: () => showSelectFolderDialog(context, initialDir: null, currentDirs: []),
),
return Stack(
alignment: Alignment.center,
children: [
ElevatedButton(
child: Text('Select folder'),
onPressed: () => showSelectFolderDialog(context, initialDir: null, currentDirs: []),
),
Positioned(
right: 16,
top: 16,
child: Tooltip(
message: 'Preferences',
child: ElevatedButton(
child: Icon(Icons.settings),
style: ElevatedButton.styleFrom(shape: CircleBorder()),
onPressed: () => showPreferencesDialog(context, sharedPreferences),
),
),
),
],
);
}
if (state is FdupesStateFdupesNotFound) {
Expand Down Expand Up @@ -72,7 +93,10 @@ class DupeScreen extends StatelessWidget {
padding: EdgeInsets.all(8),
child: Column(
children: <Widget>[
DupesTopBar(baseDirs: state.dirs),
DupesTopBar(
baseDirs: state.dirs,
sharedPreferences: sharedPreferences,
),
SizedBox(height: 8),
if (state.dupeGroups.isEmpty)
Text('no dupes found')
Expand Down
28 changes: 22 additions & 6 deletions lib/presentation/dupes_top_bar.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,20 @@ import 'dart:io';
import 'package:fdupes_gui/domain/fdupes_bloc.dart';
import 'package:fdupes_gui/presentation/about_dialog.dart';
import 'package:fdupes_gui/presentation/base_dirs.dart';
import 'package:fdupes_gui/presentation/prefs_dialog.dart';
import 'package:fdupes_gui/presentation/select_folder_dialog.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:shared_preferences/shared_preferences.dart';

class DupesTopBar extends StatelessWidget {
final List<Directory> baseDirs;
final SharedPreferences sharedPreferences;

DupesTopBar({
super.key,
required this.baseDirs,
required this.sharedPreferences,
});

@override
Expand Down Expand Up @@ -44,12 +48,24 @@ class DupesTopBar extends StatelessWidget {
Expanded(
child: BaseDirs(baseDirs: baseDirs),
),
Tooltip(
message: 'Find duplicates',
child: ElevatedButton(
child: Icon(Icons.info_outline),
onPressed: () => showAboutDialoog(context),
),
Column(
children: [
Tooltip(
message: 'About this app',
child: ElevatedButton(
child: Icon(Icons.info_outline),
onPressed: () => showAppAboutDialog(context),
),
),
SizedBox(height: 8),
Tooltip(
message: 'Preferences',
child: ElevatedButton(
child: Icon(Icons.settings),
onPressed: () => showPreferencesDialog(context, sharedPreferences),
),
),
],
),
],
);
Expand Down
78 changes: 78 additions & 0 deletions lib/presentation/prefs_dialog.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';

Future<void> showPreferencesDialog(
BuildContext context,
SharedPreferences sharedPreferences,
) async {
showDialog(
context: context,
barrierDismissible: true,
builder: (context) => PreferencesDialog(sharedPreferences),
);
}

class PreferencesDialog extends StatefulWidget {
final SharedPreferences sharedPreferences;

PreferencesDialog(this.sharedPreferences);

@override
State<PreferencesDialog> createState() => _PreferencesDialogState();
}

class _PreferencesDialogState extends State<PreferencesDialog> {
/// use local state instead of directly accessing shared preferences since we write the settings asynchronously without waiting
late bool skipEmpty;
late bool useCache;
late bool followSymlinks;

@override
void initState() {
super.initState();
skipEmpty = widget.sharedPreferences.getBool('noempty') ?? false;
useCache = widget.sharedPreferences.getBool('usecache') ?? false;
followSymlinks = widget.sharedPreferences.getBool('followsymlinks') ?? false;
}

@override
Widget build(BuildContext context) {
return AlertDialog(
title: Text('Preferences'),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
SwitchListTile(
title: const Text('Skip empty files', softWrap: false),
value: skipEmpty,
onChanged: (value) {
setState(() {
skipEmpty = value;
widget.sharedPreferences.setBool('noempty', value);
});
},
),
SwitchListTile(
title: const Text('Use cache'),
subtitle: const Text('fdupes 2.3.0+', softWrap: false),
value: useCache,
onChanged: (value) {
setState(() {
useCache = value;
widget.sharedPreferences.setBool('usecache', value);
});
}),
SwitchListTile(
title: const Text('Follow symlinks'),
value: followSymlinks,
onChanged: (value) {
setState(() {
followSymlinks = value;
widget.sharedPreferences.setBool('followsymlinks', value);
});
}),
],
),
);
}
}
Loading