mirror of
				https://github.com/immich-app/immich.git
				synced 2025-10-31 02:27:08 -04:00 
			
		
		
		
	* thumbnail widget * use animation ticker, improvements * use static thumbnail resolution for now * fix android sample size * free memory sooner * formatting * tweaks * wait for disposal * remove debug prints * take two on animation * fix * remote constructor * missed one * unused imports * unnecessary import * formatting
		
			
				
	
	
		
			418 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
			
		
		
	
	
			418 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
| import 'package:auto_route/auto_route.dart';
 | |
| import 'package:flutter/material.dart';
 | |
| import 'package:hooks_riverpod/hooks_riverpod.dart';
 | |
| import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
 | |
| import 'package:immich_mobile/extensions/build_context_extensions.dart';
 | |
| import 'package:immich_mobile/extensions/translate_extensions.dart';
 | |
| import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart';
 | |
| import 'package:immich_mobile/providers/infrastructure/album.provider.dart';
 | |
| import 'package:immich_mobile/providers/infrastructure/current_album.provider.dart';
 | |
| import 'package:immich_mobile/routing/router.dart';
 | |
| import 'package:immich_mobile/widgets/album/album_action_filled_button.dart';
 | |
| 
 | |
| @RoutePage()
 | |
| class DriftCreateAlbumPage extends ConsumerStatefulWidget {
 | |
|   const DriftCreateAlbumPage({super.key});
 | |
| 
 | |
|   @override
 | |
|   ConsumerState<DriftCreateAlbumPage> createState() => _DriftCreateAlbumPageState();
 | |
| }
 | |
| 
 | |
