11import 'dart:convert' ;
22import 'dart:io' ;
33import 'dart:async' ;
4- import 'dart:typed_data' ;
54import 'package:flutter/material.dart' ;
65import 'package:file_picker/file_picker.dart' ;
76import 'package:path_provider/path_provider.dart' ;
87import 'package:path/path.dart' as path;
98import 'package:http/http.dart' as http;
109import 'package:html/parser.dart' as html;
10+ import 'package:permission_handler/permission_handler.dart' ; // Import permission_handler
1111
1212class JSONProcessorTab extends StatefulWidget {
1313 const JSONProcessorTab ({super .key});
@@ -58,6 +58,7 @@ class _JSONProcessorTabState extends State<JSONProcessorTab> with SingleTickerPr
5858 }
5959
6060 void _addLog (String message, {LogType type = LogType .info}) {
61+ if (! mounted) return ;
6162 setState (() {
6263 _logs.insert (0 , LogEntry (
6364 message: message,
@@ -149,39 +150,13 @@ class _JSONProcessorTabState extends State<JSONProcessorTab> with SingleTickerPr
149150 }
150151 }
151152 } catch (e) {
152- // Silent fail for single attempt
153+ _addLog ( 'Error fetching UID for $ link : $ e ' , type : LogType .warning);
153154 }
154155
155156 return null ;
156157 }
157158
158- Future <String ?> _extractUidWithRetry (String link, int recordIndex) async {
159- const maxRetries = 3 ;
160-
161- for (int attempt = 1 ; attempt <= maxRetries; attempt++ ) {
162- _addLog ('Record ${recordIndex + 1 }: Attempt $attempt /$maxRetries ' , type: LogType .info);
163-
164- try {
165- final uid = await _extractUidSingleAttempt (link);
166- if (uid != null ) {
167- _addLog ('✓ Record ${recordIndex + 1 }: UID found on attempt $attempt : $uid ' , type: LogType .success);
168- return uid;
169- }
170-
171- if (attempt < maxRetries) {
172- await Future .delayed (Duration (seconds: attempt * 1 )); // Progressive delay
173- }
174- } catch (e) {
175- _addLog ('Record ${recordIndex + 1 }: Attempt $attempt failed: $e ' , type: LogType .warning);
176- if (attempt < maxRetries) {
177- await Future .delayed (Duration (seconds: attempt * 1 ));
178- }
179- }
180- }
181-
182- _addLog ('✗ Record ${recordIndex + 1 }: Failed to extract UID after $maxRetries attempts' , type: LogType .error);
183- return null ;
184- }
159+ // REMOVED: _extractUidWithRetry function is no longer needed.
185160
186161 Future <Map <String , dynamic >?> _processRecordConcurrently (int index, Map <String , dynamic > record) async {
187162 final username = record['username' ]? .toString () ?? '' ;
@@ -191,13 +166,14 @@ class _JSONProcessorTabState extends State<JSONProcessorTab> with SingleTickerPr
191166 return record;
192167 }
193168
194- _addLog ('Record ${index + 1 }: Starting UID extraction' , type: LogType .info);
195- final uid = await _extractUidWithRetry (username, index);
169+ _addLog ('Record ${index + 1 }: Starting UID extraction for $username ' , type: LogType .info);
170+ // MODIFIED: Call single attempt function directly.
171+ final uid = await _extractUidSingleAttempt (username);
196172
197173 if (uid != null ) {
198174 final updatedRecord = Map <String , dynamic >.from (record);
199175 updatedRecord['username' ] = uid;
200- _addLog ('✓ Record ${index + 1 }: Successfully replaced with UID' , type: LogType .success);
176+ _addLog ('✓ Record ${index + 1 }: Successfully replaced with UID: $ uid ' , type: LogType .success);
201177 return updatedRecord;
202178 } else {
203179 _addLog ('✗ Record ${index + 1 }: Removing from final data (UID not found)' , type: LogType .error);
@@ -304,39 +280,51 @@ class _JSONProcessorTabState extends State<JSONProcessorTab> with SingleTickerPr
304280 }
305281
306282 try {
307- final String fileName = _fileName ?? 'processed_data' ;
308- final String baseName = path.basenameWithoutExtension (fileName);
309- final String newFileName = 'uid_$baseName .json' ;
310-
311- Directory downloadsDir;
312-
313- // Use the same approach that works in JSONToExcelTab
283+ // FIXED: Request permission before trying to save
314284 if (Platform .isAndroid) {
315- downloadsDir = Directory ('/storage/emulated/0/Download' );
316- if (! await downloadsDir.exists ()) {
317- downloadsDir = (await getExternalStorageDirectory ())! ;
285+ var status = await Permission .storage.status;
286+ if (! status.isGranted) {
287+ status = await Permission .storage.request ();
288+ }
289+ if (! status.isGranted) {
290+ _addLog ('Storage permission denied. Cannot save file.' , type: LogType .error);
291+ ScaffoldMessenger .of (context).showSnackBar (
292+ const SnackBar (
293+ content: Text ('✗ Storage permission is required to save files.' ),
294+ backgroundColor: Colors .red,
295+ ),
296+ );
297+ return ;
318298 }
319- } else {
320- downloadsDir = (await getDownloadsDirectory ())! ;
321299 }
322-
323- final Directory saveDir = Directory ('${downloadsDir .path }/fb_saver' );
324- if (! await saveDir.exists ()) {
325- await saveDir.create (recursive: true );
300+
301+ // FIXED: Let the user pick the save location
302+ final String ? outputDirectory = await FilePicker .platform.getDirectoryPath (
303+ dialogTitle: 'Please select where to save the file' ,
304+ );
305+
306+ if (outputDirectory == null ) {
307+ // User cancelled the picker
308+ _addLog ('Save operation cancelled by user.' , type: LogType .warning);
309+ return ;
326310 }
311+
312+ final String fileName = _fileName ?? 'processed_data' ;
313+ final String baseName = path.basenameWithoutExtension (fileName);
314+ final String newFileName = 'uid_$baseName .json' ;
327315
328- final filePath = path.join (saveDir.path , newFileName);
316+ final filePath = path.join (outputDirectory , newFileName);
329317 final file = File (filePath);
330318
331319 await file.writeAsString (json.encode (_processedData));
332320
333321 // Verify file was created
334322 if (await file.exists ()) {
335- _addLog ('File saved successfully: ${ file . path } ' , type: LogType .success);
323+ _addLog ('File saved successfully: $filePath ' , type: LogType .success);
336324
337325 ScaffoldMessenger .of (context).showSnackBar (
338326 SnackBar (
339- content: Text ('✓ File saved to: ${ file . path } ' ),
327+ content: Text ('✓ File saved to: $filePath ' ),
340328 backgroundColor: const Color (0xFF467731 ),
341329 behavior: SnackBarBehavior .floating,
342330 ),
@@ -370,6 +358,10 @@ class _JSONProcessorTabState extends State<JSONProcessorTab> with SingleTickerPr
370358 });
371359 }
372360
361+ // --- OMITTED: The rest of the file (build method, etc.) is unchanged ---
362+ // --- Please use your existing build methods as they are correct ---
363+ // --- _buildStatItem, _buildLogCard, LogType, LogEntry, etc. are also unchanged ---
364+
373365 @override
374366 Widget build (BuildContext context) {
375367 return Padding (
@@ -611,4 +603,4 @@ class LogEntry {
611603 required this .timestamp,
612604 required this .type,
613605 });
614- }
606+ }
0 commit comments