Skip to content

Commit dcc649e

Browse files
authored
Merge pull request #9 from mx1up/feature/multiple_input_dirs
Feature/multiple input dirs
2 parents d18744c + d5a1543 commit dcc649e

11 files changed

Lines changed: 167 additions & 64 deletions

File tree

CHANGELOG.md

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

3+
## 0.3.0 - 2025-04-13
4+
5+
* support multiple input directories
6+
37
## 0.2.2 2025-04-13
48

59
* add windows support

README.md

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,20 +17,24 @@ Make sure the following prerequisites are met:
1717

1818
### Folder selection
1919

20-
Specify initial folder to scan for duplicates, either:
21-
* Pass a parameter (full folder path) to the `fdupes_gui` executable
20+
Specify initial folders to scan for duplicates, either:
21+
* Pass parameters (full folder paths) to the `fdupes_gui` executable
2222
* Select a folder using the 'Select folder' button
2323

2424
Change folder: press the 'Change folder' button on the top left.
2525

26+
Add additional folders to scan for duplicates: press the + button on the top right.
27+
28+
Remove a folder: click the - button on the right of the folder name.
29+
2630
### Dupe group selection
2731

2832
Clicking on a dupe group will select it and show the duplicate instances on the right pane.
2933

3034
### Dupe instances
3135

3236
A dupe instance shows its simple filename if all dupes are in the same folder, or includes
33-
its path relative to the selected dir (= base dir) otherwise.
37+
its path relative to its base dir otherwise.
3438

3539
The following actions can be performed:
3640

gfx/screenshot.png

-125 KB
Loading

lib/domain/fdupes_bloc.dart

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,13 @@ part 'fdupes_event.dart';
1212
part 'fdupes_state.dart';
1313