| class _DriftCreateAlbumPageState extends ConsumerState<DriftCreateAlbumPage> {
 | |
|   TextEditingController albumTitleController = TextEditingController();
 | |
|   TextEditingController albumDescriptionController = TextEditingController();
 | |
|   FocusNode albumTitleTextFieldFocusNode = FocusNode();
 | |
|   FocusNode albumDescriptionTextFieldFocusNode = FocusNode();
 | |
|   bool isAlbumTitleTextFieldFocus = false;
 | |
|   Set<BaseAsset> selectedAssets = {};
 | |
| 
 | |
|   @override
 | |
|   void dispose() {
 | |
|     albumTitleController.dispose();
 | |
|     albumDescriptionController.dispose();
 | |
|     albumTitleTextFieldFocusNode.dispose();
 | |
|     albumDescriptionTextFieldFocusNode.dispose();
 | |
|     super.dispose();
 | |
|   }
 | |
| 
 | |
|   bool get _canCreateAlbum => albumTitleController.text.isNotEmpty;
 | |
| 
 | |
|   String _getEffectiveTitle() {
 | |
|     return albumTitleController.text.isNotEmpty
 | |
|         ? albumTitleController.text
 | |
|         : 'create_album_page_untitled'.t(context: context);
 | |
|   }
 | |
| 
 | |
|   Widget _buildSliverAppBar() {
 | |
|     return SliverAppBar(
 | |
|       backgroundColor: context.scaffoldBackgroundColor,
 | |
|       elevation: 0,
 | |
|       automaticallyImplyLeading: false,
 | |
|       pinned: true,
 | |
|       snap: false,
 | |
|       floating: false,
 | |
|       bottom: PreferredSize(
 | |
|         preferredSize: const Size.fromHeight(200.0),
 | |
|         child: SizedBox(
 | |
|           height: 200,
 | |
|           child: Column(
 | |
|             mainAxisAlignment: MainAxisAlignment.start,
 | |
|             children: [
 | |
|               buildTitleInputField(),
 | |
|               buildDescriptionInputField(),
 | |
|               if (selectedAssets.isNotEmpty) buildControlButton(),
 | |
|             ],
 | |
|           ),
 | |
|         ),
 | |
|       ),
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   Widget _buildContent() {
 | |
|     if (selectedAssets.isEmpty) {
 | |
|       return SliverList(delegate: SliverChildListDelegate([_buildEmptyState(), _buildSelectPhotosButton()]));
 | |
|     } else {
 | |
|       return _buildSelectedImageGrid();
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   Widget _buildEmptyState() {
 | |
|     return Padding(
 | |
|       padding: const EdgeInsets.only(top: 0, left: 18),
 | |
|       child: Text('create_shared_album_page_share_add_assets', style: context.textTheme.labelLarge).t(),
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   Widget _buildSelectPhotosButton() {
 | |
|     return Padding(
 | |
|       padding: const EdgeInsets.all(16.0),
 | |
|       child: FilledButton.icon(
 | |
|         style: FilledButton.styleFrom(
 | |
|           alignment: Alignment.centerLeft,
 | |
|           padding: const EdgeInsets.symmetric(vertical: 24.0, horizontal: 16.0),
 | |
|           shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(10.0))),
 | |
|           backgroundColor: context.colorScheme.surfaceContainerHigh,
 | |
|         ),
 | |
|         onPressed: onSelectPhotos,
 | |
|         icon: Icon(Icons.add_rounded, color: context.primaryColor),
 | |
|         label: Padding(
 | |
|           padding: const EdgeInsets.only(left: 8.0),
 | |
|           child: Text(
 | |
|             'create_shared_album_page_share_select_photos',
 | |
|             style: context.textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w600, color: context.primaryColor),
 | |
|           ).t(),
 | |
|         ),
 | |
|       ),
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   Widget _buildSelectedImageGrid() {
 | |
|     return SliverPadding(
 | |
|       padding: const EdgeInsets.only(top: 16.0),
 | |
|       sliver: SliverGrid(
 | |
|         gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
 | |
|           crossAxisCount: 4,
 | |
|           crossAxisSpacing: 1.0,
 | |
|           mainAxisSpacing: 1.0,
 | |
|         ),
 | |
|         delegate: SliverChildBuilderDelegate((context, index) {
 | |
|           final asset = selectedAssets.elementAt(index);
 | |
|           return GestureDetector(
 | |
|             onTap: onBackgroundTapped,
 | |
|             child: Thumbnail.fromAsset(asset: asset),
 | |
|           );
 | |
|         }, childCount: selectedAssets.length),
 | |
|       ),
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   void onBackgroundTapped() {
 | |
|     albumTitleTextFieldFocusNode.unfocus();
 | |
|     albumDescriptionTextFieldFocusNode.unfocus();
 | |
|     setState(() {
 | |
|       isAlbumTitleTextFieldFocus = false;
 | |
|     });
 | |
| 
 | |
|     if (albumTitleController.text.isEmpty) {
 | |
|       final untitledText = 'create_album_page_untitled'.t();
 | |
|       albumTitleController.text = untitledText;
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   Future<void> onSelectPhotos() async {
 | |
|     final assets = await context.pushRoute<Set<BaseAsset>>(
 | |
|       DriftAssetSelectionTimelineRoute(lockedSelectionAssets: selectedAssets),
 | |
|     );
 | |
| 
 | |
|     if (assets == null || assets.isEmpty) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     setState(() {
 | |
|       selectedAssets = selectedAssets.union(assets);
 | |
|     });
 | |
|   }
 | |
| 
 | |
|   Future<void> createAlbum() async {
 | |
|     onBackgroundTapped();
 | |
| 
 | |
|     final title = _getEffectiveTitle().trim();
 | |
|     if (title.isEmpty) {
 | |
|       if (context.mounted) {
 | |
|         ScaffoldMessenger.of(context).showSnackBar(
 | |
|           SnackBar(content: Text('create_album_title_required'.t()), backgroundColor: context.colorScheme.error),
 | |
|         );
 | |
|       }
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     final album = await ref
 | |
|         .watch(remoteAlbumProvider.notifier)
 | |
|         .createAlbum(
 | |
|           title: title,
 | |
|           description: albumDescriptionController.text.trim(),
 | |
|           assetIds: selectedAssets.map((asset) {
 | |
|             final remoteAsset = asset as RemoteAsset;
 | |
|             return remoteAsset.id;
 | |
|           }).toList(),
 | |
|         );
 | |
| 
 | |
|     if (album != null) {
 | |
|       ref.read(currentRemoteAlbumProvider.notifier).setAlbum(album);
 | |
|       context.replaceRoute(RemoteAlbumRoute(album: album));
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   Widget buildTitleInputField() {
 | |
|     return Padding(
 | |
|       padding: const EdgeInsets.only(right: 10.0, left: 10.0),
 | |
|       child: _AlbumTitleTextField(
 | |
|         focusNode: albumTitleTextFieldFocusNode,
 | |
|         textController: albumTitleController,
 | |
|         isFocus: isAlbumTitleTextFieldFocus,
 | |
|         onFocusChanged: (focus) {
 | |
|           setState(() {
 | |
|             isAlbumTitleTextFieldFocus = focus;
 | |
|           });
 | |
|         },
 | |
|       ),
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   Widget buildDescriptionInputField() {
 | |
|     return Padding(
 | |
|       padding: const EdgeInsets.only(right: 10.0, left: 10.0, top: 8),
 | |
|       child: _AlbumViewerEditableDescription(
 | |
|         textController: albumDescriptionController,
 | |
|         focusNode: albumDescriptionTextFieldFocusNode,
 | |
|       ),
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   Widget buildControlButton() {
 | |
|     return Padding(
 | |
|       padding: const EdgeInsets.only(left: 12.0, top: 8.0, bottom: 8.0),
 | |
|       child: SizedBox(
 | |
|         height: 42.0,
 | |
|         child: ListView(
 | |
|           scrollDirection: Axis.horizontal,
 | |
|           children: [
 | |
|             AlbumActionFilledButton(
 | |
|               iconData: Icons.add_photo_alternate_outlined,
 | |
|               onPressed: onSelectPhotos,
 | |
|               labelText: "add_photos".t(),
 | |
|             ),
 | |
|           ],
 | |
|         ),
 | |
|       ),
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   @override
 | |
|   Widget build(BuildContext context) {
 | |
|     return Scaffold(
 | |
|       appBar: AppBar(
 | |
|         elevation: 0,
 | |
|         centerTitle: false,
 | |
|         backgroundColor: context.scaffoldBackgroundColor,
 | |
|         leading: IconButton(onPressed: () => context.maybePop(), icon: const Icon(Icons.close_rounded)),
 | |
|         title: const Text('create_album').t(),
 | |
|         actions: [
 | |
|           TextButton(
 | |
|             onPressed: _canCreateAlbum ? createAlbum : null,
 | |
|             child: Text(
 | |
|               'create'.t(),
 | |
|               style: TextStyle(
 | |
|                 fontWeight: FontWeight.bold,
 | |
|                 color: _canCreateAlbum ? context.primaryColor : context.themeData.disabledColor,
 | |
|               ),
 | |
|             ),
 | |
|           ),
 | |
|         ],
 | |
|       ),
 | |
|       body: GestureDetector(
 | |
|         onTap: onBackgroundTapped,
 | |
|         child: CustomScrollView(slivers: [_buildSliverAppBar(), _buildContent()]),
 | |
|       ),
 | |
|     );
 | |
|   }
 | |
| }
 | |
| 
 | |
| class _AlbumTitleTextField extends StatefulWidget {
 | |
|   const _AlbumTitleTextField({
 | |
|     required this.focusNode,
 | |
|     required this.textController,
 | |
|     required this.isFocus,
 | |
|     required this.onFocusChanged,
 | |
|   });
 | |
| 
 | |
|   final FocusNode focusNode;
 | |
|   final TextEditingController textController;
 | |
|   final bool isFocus;
 | |
|   final ValueChanged<bool> onFocusChanged;
 | |
| 
 | |
|   @override
 | |
|   State<_AlbumTitleTextField> createState() => _AlbumTitleTextFieldState();
 | |
| }
 | |
| 
 | |
| class _AlbumTitleTextFieldState extends State<_AlbumTitleTextField> {
 | |
|   @override
 | |
|   void initState() {
 | |
|     super.initState();
 | |
|     widget.focusNode.addListener(_onFocusChange);
 | |
|   }
 | |
| 
 | |
|   @override
 | |
|   void dispose() {
 | |
|     widget.focusNode.removeListener(_onFocusChange);
 | |
|     super.dispose();
 | |
|   }
 | |
| 
 | |
|   void _onFocusChange() {
 | |
|     widget.onFocusChanged(widget.focusNode.hasFocus);
 | |
|   }
 | |
| 
 | |
|   @override
 | |
|   Widget build(BuildContext context) {
 | |
|     return TextField(
 | |
|       focusNode: widget.focusNode,
 | |
|       style: TextStyle(fontSize: 28.0, color: context.colorScheme.onSurface, fontWeight: FontWeight.bold),
 | |
|       controller: widget.textController,
 | |
|       onTap: () {
 | |
|         if (widget.textController.text == 'create_album_page_untitled'.t(context: context)) {
 | |
|           widget.textController.clear();
 | |
|         }
 | |
|       },
 | |
|       decoration: InputDecoration(
 | |
|         contentPadding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 16.0),
 | |
|         suffixIcon: widget.textController.text.isNotEmpty && widget.isFocus
 | |
|             ? IconButton(
 | |
|                 onPressed: () {
 | |
|                   widget.textController.clear();
 | |
|                 },
 | |
|                 icon: Icon(Icons.cancel_rounded, color: context.primaryColor),
 | |
|                 splashRadius: 10.0,
 | |
|               )
 | |
|             : null,
 | |
|         enabledBorder: const OutlineInputBorder(
 | |
|           borderSide: BorderSide(color: Colors.transparent),
 | |
|           borderRadius: BorderRadius.all(Radius.circular(16.0)),
 | |
|         ),
 | |
|         focusedBorder: OutlineInputBorder(
 | |
|           borderSide: BorderSide(color: context.primaryColor.withValues(alpha: 0.3)),
 | |
|           borderRadius: const BorderRadius.all(Radius.circular(16.0)),
 | |
|         ),
 | |
|         hintText: 'add_a_title'.t(),
 | |
|         hintStyle: context.themeData.inputDecorationTheme.hintStyle?.copyWith(
 | |
|           fontSize: 28.0,
 | |
|           fontWeight: FontWeight.bold,
 | |
|           height: 1.2,
 | |
|         ),
 | |
|         focusColor: Colors.grey[300],
 | |
|         fillColor: context.colorScheme.surfaceContainerHigh,
 | |
|         filled: true,
 | |
|       ),
 | |
|     );
 | |
|   }
 | |
| }
 | |
| 
 | |
| class _AlbumViewerEditableDescription extends StatefulWidget {
 | |
|   const _AlbumViewerEditableDescription({required this.textController, required this.focusNode});
 | |
| 
 | |
|   final TextEditingController textController;
 | |
|   final FocusNode focusNode;
 | |
| 
 | |
|   @override
 | |
|   State<_AlbumViewerEditableDescription> createState() => _AlbumViewerEditableDescriptionState();
 | |
| }
 | |
| 
 | |
| class _AlbumViewerEditableDescriptionState extends State<_AlbumViewerEditableDescription> {
 | |
|   @override
 | |
|   void initState() {
 | |
|     super.initState();
 | |
|     widget.focusNode.addListener(_onFocusModeChange);
 | |
|     widget.textController.addListener(_onTextChange);
 | |
|   }
 | |
| 
 | |
|   @override
 | |
|   void dispose() {
 | |
|     widget.focusNode.removeListener(_onFocusModeChange);
 | |
|     widget.textController.removeListener(_onTextChange);
 | |
|     super.dispose();
 | |
|   }
 | |
| 
 | |
|   void _onFocusModeChange() {
 | |
|     setState(() {
 | |
|       if (!widget.focusNode.hasFocus && widget.textController.text.isEmpty) {
 | |
|         widget.textController.clear();
 | |
|       }
 | |
|     });
 | |
|   }
 | |
| 
 | |
|   void _onTextChange() {
 | |
|     setState(() {});
 | |
|   }
 | |
| 
 | |
|   @override
 | |
|   Widget build(BuildContext context) {
 | |
|     return Material(
 | |
|       color: Colors.transparent,
 | |
|       child: TextField(
 | |
|         focusNode: widget.focusNode,
 | |
|         style: context.textTheme.bodyLarge,
 | |
|         maxLines: 3,
 | |
|         minLines: 1,
 | |
|         controller: widget.textController,
 | |
|         decoration: InputDecoration(
 | |
|           contentPadding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 16.0),
 | |
|           suffixIcon: widget.focusNode.hasFocus && widget.textController.text.isNotEmpty
 | |
|               ? IconButton(
 | |
|                   onPressed: () {
 | |
|                     widget.textController.clear();
 | |
|                   },
 | |
|                   icon: Icon(Icons.cancel_rounded, color: context.primaryColor),
 | |
|                   splashRadius: 10.0,
 | |
|                 )
 | |
|               : null,
 | |
|           enabledBorder: OutlineInputBorder(
 | |
|             borderSide: BorderSide(color: context.colorScheme.outline.withValues(alpha: 0.3)),
 | |
|             borderRadius: const BorderRadius.all(Radius.circular(16.0)),
 | |
|           ),
 | |
|           focusedBorder: OutlineInputBorder(
 | |
|             borderSide: BorderSide(color: context.primaryColor.withValues(alpha: 0.3)),
 | |
|             borderRadius: const BorderRadius.all(Radius.circular(16.0)),
 | |
|           ),
 | |
|           hintStyle: context.themeData.inputDecorationTheme.hintStyle?.copyWith(
 | |
|             fontSize: 16.0,
 | |
|             color: context.colorScheme.onSurface.withValues(alpha: 0.6),
 | |
|           ),
 | |
|           focusColor: Colors.grey[300],
 | |
|           fillColor: context.scaffoldBackgroundColor,
 | |
|           filled: widget.focusNode.hasFocus,
 | |
|           hintText: 'add_a_description'.t(),
 | |
|         ),
 | |
|       ),
 | |
|     );
 | |
|   }
 | |
| }
 |