269 lines
8.7 KiB
Dart
269 lines
8.7 KiB
Dart
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<GalleryScreen> createState() => _GalleryScreenState();
|
|
}
|
|
|
|
class _GalleryScreenState extends State<GalleryScreen> {
|
|
final _scroll = ScrollController();
|
|
final List<AssetEntity> _assets = [];
|
|
final Set<String> _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<void> _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<void> _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<void> _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<UploadController>().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',
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|