import 'dart:io'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_code_editor/flutter_code_editor.dart'; import 'package:flutter_highlight/themes/monokai-sublime.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:gap/gap.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:island/models/site_file.dart'; import 'package:island/models/publication_site.dart'; import 'package:island/pods/site_files.dart'; import 'package:island/pods/network.dart'; import 'package:island/widgets/alert.dart'; import 'package:island/widgets/content/sheet.dart'; import 'package:material_symbols_icons/symbols.dart'; import 'package:path_provider/path_provider.dart'; import 'package:styled_widget/styled_widget.dart'; class FileItem extends HookConsumerWidget { final SnSiteFileEntry file; final SnPublicationSite site; final void Function(String path)? onNavigateDirectory; const FileItem({ super.key, required this.file, required this.site, this.onNavigateDirectory, }); Future _downloadFile(BuildContext context, WidgetRef ref) async { try { final apiClient = ref.read(apiClientProvider); // Get downloads directory Directory? directory; if (Platform.isAndroid) { directory = await getExternalStorageDirectory(); if (directory != null) { directory = Directory('${directory.path}/Download'); } } else { directory = await getDownloadsDirectory(); } if (directory == null) { throw Exception('Unable to access downloads directory'); } // Create directory if it doesn't exist await directory.create(recursive: true); // Generate file path final fileName = file.relativePath.split('/').last; final filePath = '${directory.path}/$fileName'; // Use Dio's download method to directly stream from server to file await apiClient.download( '/zone/sites/${site.id}/files/content/${file.relativePath}', filePath, ); showSnackBar('Downloaded to $filePath'); } catch (e) { showErrorAlert(e); } } Future _showEditSheet(BuildContext context, WidgetRef ref) async { try { final fileContent = await ref.read( siteFileContentProvider( siteId: site.id, relativePath: file.relativePath, ).future, ); if (context.mounted) { await showModalBottomSheet( context: context, isScrollControlled: true, useSafeArea: false, constraints: BoxConstraints( maxHeight: MediaQuery.of(context).size.height, ), barrierColor: Theme.of(context).colorScheme.surfaceContainerLow, backgroundColor: Theme.of(context).colorScheme.surfaceContainerLow, builder: (BuildContext context) { return FileEditorSheet( file: file, site: site, initialContent: fileContent.content, ); }, ); } } catch (e) { showErrorAlert(e); } } @override Widget build(BuildContext context, WidgetRef ref) { final theme = Theme.of(context); return Card( color: Theme.of(context).colorScheme.surfaceContainerHigh, elevation: 0, child: ListTile( shape: const RoundedRectangleBorder( borderRadius: BorderRadius.all(Radius.circular(8)), ), leading: Icon( file.isDirectory ? Symbols.folder : Symbols.description, color: theme.colorScheme.primary, ), title: Text(file.relativePath), subtitle: Text( file.isDirectory ? 'Directory' : '${(file.size / 1024).toStringAsFixed(1)} KB', ), trailing: PopupMenuButton( itemBuilder: (context) => [ PopupMenuItem( value: 'download', child: Row( children: [ const Icon(Symbols.download), const Gap(16), Text('Download'), ], ), ), if (!file.isDirectory) ...[ PopupMenuItem( value: 'edit', child: Row( children: [ const Icon(Symbols.edit), const Gap(16), Text('Edit Content'), ], ), ), ], PopupMenuItem( value: 'delete', child: Row( children: [ const Icon(Symbols.delete, color: Colors.red), const Gap(16), Text('Delete').textColor(Colors.red), ], ), ), ], onSelected: (value) async { switch (value) { case 'download': await _downloadFile(context, ref); break; case 'edit': await _showEditSheet(context, ref); break; case 'delete': final confirmed = await showDialog( context: context, builder: (context) => AlertDialog( title: const Text('Delete File'), content: Text( 'Are you sure you want to delete "${file.relativePath}"?', ), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(false), child: const Text('Cancel'), ), TextButton( onPressed: () => Navigator.of(context).pop(true), child: const Text('Delete'), ), ], ), ); if (confirmed == true) { try { await ref .read( siteFilesNotifierProvider(( siteId: site.id, path: null, )).notifier, ) .deleteFile(file.relativePath); showSnackBar('File deleted successfully'); } catch (e) { showErrorAlert(e); } } break; } }, ), onTap: () { if (file.isDirectory) { onNavigateDirectory?.call(file.relativePath); } else { _showEditSheet(context, ref); } }, ), ); } } class FileEditorSheet extends HookConsumerWidget { final SnSiteFileEntry file; final SnPublicationSite site; final String initialContent; const FileEditorSheet({ super.key, required this.file, required this.site, required this.initialContent, }); @override Widget build(BuildContext context, WidgetRef ref) { final codeController = useMemoized( () => CodeController( text: initialContent, language: null, // Let the editor auto-detect or use plain text ), ); final isSaving = useState(false); final saveFile = useCallback(() async { if (codeController.text.trim().isEmpty) { showSnackBar('contentCantEmpty'.tr()); return; } isSaving.value = true; try { await ref .read( siteFilesNotifierProvider((siteId: site.id, path: null)).notifier, ) .updateFileContent(file.relativePath, codeController.text); if (context.mounted) { showSnackBar('File saved successfully'); Navigator.of(context).pop(); } } catch (e) { showErrorAlert(e); } finally { isSaving.value = false; } }, [codeController, ref, site.id, file.relativePath, context, isSaving]); return SheetScaffold( heightFactor: 1, titleText: 'Edit ${file.relativePath}', actions: [ FilledButton( onPressed: isSaving.value ? null : saveFile, child: Text(isSaving.value ? 'Saving...' : 'Save'), ), ], child: SingleChildScrollView( padding: EdgeInsets.zero, child: CodeTheme( data: CodeThemeData(styles: monokaiSublimeTheme), child: CodeField( controller: codeController, minLines: 20, maxLines: null, ), ), ), ); } }