import 'dart:async'; import "dart:math"; import 'package:flutter/material.dart'; import 'package:logging/logging.dart'; import 'package:photos/core/event_bus.dart'; import 'package:photos/db/device_files_db.dart'; import 'package:photos/db/files_db.dart'; import 'package:photos/events/backup_folders_updated_event.dart'; import 'package:photos/events/local_photos_updated_event.dart'; import "package:photos/generated/l10n.dart"; import 'package:photos/models/device_collection.dart'; import "package:photos/ui/collections/device/device_folder_item.dart"; import 'package:photos/ui/common/loading_widget.dart'; import 'package:photos/ui/viewer/gallery/empty_state.dart'; import "package:photos/utils/debouncer.dart"; class DeviceFolderVerticalGridView extends StatelessWidget { final Widget? appTitle; final String? tag; const DeviceFolderVerticalGridView({this.appTitle, this.tag, super.key}); @override Widget build(BuildContext context) { return Scaffold( body: CustomScrollView( physics: const BouncingScrollPhysics(), slivers: [ SliverAppBar( elevation: 0, title: tag != null ? Hero( tag: tag!, child: appTitle ?? const SizedBox.shrink(), ) : appTitle ?? const SizedBox.shrink(), floating: true, ), const _DeviceFolderVerticalGridViewBody(), ], ), ); } } class _DeviceFolderVerticalGridViewBody extends StatefulWidget { const _DeviceFolderVerticalGridViewBody({ Key? key, }) : super(key: key); @override State<_DeviceFolderVerticalGridViewBody> createState() => _DeviceFolderVerticalGridViewBodyState(); } class _DeviceFolderVerticalGridViewBodyState extends State<_DeviceFolderVerticalGridViewBody> { StreamSubscription? _backupFoldersUpdatedEvent; StreamSubscription? _localFilesSubscription; String _loadReason = "init"; final logger = Logger((_DeviceFolderVerticalGridViewBodyState).toString()); final _debouncer = Debouncer( const Duration(milliseconds: 1500), executionInterval: const Duration(seconds: 4), ); /* Aspect ratio 1:1 Max width 224 Fixed gap 8 Width changes dynamically with screen width such that we can fit 2 in one row. Keep the width integral (center the albums to distribute excess pixels) */ static const maxThumbnailWidth = 224.0; static const fixedGapBetweenAlbum = 8.0; static const minGapForHorizontalPadding = 8.0; @override void initState() { _backupFoldersUpdatedEvent = Bus.instance.on().listen((event) { _loadReason = event.reason; if (mounted) { setState(() {}); } }); _localFilesSubscription = Bus.instance.on().listen((event) { _debouncer.run(() async { if (mounted) { _loadReason = event.reason; setState(() {}); } }); }); super.initState(); } @override Widget build(BuildContext context) { debugPrint( "${(_DeviceFolderVerticalGridViewBody).toString()} - $_loadReason", ); return FutureBuilder>( future: FilesDB.instance.getDeviceCollections(includeCoverThumbnail: true), builder: (context, snapshot) { if (snapshot.hasData) { final double screenWidth = MediaQuery.of(context).size.width; final int albumsCountInOneRow = max(screenWidth ~/ maxThumbnailWidth, 2); final double gapBetweenAlbums = (albumsCountInOneRow - 1) * fixedGapBetweenAlbum; final double gapOnSizeOfAlbums = minGapForHorizontalPadding + (screenWidth - gapBetweenAlbums - (2 * minGapForHorizontalPadding)) % albumsCountInOneRow; final double sideOfThumbnail = (screenWidth - gapOnSizeOfAlbums - gapBetweenAlbums) / albumsCountInOneRow; return snapshot.data!.isEmpty ? const SliverFillRemaining(child: EmptyState()) : SliverPadding( padding: const EdgeInsets.symmetric(horizontal: 8), sliver: SliverGrid( gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: albumsCountInOneRow, mainAxisSpacing: 4, crossAxisSpacing: gapBetweenAlbums, childAspectRatio: sideOfThumbnail / (sideOfThumbnail + 46), ), delegate: SliverChildBuilderDelegate( (BuildContext context, int index) { final deviceCollection = snapshot.data![index]; return DeviceFolderItem( deviceCollection, sideOfThumbnail: sideOfThumbnail, ); }, childCount: snapshot.data!.length, ), ), ); } else if (snapshot.hasError) { logger.severe("failed to load device gallery", snapshot.error); return SliverFillRemaining( child: Center(child: Text(S.of(context).failedToLoadAlbums)), ); } else { return const SliverFillRemaining(child: EnteLoadingWidget()); } }, ); } @override void dispose() { _backupFoldersUpdatedEvent?.cancel(); _localFilesSubscription?.cancel(); _debouncer.cancelDebounce(); super.dispose(); } }