import 'dart:io'; import 'package:flutter/material.dart'; import 'package:path_provider/path_provider.dart'; import 'package:photo_manager/photo_manager.dart'; import 'package:photo_manager_image_provider/photo_manager_image_provider.dart'; import 'package:provider/provider.dart'; import '../controllers/upload_controller.dart'; import '../core/app_theme.dart'; class GalleryScreen extends StatefulWidget { const GalleryScreen({super.key, required this.folderId}); final String? folderId; @override State createState() => _GalleryScreenState(); } class _GalleryScreenState extends State { final _scroll = ScrollController(); final List _assets = []; final Set _selected = {}; AssetPathEntity? _album; int _page = 0; bool _loading = true; bool _more = true; bool _queueing = false; String? _error; @override void initState() { super.initState(); _scroll.addListener(() { if (_scroll.position.extentAfter < 800) _loadMore(); }); _initialize(); } @override void dispose() { _scroll.dispose(); super.dispose(); } Future _initialize() async { final permission = await PhotoManager.requestPermissionExtend(); if (!permission.hasAccess) { setState(() { _loading = false; _error = 'Gallery access is required to choose your media.'; }); return; } final albums = await PhotoManager.getAssetPathList( type: RequestType.common, onlyAll: true, ); if (albums.isEmpty) { setState(() { _loading = false; _error = 'No photos or videos were found.'; }); return; } _album = albums.first; await _loadMore(); } Future _loadMore() async { if (!_more || (_loading && _assets.isNotEmpty) || _album == null && !_loading) { return; } final album = _album; if (album == null) return; if (_assets.isNotEmpty) setState(() => _loading = true); final page = await album.getAssetListPaged(page: _page, size: 80); if (!mounted) return; setState(() { _assets.addAll(page); _page++; _more = page.length == 80; _loading = false; }); } Future _queueUploads() async { if (_selected.isEmpty) return; setState(() => _queueing = true); var queued = 0; try { final support = await getApplicationSupportDirectory(); final queue = Directory( '${support.path}${Platform.pathSeparator}upload_queue', ); await queue.create(recursive: true); final chosen = _assets .where((asset) => _selected.contains(asset.id)) .toList(); for (var index = 0; index < chosen.length; index++) { final asset = chosen[index]; final source = await asset.originFile; if (source == null) continue; final original = (asset.title ?? source.uri.pathSegments.last) .replaceAll(RegExp(r'[\\/:*?"<>|]'), '_'); final stagedName = '${DateTime.now().microsecondsSinceEpoch}_$index-$original'; await source.copy('${queue.path}${Platform.pathSeparator}$stagedName'); if (!mounted) return; await context.read().enqueue( filename: stagedName, displayName: original, folderId: widget.folderId, ); queued++; } if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text( '$queued file${queued == 1 ? '' : 's'} added to the background upload queue.', ), ), ); Navigator.pop(context, true); } } catch (error) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Could not queue all files: $error')), ); } } finally { if (mounted) setState(() => _queueing = false); } } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('Choose from gallery'), actions: [ if (_selected.isNotEmpty) TextButton( onPressed: () => setState(_selected.clear), child: const Text('Clear'), ), ], ), body: _error != null ? Center( child: Padding( padding: const EdgeInsets.all(32), child: Column( mainAxisSize: MainAxisSize.min, children: [ const Icon( Icons.photo_library_outlined, size: 62, color: AppTheme.blue, ), const SizedBox(height: 14), Text(_error!, textAlign: TextAlign.center), const SizedBox(height: 14), OutlinedButton( onPressed: PhotoManager.openSetting, child: const Text('Open settings'), ), ], ), ), ) : _assets.isEmpty && _loading ? const Center(child: CircularProgressIndicator()) : GridView.builder( controller: _scroll, padding: const EdgeInsets.fromLTRB(3, 3, 3, 110), gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 3, mainAxisSpacing: 3, crossAxisSpacing: 3, ), itemCount: _assets.length + (_loading ? 1 : 0), itemBuilder: (context, index) { if (index >= _assets.length) { return const Center( child: CircularProgressIndicator(strokeWidth: 2), ); } final asset = _assets[index]; final selected = _selected.contains(asset.id); return GestureDetector( onTap: () => setState( () => selected ? _selected.remove(asset.id) : _selected.add(asset.id), ), child: Stack( fit: StackFit.expand, children: [ AssetEntityImage( asset, isOriginal: false, thumbnailSize: const ThumbnailSize.square(350), fit: BoxFit.cover, ), if (selected) Container(color: AppTheme.blue.withValues(alpha: .30)), if (asset.type == AssetType.video) const Positioned( left: 7, bottom: 7, child: Icon( Icons.videocam_rounded, color: Colors.white, ), ), Positioned( right: 7, top: 7, child: AnimatedContainer( duration: const Duration(milliseconds: 150), width: 25, height: 25, decoration: BoxDecoration( shape: BoxShape.circle, color: selected ? AppTheme.blue : Colors.black38, border: Border.all(color: Colors.white, width: 2), ), child: selected ? const Icon( Icons.check, color: Colors.white, size: 17, ) : null, ), ), ], ), ); }, ), bottomNavigationBar: SafeArea( child: Padding( padding: const EdgeInsets.all(14), child: FilledButton.icon( onPressed: _selected.isEmpty || _queueing ? null : _queueUploads, icon: _queueing ? const SizedBox.square( dimension: 20, child: CircularProgressIndicator( strokeWidth: 2, color: Colors.white, ), ) : const Icon(Icons.cloud_upload_outlined), label: Text( _selected.isEmpty ? 'Select photos or videos' : 'Upload ${_selected.length} selected', ), ), ), ), ); } }