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 createState() => _DriftCreateAlbumPageState(); } class _DriftCreateAlbumPageState extends ConsumerState { TextEditingController albumTitleController = TextEditingController(); TextEditingController albumDescriptionController = TextEditingController(); FocusNode albumTitleTextFieldFocusNode = FocusNode(); FocusNode albumDescriptionTextFieldFocusNode = FocusNode(); bool isAlbumTitleTextFieldFocus = false; Set 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(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 onSelectPhotos() async { final assets = await context.pushRoute>( DriftAssetSelectionTimelineRoute( lockedSelectionAssets: selectedAssets, ), ); if (assets == null || assets.isEmpty) { return; } setState(() { selectedAssets = selectedAssets.union(assets); }); } Future 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 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(), ), ), ); } }