1414
class FdupesBloc extends Bloc<FdupesEvent, FdupesState> {
15-
final String? initialDir;
15+
final List<Directory>? initialDirs;
1616
String? fdupesLocation;
1717

18-
FdupesBloc({this.initialDir}) : super(FdupesStateInitial(initialDir)) {
18+
FdupesBloc({this.initialDirs}) : super(FdupesStateInitial(initialDirs)) {
1919
on<FdupesEventCheckFdupesAvailability>(_onCheckFdupesAvailability);
2020
on<FdupesEventSelectFdupesLocation>(_onSelectFdupesLocation);
21-
on<FdupesEventDirSelected>(_onDirSelected);
21+
on<FdupesEventDirsSelected>(_onDirsSelected);
2222
on<FdupesEventDupeSelected>(_onDupeSelected);
2323
on<FdupesEventDeleteDupeInstance>(_onDeleteDupeInstance);
2424
on<FdupesEventRenameDupeInstance>(_onRenameDupeInstance);
@@ -43,10 +43,10 @@ class FdupesBloc extends Bloc<FdupesEvent, FdupesState> {
4343
emit(FdupesStateFdupesNotFound());
4444
return;
4545
}
46-
if (initialDir != null) {
47-
add(FdupesEventDirSelected(initialDir!));
46+
if (initialDirs != null) {
47+
add(FdupesEventDirsSelected(initialDirs!));
4848
} else {
49-
emit(FdupesStateInitial(initialDir));
49+
emit(FdupesStateInitial(initialDirs));
5050
}
5151
}
5252

@@ -76,17 +76,17 @@ class FdupesBloc extends Bloc<FdupesEvent, FdupesState> {
7676
}
7777
}
7878

79-
FutureOr<void> _onDirSelected(FdupesEventDirSelected event, Emitter<FdupesState> emit) async {
79+
FutureOr<void> _onDirsSelected(FdupesEventDirsSelected event, Emitter<FdupesState> emit) async {
8080
final s = state;
8181
if (s is FdupesStateInitial) {
82-
emit(FdupesStateResult(dir: event.dir, dupeGroups: [], loading: true));
82+
emit(FdupesStateResult(dirs: event.dirs, dupeGroups: [], loading: true));
8383
}
8484
if (s is FdupesStateResult) {
8585
emit(s.copyWith(loading: true));
8686
}
87-
final dupes = await findDupes(event.dir, emit: emit);
87+
final dupes = await findDupes(event.dirs, emit: emit);
8888

89-
emit(FdupesStateResult(dir: event.dir, dupeGroups: dupes));
89+
emit(FdupesStateResult(dirs: event.dirs, dupeGroups: dupes));
9090
}
9191

9292
void _onDupeSelected(FdupesEventDupeSelected event, Emitter<FdupesState> emit) {
@@ -156,10 +156,10 @@ class FdupesBloc extends Bloc<FdupesEvent, FdupesState> {
156156
}
157157
}
158158

159-
Future<List<List<String>>> findDupes(String dir, {required Emitter<FdupesState> emit}) async {
160-
print("finding dupes in dir $dir");
159+
Future<List<List<String>>> findDupes(List<Directory> dirs, {required Emitter<FdupesState> emit}) async {
160+
print("finding dupes in dirs $dirs");
161161
List<List<String>> dupes = [];
162-
Process process = await Process.start(fdupesLocation!, ['-r', dir]);
162+
Process process = await Process.start(fdupesLocation!, ['-r', ...dirs.map((d) => d.path)]);
163163
// stdout.addStream(process.stdout);
164164
final regex = RegExp(r'\[(\d+)/(\d+)\]');
165165
final stderrBC = process.stderr.asBroadcastStream();

lib/domain/fdupes_event.dart

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,10 @@ class FdupesEventRenameDupeInstance extends FdupesEvent {
2727
FdupesEventRenameDupeInstance(this.filename, this.newFilename);
2828
}
2929

30-
class FdupesEventDirSelected extends FdupesEvent {
31-
final String dir;
30+
class FdupesEventDirsSelected extends FdupesEvent {
31+
final List<Directory> dirs;
3232

33-
FdupesEventDirSelected(this.dir);
33+
FdupesEventDirsSelected(this.dirs);
3434
}
3535

3636
class FdupesEventDupeSelected extends FdupesEvent {

lib/domain/fdupes_state.dart

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,12 @@ part of 'fdupes_bloc.dart';
44
abstract class FdupesState extends Equatable {}
55

66
class FdupesStateInitial extends FdupesState {
7-
final String? initialDir;
7+
final List<Directory>? initialDirs;
88

9-
FdupesStateInitial(this.initialDir);
9+
FdupesStateInitial(this.initialDirs);
1010

1111
@override
12-
List<Object?> get props => [initialDir];
12+
List<Object?> get props => [initialDirs];
1313
}
1414

1515
class FdupesStateError extends FdupesState {
@@ -41,35 +41,35 @@ class FdupesStateLoading extends FdupesState {
4141
}
4242

4343
class FdupesStateResult extends FdupesState {
44-
final String dir;
44+
final List<Directory> dirs;
4545
final List<List<String>> dupeGroups;
4646
//todo review nullability
4747
final int? selectedDupeGroup;
4848
final bool loading;
4949

5050
@override
5151
List<Object?> get props => [
52-
dir,
52+
dirs,
5353
dupeGroups,
5454
selectedDupeGroup,
5555
loading,
5656
];
5757

5858
FdupesStateResult({
59-
required this.dir,
59+
required this.dirs,
6060
required this.dupeGroups,
6161
this.selectedDupeGroup,
6262
this.loading = false,
6363
});
6464

6565
FdupesStateResult copyWith({
6666
bool? loading,
67-
String? dir,
67+
List<Directory>? dirs,
6868
List<List<String>>? dupes,
6969
int? selectedDupe,
7070
}) {
7171
return FdupesStateResult(
72-
dir: dir ?? this.dir,
72+
dirs: dirs ?? this.dirs,
7373
dupeGroups: dupes ?? this.dupeGroups,
7474
selectedDupeGroup: selectedDupe ?? this.selectedDupeGroup,
7575
loading: loading ?? this.loading,

lib/main.dart

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
1+
import 'dart:io';
2+
13
import 'package:adaptive_theme/adaptive_theme.dart';
24
import 'package:fdupes_gui/domain/fdupes_bloc.dart';
35
import 'package:fdupes_gui/presentation/dupe_screen.dart';
6+
import 'package:fdupes_gui/theme.dart';
47
import 'package:flutter/material.dart';
58
import 'package:flutter_bloc/flutter_bloc.dart';
69

@@ -31,29 +34,32 @@ class MyBlocObserver extends BlocObserver {
3134
}
3235

3336
void main(List<String> args) {
34-
String? initialDir;
37+
List<String>? initialDirsArg;
3538
if (args.length > 0) {
36-
initialDir = args[0];
39+
initialDirsArg = args;
3740
}
38-
print('initialDir=$initialDir');
41+
print('initialDirs=$initialDirsArg');
42+
final initialDirs =
43+
initialDirsArg?.map((e) => Directory(e)).where((element) => element.existsSync()).map((e) => e.absolute).toList();
44+
print('valid initialDirs=$initialDirs');
3945
Bloc.observer = MyBlocObserver();
4046

41-
runApp(MyApp(initialDir));
47+
runApp(MyApp(initialDirs));
4248
}
4349

4450
class MyApp extends StatelessWidget {
45-
final String? initialDir;
51+
final List<Directory>? initialDirs;
4652

47-
MyApp(this.initialDir);
53+
MyApp(this.initialDirs);
4854

4955
@override
5056
Widget build(BuildContext context) {
5157
return BlocProvider<FdupesBloc>(
52-
create: (context) => FdupesBloc(initialDir: initialDir),
58+
create: (context) => FdupesBloc(initialDirs: initialDirs),
5359
child: AdaptiveTheme(
5460
// debugShowFloatingThemeButton: true,
55-
light: ThemeData.light(useMaterial3: true),
56-
dark: ThemeData.dark(useMaterial3: true),
61+
light: FdupesTheme.light(),
62+
dark: FdupesTheme.dark(),
5763
initial: AdaptiveThemeMode.system,
5864
builder: (theme, darkTheme) => MaterialApp(
5965
title: 'Fdupes gui',

lib/presentation/dupe_screen.dart

Lines changed: 72 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import 'dart:io';
2+
13
import 'package:fdupes_gui/core/util.dart' as util;
24
import 'package:fdupes_gui/domain/fdupes_bloc.dart';
35
import 'package:fdupes_gui/presentation/dupes_body.dart';
@@ -14,7 +16,7 @@ class DupeScreen extends StatelessWidget {
1416
return Center(
1517
child: ElevatedButton(
1618
child: Text('Select folder'),
17-
onPressed: () => _showSelectFolderDialog(context, null),
19+
onPressed: () => _showSelectFolderDialog(context, initialDir: null, currentDirs: []),
1820
),
1921
);
2022
}
@@ -61,25 +63,69 @@ class DupeScreen extends StatelessWidget {
6163
padding: EdgeInsets.all(8),
6264
child: Column(
6365
children: <Widget>[
64-
Row(children: [
65-
ElevatedButton(
66-
child: Text('Change folder'),
67-
onPressed: () => _showSelectFolderDialog(context, state.dir),
68-
),
69-
SizedBox(width: 8),
70-
Expanded(child: Text(state.dir)),
71-
SizedBox(width: 8),
72-
ElevatedButton(
73-
child: Icon(Icons.refresh),
74-
onPressed: () => BlocProvider.of<FdupesBloc>(context).add(FdupesEventDirSelected(state.dir)),
75-
),
76-
]),
66+
Row(
67+
mainAxisSize: MainAxisSize.max,
68+
crossAxisAlignment: CrossAxisAlignment.start,
69+
children: [
70+
Expanded(
71+
child: Column(
72+
children: state.dirs
73+
.map((dir) => [
74+
Row(children: [
75+
ElevatedButton(
76+
child: Text('Change folder'),
77+
onPressed: () => _showSelectFolderDialog(
78+
context,
79+
initialDir: dir,
80+
currentDirs: state.dirs,
81+
),
82+
),
83+
SizedBox(width: 8),
84+
Text(dir.path),
85+
IconButton(
86+
icon: Icon(Icons.remove_circle),
87+
visualDensity: VisualDensity.compact,
88+
iconSize: 14,
89+
onPressed: () => BlocProvider.of<FdupesBloc>(context)
90+
.add(FdupesEventDirsSelected(state.dirs..remove(dir))),
91+
),
92+
]),
93+
SizedBox(height: 8),
94+
])
95+
.expand((e) => e)
96+
.toList(),
97+
),
98+
),
99+
SizedBox(width: 8),
100+
Column(
101+
children: [
102+
Tooltip(
103+
message: 'Find duplicates',
104+
child: ElevatedButton(
105+
child: Icon(Icons.refresh),
106+
onPressed: () =>
107+
BlocProvider.of<FdupesBloc>(context).add(FdupesEventDirsSelected(state.dirs)),
108+
),
109+
),
110+
SizedBox(height: 8),
111+
Tooltip(
112+
message: 'Add input folder',
113+
child: ElevatedButton(
114+
child: Icon(Icons.add),
115+
onPressed: () =>
116+
_showSelectFolderDialog(context, initialDir: null, currentDirs: state.dirs),
117+
),
118+
),
119+
],
120+
),
121+
],
122+
),
77123
SizedBox(height: 8),
78124
if (state.dupeGroups.isEmpty)
79125
Text('no dupes found')
80126
else
81127
DupesBody(
82-
baseDir: state.dir,
128+
baseDirs: state.dirs,
83129
dupeGroups: state.dupeGroups,
84130
selectedDupeGroup: state.selectedDupeGroup,
85131
),
@@ -92,11 +138,19 @@ class DupeScreen extends StatelessWidget {
92138
);
93139
}
94140

95-
Future<void> _showSelectFolderDialog(BuildContext context, String? initialDir) async {
141+
Future<void> _showSelectFolderDialog(
142+
BuildContext context, {
143+
Directory? initialDir,
144+
required List<Directory> currentDirs,
145+
}) async {
96146
final dir = await FileSelectorPlatform.instance
97-
.getDirectoryPath(initialDirectory: initialDir ?? util.userHome, confirmButtonText: 'Select');
147+
.getDirectoryPath(initialDirectory: initialDir?.path ?? util.userHome, confirmButtonText: 'Select');
98148
if (dir != null) {
99-
BlocProvider.of<FdupesBloc>(context).add(FdupesEventDirSelected(dir));
149+
if (initialDir != null) {
150+
currentDirs.remove(initialDir);
151+
}
152+
currentDirs.add(Directory(dir));
153+
BlocProvider.of<FdupesBloc>(context).add(FdupesEventDirsSelected(currentDirs));
100154
}
101155
}
102156

0 commit comments

Comments
 (0)