@@ -54,22 +54,26 @@ class SettingsController extends GetxController {
5454 // InheritedProfiles profilesWidget = ProfilesWidget.of(context);
5555 var profilesWidget = Get .find <SplashController >();
5656 Directory source = profilesWidget.baseDirectory ();
57- Directory destination = Directory (value);
57+ Directory destination = Directory (value);
5858 moveDirectory (source.path, destination.path).then ((value) async {
5959 isMovingDirectory.value = false ;
6060 update ();
6161 if (value == "same" ) {
6262 return ;
63- } else if (value == "success" ) {
63+ } else if (value == "success" ) {
6464 profilesWidget.setBaseDirectory (destination);
6565 SharedPreferences prefs = await SharedPreferences .getInstance ();
6666 prefs.setString ('baseDirectory' , destination.path);
6767 baseDirectory.value = destination.path;
68- } else {
69- showDialog (
70- context: context,
71- builder: (BuildContext context) {
72- return Utils .showAlertDialog (
68+ Get .snackbar (
69+ 'Success' ,
70+ 'Base directory moved successfully' ,
71+ snackPosition: SnackPosition .BOTTOM ,
72+ duration: const Duration (seconds: 2 ),
73+ );
74+ } else {
75+ Get .dialog (
76+ Utils .showAlertDialog (
7377 title: Text (
7478 'Error' ,
7579 style: GoogleFonts .poppins (
@@ -83,7 +87,9 @@ class SettingsController extends GetxController {
8387 ? "Cannot move to a nested directory"
8488 : value == "not-empty"
8589 ? "Destination directory is not empty"
86- : "An error occurred" ,
90+ : value == "not-permitted"
91+ ? "Selected folder can't be written to (Android SAF). Please choose a different folder."
92+ : "An error occurred" ,
8793 style: GoogleFonts .poppins (
8894 color: TaskWarriorColors .grey,
8995 fontSize: TaskWarriorFonts .fontSizeSmall,
@@ -92,7 +98,7 @@ class SettingsController extends GetxController {
9298 actions: [
9399 TextButton (
94100 onPressed: () {
95- Navigator . pop (context );
101+ Get . back ( );
96102 },
97103 child: Text (
98104 'OK' ,
@@ -102,10 +108,9 @@ class SettingsController extends GetxController {
102108 ),
103109 )
104110 ],
105- );
106- },
107- );
108- }
111+ ),
112+ );
113+ }
109114 });
110115 }
111116 });
@@ -120,16 +125,48 @@ class SettingsController extends GetxController {
120125 return "nested" ;
121126 }
122127
123- Directory toDir = Directory (toDirectory);
128+ Directory toDir = Directory (toDirectory);
129+ // Ensure destination exists before checking contents
130+ await toDir.create (recursive: true );
124131 final length = await toDir.list ().length;
125132 if (length > 0 ) {
126133 return "not-empty" ;
127134 }
128135
129- await moveDirectoryRecurse (fromDirectory, toDirectory);
130- return "success" ;
136+ // Preflight: on Android, check that we can actually write to the chosen directory
137+ // to avoid crashing with Operation not permitted when a SAF tree URI was selected.
138+ try {
139+ final testFile = File (path.join (toDirectory, ".tw_write_test" ));
140+ await toDir.create (recursive: true );
141+ await testFile.writeAsString ("ok" );
142+ await testFile.delete ();
143+ } on FileSystemException catch (e) {
144+ // Map common permission error to a friendly status
145+ if (e.osError? .errorCode == 1 ||
146+ (e.osError? .message.toLowerCase ().contains ("operation not permitted" ) ?? false )) {
147+ return "not-permitted" ;
148+ }
149+ return "error" ;
150+ } catch (_) {
151+ return "error" ;
152+ }
153+
154+ try {
155+ await moveDirectoryRecurse (fromDirectory, toDirectory);
156+ return "success" ;
157+ } on FileSystemException catch (e) {
158+ if (e.osError? .errorCode == 1 ||
159+ (e.osError? .message.toLowerCase ().contains ("operation not permitted" ) ?? false )) {
160+ return "not-permitted" ;
161+ }
162+ return "error" ;
163+ } catch (_) {
164+ return "error" ;
165+ }
131166 }
132167
168+ // ... no hardcoded SAF path mapping; rely on guard and proper APIs if enabled in future
169+
133170 Future <void > moveDirectoryRecurse (
134171 String fromDirectory, String toDirectory) async {
135172 Directory fromDir = Directory (fromDirectory);
@@ -140,18 +177,21 @@ class SettingsController extends GetxController {
140177
141178 // Loop through each file and directory and move it to the toDirectory
142179 await for (final entity in fromDir.list ()) {
180+ // Skip flutter runtime assets – they should not be moved
181+ final relativePath = path.relative (entity.path, from: fromDirectory);
182+ if (relativePath.split (path.separator).contains ('flutter_assets' )) {
183+ continue ;
184+ }
143185 if (entity is File ) {
144186 // If it's a file, move it to the toDirectory
145187 File file = entity;
146- String newPath = path.join (
147- toDirectory, path.relative (file.path, from: fromDirectory));
188+ String newPath = path.join (toDirectory, relativePath);
148189 await File (newPath).writeAsBytes (await file.readAsBytes ());
149190 await file.delete ();
150191 } else if (entity is Directory ) {
151192 // If it's a directory, create it in the toDirectory and recursively move its contents
152193 Directory dir = entity;
153- String newPath = path.join (
154- toDirectory, path.relative (dir.path, from: fromDirectory));
194+ String newPath = path.join (toDirectory, relativePath);
155195 Directory newDir = Directory (newPath);
156196 await newDir.create (recursive: true );
157197 await moveDirectoryRecurse (dir.path, newPath);
0 commit comments