@@ -8,12 +8,11 @@ import 'package:open_filex/open_filex.dart';
88import 'package:path/path.dart' as p;
99import 'package:trios/about/about_page.dart' ;
1010import 'package:trios/models/mod_variant.dart' ;
11- import 'package:trios/thirdparty/dartx/iterable.dart' ;
1211import 'package:trios/thirdparty/faded_scrollable/faded_scrollable.dart' ;
1312import 'package:trios/trios/app_state.dart' ;
1413import 'package:trios/trios/constants.dart' ;
1514import 'package:trios/utils/platform_specific.dart' ;
16- import 'package:trios/widgets/checkbox_with_label .dart' ;
15+ import 'package:trios/widgets/disable .dart' ;
1716import 'package:trios/widgets/trios_app_icon.dart' ;
1817
1918import 'logging.dart' ;
@@ -131,16 +130,77 @@ Future<void> showDeleteModFoldersConfirmationDialog(
131130 ref.read (AppState .modVariants.notifier).reloadModVariants ();
132131 }
133132
133+ Row _buildVariantToDeleteRow (
134+ ModVariant variant,
135+ bool isEnabled,
136+ ThemeData theme, {
137+ required bool checked,
138+ required ValueChanged <bool ?> onChanged,
139+ }) {
140+ return Row (
141+ children: [
142+ Checkbox (value: checked, onChanged: onChanged),
143+ InkWell (
144+ onTap: () => onChanged (! checked),
145+ splashColor: Colors .transparent,
146+ highlightColor: Colors .transparent,
147+ hoverColor: Colors .transparent,
148+ child: Padding (
149+ padding: const .only (left: 4 ),
150+ child: Column (
151+ mainAxisSize: MainAxisSize .min,
152+ crossAxisAlignment: CrossAxisAlignment .start,
153+ children: [
154+ Row (
155+ children: [
156+ Text (
157+ "${variant .modInfo .nameOrId } v${variant .modInfo .version }" ,
158+ style: theme.textTheme.labelLarge,
159+ ),
160+ if (isEnabled)
161+ Text (
162+ " (enabled)" ,
163+ style: theme.textTheme.labelLarge? .copyWith (
164+ fontWeight: FontWeight .bold,
165+ ),
166+ ),
167+ ],
168+ ),
169+ Text (
170+ "${variant .modFolder .path }" ,
171+ style: theme.textTheme.labelMedium? .copyWith (
172+ color: theme.textTheme.bodyMedium? .color? .withAlpha (200 ),
173+ ),
174+ ),
175+ ],
176+ ),
177+ ),
178+ ),
179+ ],
180+ );
181+ }
182+
134183 if (variantsToDelete.isEmpty) {
135184 return ;
136185 }
137186
138187 runZonedGuarded (
139188 () async {
140- bool allowDeletingEnabledMods =
141- allowDeletingEnabledModsDefaultState ?? false ;
142- List <ModVariant > filteredVariantsToDelete = variantsToDelete;
143- List <ModVariant > enabledVariantsThatWillNotBeDeleted = [];
189+ // Build enabled/disabled maps and selection state
190+ final allMods = ref.read (AppState .mods);
191+ final isEnabledBySmolId = < String , bool > {
192+ for (final v in variantsToDelete) v.smolId: v.isEnabled (allMods),
193+ };
194+ final enabledSmolIds = isEnabledBySmolId.entries
195+ .where ((e) => e.value)
196+ .map ((e) => e.key)
197+ .toSet ();
198+ final disabledSmolIds = isEnabledBySmolId.entries
199+ .where ((e) => ! e.value)
200+ .map ((e) => e.key)
201+ .toSet ();
202+ final selectedSmolIds = < String > {};
203+ bool initializedSelection = false ;
144204
145205 final shouldDelete = await showDialog <bool >(
146206 context: context,
@@ -149,27 +209,26 @@ Future<void> showDeleteModFoldersConfirmationDialog(
149209
150210 return StatefulBuilder (
151211 builder: (context, setState) {
152- final allMods = ref. read ( AppState .mods);
153-
154- final isOnlyEnabledMods = variantsToDelete. all (
155- (variant) => variant. isEnabled (allMods),
156- );
157-
158- enabledVariantsThatWillNotBeDeleted =
159- (isOnlyEnabledMods || allowDeletingEnabledMods)
160- ? []
161- : variantsToDelete
162- . where ((variant) => ! variant. isEnabled (allMods) )
163- . toList ( );
164-
165- filteredVariantsToDelete =
166- variantsToDelete - enabledVariantsThatWillNotBeDeleted;
212+ // Initialize the default selection once per dialog open
213+ if ( ! initializedSelection) {
214+ final hasBoth =
215+ enabledSmolIds.isNotEmpty && disabledSmolIds.isNotEmpty;
216+ if (hasBoth) {
217+ selectedSmolIds
218+ .. clear ()
219+ .. addAll (disabledSmolIds);
220+ } else {
221+ selectedSmolIds
222+ .. clear ( )
223+ .. addAll (isEnabledBySmolId.keys );
224+ }
225+ initializedSelection = true ;
226+ }
167227
168- final enabledVariants = variantsToDelete
169- .where ((variant) => variant.isEnabled (allMods))
170- .toList ();
171-
172- final s = filteredVariantsToDelete.length == 1 ? "s" : "" ;
228+ final hasBothEnabledAndDisabled =
229+ enabledSmolIds.isNotEmpty && disabledSmolIds.isNotEmpty;
230+ final selectedCount = selectedSmolIds.length;
231+ final s = selectedCount == 1 ? "" : "s" ;
173232
174233 return AlertDialog (
175234 title: Text ('Delete Mod$s ' ),
@@ -180,9 +239,7 @@ Future<void> showDeleteModFoldersConfirmationDialog(
180239 crossAxisAlignment: CrossAxisAlignment .start,
181240 spacing: 8 ,
182241 children: [
183- if (filteredVariantsToDelete.isNotEmpty ||
184- isOnlyEnabledMods)
185- const Text ('Are you sure you want to delete:' ),
242+ const Text ('Select the mod folders to delete:' ),
186243 Flexible (
187244 child: FadedScrollable (
188245 child: Scrollbar (
@@ -196,67 +253,73 @@ Future<void> showDeleteModFoldersConfirmationDialog(
196253 crossAxisAlignment: CrossAxisAlignment .start,
197254 spacing: 4 ,
198255 children: [
199- for (final variant
200- in filteredVariantsToDelete)
201- _buildVariantToDeleteRow (variant, theme),
202- if (enabledVariantsThatWillNotBeDeleted
203- .isNotEmpty)
204- Padding (
205- padding: const .only (top: 16 ),
206- child: Text .rich (
207- TextSpan (
208- style: theme.textTheme.bodyLarge
209- ? .copyWith (fontSize: 20 ),
210- children: [
211- TextSpan (
212- text:
213- "The following mods will " ,
214- ),
215- TextSpan (
216- text: "not" ,
217- style: theme
218- .textTheme
219- .bodyMedium
220- ? .copyWith (
221- fontSize: 20 ,
222- fontWeight: .bold,
223- ),
224- ),
225- TextSpan (
226- text:
227- " be deleted because they are enabled:" ,
228- ),
229- ],
230- ),
256+ for (final variant in variantsToDelete)
257+ _buildVariantToDeleteRow (
258+ variant,
259+ variant.isEnabled (allMods),
260+ theme,
261+ checked: selectedSmolIds.contains (
262+ variant.smolId,
231263 ),
264+ onChanged: (newValue) {
265+ setState (() {
266+ if (newValue == true ) {
267+ selectedSmolIds.add (
268+ variant.smolId,
269+ );
270+ } else {
271+ selectedSmolIds.remove (
272+ variant.smolId,
273+ );
274+ }
275+ });
276+ },
232277 ),
233- for (final variant
234- in enabledVariantsThatWillNotBeDeleted)
235- _buildVariantToDeleteRow (variant, theme),
236278 ],
237279 ),
238280 ),
239281 ),
240282 ),
241283 ),
242284 ),
243- if (! isOnlyEnabledMods && enabledVariants.isNotEmpty)
244- CheckboxWithLabel (
245- label: "Allow deleting of currently-enabled mods" ,
246- value: allowDeletingEnabledMods,
247- onChanged: (newValue) {
248- setState (() {
249- allowDeletingEnabledMods =
250- newValue ??
251- allowDeletingEnabledModsDefaultState ??
252- false ;
253- });
254- },
285+ if (variantsToDelete.length > 5 )
286+ Row (
287+ children: [
288+ Align (
289+ alignment: Alignment .centerLeft,
290+ child: TextButton .icon (
291+ onPressed: () {
292+ setState (() {
293+ selectedSmolIds.addAll (
294+ variantsToDelete.map ((v) => v.smolId),
295+ );
296+ });
297+ },
298+ icon: const Icon (Icons .select_all),
299+ label: const Text ('Select all' ),
300+ ),
301+ ),
302+ Align (
303+ alignment: Alignment .centerLeft,
304+ child: TextButton .icon (
305+ onPressed: () {
306+ setState (() {
307+ selectedSmolIds.clear ();
308+ });
309+ },
310+ icon: const Icon (Icons .deselect),
311+ label: const Text ('Deselect all' ),
312+ ),
313+ ),
314+ ],
255315 ),
256- Text (
257- "This will delete the mod folder$s on disk. This action cannot be undone." ,
258- style: theme.textTheme.bodyMedium? .copyWith (
259- fontWeight: FontWeight .bold,
316+ Padding (
317+ padding: const .only (top: 16 ),
318+ child: Text (
319+ "This will delete the mod folder$s on disk. This action cannot be undone." ,
320+ style: theme.textTheme.bodyMedium? .copyWith (
321+ fontWeight: FontWeight .bold,
322+ ),
260323 ),
261324 ),
262325 ],
@@ -267,12 +330,13 @@ Future<void> showDeleteModFoldersConfirmationDialog(
267330 onPressed: () => Navigator .of (context).pop (false ),
268331 child: const Text ('Cancel' ),
269332 ),
270- TextButton .icon (
271- onPressed: () => Navigator .of (context).pop (true ),
272- label: Text (
273- 'Delete ${filteredVariantsToDelete .length } Mod$s ' ,
333+ Disable (
334+ isEnabled: selectedCount > 0 ,
335+ child: TextButton .icon (
336+ onPressed: () => Navigator .of (context).pop (true ),
337+ label: Text ('Delete $selectedCount Mod$s ' ),
338+ icon: const Icon (Icons .delete),
274339 ),
275- icon: const Icon (Icons .delete),
276340 ),
277341 ],
278342 );
@@ -282,7 +346,10 @@ Future<void> showDeleteModFoldersConfirmationDialog(
282346 );
283347
284348 if (shouldDelete == true ) {
285- for (var variant in filteredVariantsToDelete) {
349+ final toDelete = variantsToDelete
350+ .where ((v) => selectedSmolIds.contains (v.smolId))
351+ .toList ();
352+ for (var variant in toDelete) {
286353 deleteFolder (variant.modFolder.absolute.path);
287354 }
288355 }
@@ -297,30 +364,3 @@ Future<void> showDeleteModFoldersConfirmationDialog(
297364 },
298365 );
299366}
300-
301- Row _buildVariantToDeleteRow (ModVariant variant, ThemeData theme) {
302- return Row (
303- children: [
304- Checkbox (value: true , onChanged: (newValue) {}),
305- Padding (
306- padding: const .only (left: 4 ),
307- child: Column (
308- mainAxisSize: MainAxisSize .min,
309- crossAxisAlignment: CrossAxisAlignment .start,
310- children: [
311- Text (
312- "${variant .modInfo .nameOrId } v${variant .modInfo .version }" ,
313- style: theme.textTheme.labelLarge,
314- ),
315- Text (
316- variant.modFolder.path,
317- style: theme.textTheme.labelMedium? .copyWith (
318- color: theme.textTheme.bodyMedium? .color? .withAlpha (200 ),
319- ),
320- ),
321- ],
322- ),
323- ),
324- ],
325- );
326- }
0 commit comments