mirror of
https://github.com/immich-app/immich.git
synced 2025-07-31 15:08:44 -04:00
feat: new create album page (#19731)
* feat: new create album page * finished create album flow * refactor into stateful widgets * refactor * focus fix * lint * default sort * pr feedback
This commit is contained in:
parent
1f50a0075e
commit
68db17028b
@ -3,7 +3,7 @@
|
|||||||
archiveVersion = 1;
|
archiveVersion = 1;
|
||||||
classes = {
|
classes = {
|
||||||
};
|
};
|
||||||
objectVersion = 54;
|
objectVersion = 77;
|
||||||
objects = {
|
objects = {
|
||||||
|
|
||||||
/* Begin PBXBuildFile section */
|
/* Begin PBXBuildFile section */
|
||||||
@ -117,8 +117,6 @@
|
|||||||
/* Begin PBXFileSystemSynchronizedRootGroup section */
|
/* Begin PBXFileSystemSynchronizedRootGroup section */
|
||||||
B2CF7F8C2DDE4EBB00744BF6 /* Sync */ = {
|
B2CF7F8C2DDE4EBB00744BF6 /* Sync */ = {
|
||||||
isa = PBXFileSystemSynchronizedRootGroup;
|
isa = PBXFileSystemSynchronizedRootGroup;
|
||||||
exceptions = (
|
|
||||||
);
|
|
||||||
path = Sync;
|
path = Sync;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
@ -473,10 +471,14 @@
|
|||||||
inputFileListPaths = (
|
inputFileListPaths = (
|
||||||
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist",
|
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist",
|
||||||
);
|
);
|
||||||
|
inputPaths = (
|
||||||
|
);
|
||||||
name = "[CP] Copy Pods Resources";
|
name = "[CP] Copy Pods Resources";
|
||||||
outputFileListPaths = (
|
outputFileListPaths = (
|
||||||
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist",
|
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist",
|
||||||
);
|
);
|
||||||
|
outputPaths = (
|
||||||
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
shellPath = /bin/sh;
|
shellPath = /bin/sh;
|
||||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
|
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
|
||||||
@ -505,10 +507,14 @@
|
|||||||
inputFileListPaths = (
|
inputFileListPaths = (
|
||||||
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
|
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
|
||||||
);
|
);
|
||||||
|
inputPaths = (
|
||||||
|
);
|
||||||
name = "[CP] Embed Pods Frameworks";
|
name = "[CP] Embed Pods Frameworks";
|
||||||
outputFileListPaths = (
|
outputFileListPaths = (
|
||||||
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
|
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
|
||||||
);
|
);
|
||||||
|
outputPaths = (
|
||||||
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
shellPath = /bin/sh;
|
shellPath = /bin/sh;
|
||||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
|
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
|
||||||
|
@ -1,12 +1,14 @@
|
|||||||
import 'package:immich_mobile/domain/models/album/album.model.dart';
|
import 'package:immich_mobile/domain/models/album/album.model.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/remote_album.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/remote_album.repository.dart';
|
||||||
import 'package:immich_mobile/models/albums/album_search.model.dart';
|
import 'package:immich_mobile/models/albums/album_search.model.dart';
|
||||||
|
import 'package:immich_mobile/repositories/drift_album_api_repository.dart';
|
||||||
import 'package:immich_mobile/utils/remote_album.utils.dart';
|
import 'package:immich_mobile/utils/remote_album.utils.dart';
|
||||||
|
|
||||||
class RemoteAlbumService {
|
class RemoteAlbumService {
|
||||||
final DriftRemoteAlbumRepository _repository;
|
final DriftRemoteAlbumRepository _repository;
|
||||||
|
final DriftAlbumApiRepository _albumApiRepository;
|
||||||
|
|
||||||
const RemoteAlbumService(this._repository);
|
const RemoteAlbumService(this._repository, this._albumApiRepository);
|
||||||
|
|
||||||
Future<List<RemoteAlbum>> getAll() {
|
Future<List<RemoteAlbum>> getAll() {
|
||||||
return _repository.getAll();
|
return _repository.getAll();
|
||||||
@ -57,4 +59,20 @@ class RemoteAlbumService {
|
|||||||
|
|
||||||
return filtered;
|
return filtered;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<RemoteAlbum> createAlbum({
|
||||||
|
required String title,
|
||||||
|
required List<String> assetIds,
|
||||||
|
String? description,
|
||||||
|
}) async {
|
||||||
|
final album = await _albumApiRepository.createDriftAlbum(
|
||||||
|
title,
|
||||||
|
description: description,
|
||||||
|
assetIds: assetIds,
|
||||||
|
);
|
||||||
|
|
||||||
|
await _repository.create(album, assetIds);
|
||||||
|
|
||||||
|
return album;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,16 +1,17 @@
|
|||||||
import 'package:drift/drift.dart';
|
import 'package:drift/drift.dart';
|
||||||
import 'package:immich_mobile/domain/models/album/album.model.dart';
|
import 'package:immich_mobile/domain/models/album/album.model.dart';
|
||||||
import 'package:immich_mobile/infrastructure/entities/remote_album.entity.drift.dart';
|
import 'package:immich_mobile/infrastructure/entities/remote_album.entity.drift.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/entities/remote_album_asset.entity.drift.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
||||||
|
|
||||||
enum SortRemoteAlbumsBy { id }
|
enum SortRemoteAlbumsBy { id, updatedAt }
|
||||||
|
|
||||||
class DriftRemoteAlbumRepository extends DriftDatabaseRepository {
|
class DriftRemoteAlbumRepository extends DriftDatabaseRepository {
|
||||||
final Drift _db;
|
final Drift _db;
|
||||||
const DriftRemoteAlbumRepository(this._db) : super(_db);
|
const DriftRemoteAlbumRepository(this._db) : super(_db);
|
||||||
|
|
||||||
Future<List<RemoteAlbum>> getAll({
|
Future<List<RemoteAlbum>> getAll({
|
||||||
Set<SortRemoteAlbumsBy> sortBy = const {},
|
Set<SortRemoteAlbumsBy> sortBy = const {SortRemoteAlbumsBy.updatedAt},
|
||||||
}) {
|
}) {
|
||||||
final assetCount = _db.remoteAlbumAssetEntity.assetId.count();
|
final assetCount = _db.remoteAlbumAssetEntity.assetId.count();
|
||||||
|
|
||||||
@ -43,6 +44,8 @@ class DriftRemoteAlbumRepository extends DriftDatabaseRepository {
|
|||||||
orderings.add(
|
orderings.add(
|
||||||
switch (sort) {
|
switch (sort) {
|
||||||
SortRemoteAlbumsBy.id => OrderingTerm.asc(_db.remoteAlbumEntity.id),
|
SortRemoteAlbumsBy.id => OrderingTerm.asc(_db.remoteAlbumEntity.id),
|
||||||
|
SortRemoteAlbumsBy.updatedAt =>
|
||||||
|
OrderingTerm.desc(_db.remoteAlbumEntity.updatedAt),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -58,6 +61,43 @@ class DriftRemoteAlbumRepository extends DriftDatabaseRepository {
|
|||||||
)
|
)
|
||||||
.get();
|
.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> create(
|
||||||
|
RemoteAlbum album,
|
||||||
|
List<String> assetIds,
|
||||||
|
) async {
|
||||||
|
await _db.transaction(() async {
|
||||||
|
final entity = RemoteAlbumEntityCompanion(
|
||||||
|
id: Value(album.id),
|
||||||
|
name: Value(album.name),
|
||||||
|
ownerId: Value(album.ownerId),
|
||||||
|
createdAt: Value(album.createdAt),
|
||||||
|
updatedAt: Value(album.updatedAt),
|
||||||
|
description: Value(album.description),
|
||||||
|
thumbnailAssetId: Value(album.thumbnailAssetId),
|
||||||
|
isActivityEnabled: Value(album.isActivityEnabled),
|
||||||
|
order: Value(album.order),
|
||||||
|
);
|
||||||
|
|
||||||
|
await _db.remoteAlbumEntity.insertOne(entity);
|
||||||
|
|
||||||
|
if (assetIds.isNotEmpty) {
|
||||||
|
final albumAssets = assetIds.map(
|
||||||
|
(assetId) => RemoteAlbumAssetEntityCompanion(
|
||||||
|
albumId: Value(album.id),
|
||||||
|
assetId: Value(assetId),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await _db.batch((batch) {
|
||||||
|
batch.insertAll(
|
||||||
|
_db.remoteAlbumAssetEntity,
|
||||||
|
albumAssets,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension on RemoteAlbumEntityData {
|
extension on RemoteAlbumEntityData {
|
||||||
|
@ -97,7 +97,20 @@ class _DriftAlbumsPageState extends ConsumerState<DriftAlbumsPage> {
|
|||||||
onRefresh: onRefresh,
|
onRefresh: onRefresh,
|
||||||
child: CustomScrollView(
|
child: CustomScrollView(
|
||||||
slivers: [
|
slivers: [
|
||||||
const ImmichSliverAppBar(),
|
ImmichSliverAppBar(
|
||||||
|
actions: [
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(
|
||||||
|
Icons.add_rounded,
|
||||||
|
size: 28,
|
||||||
|
),
|
||||||
|
onPressed: () => context.pushRoute(
|
||||||
|
const DriftCreateAlbumRoute(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
showUploadButton: false,
|
||||||
|
),
|
||||||
_SearchBar(
|
_SearchBar(
|
||||||
searchController: searchController,
|
searchController: searchController,
|
||||||
searchFocusNode: searchFocusNode,
|
searchFocusNode: searchFocusNode,
|
||||||
|
500
mobile/lib/presentation/pages/drift_create_album.page.dart
Normal file
500
mobile/lib/presentation/pages/drift_create_album.page.dart
Normal file
@ -0,0 +1,500 @@
|
|||||||
|
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/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(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) {
|
||||||
|
context.replaceRoute(
|
||||||
|
RemoteTimelineRoute(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(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -7,6 +7,7 @@ import 'package:immich_mobile/infrastructure/repositories/local_album.repository
|
|||||||
import 'package:immich_mobile/infrastructure/repositories/remote_album.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/remote_album.repository.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/remote_album.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/remote_album.provider.dart';
|
||||||
|
import 'package:immich_mobile/repositories/drift_album_api_repository.dart';
|
||||||
|
|
||||||
final localAlbumRepository = Provider<DriftLocalAlbumRepository>(
|
final localAlbumRepository = Provider<DriftLocalAlbumRepository>(
|
||||||
(ref) => DriftLocalAlbumRepository(ref.watch(driftProvider)),
|
(ref) => DriftLocalAlbumRepository(ref.watch(driftProvider)),
|
||||||
@ -30,7 +31,10 @@ final remoteAlbumRepository = Provider<DriftRemoteAlbumRepository>(
|
|||||||
);
|
);
|
||||||
|
|
||||||
final remoteAlbumServiceProvider = Provider<RemoteAlbumService>(
|
final remoteAlbumServiceProvider = Provider<RemoteAlbumService>(
|
||||||
(ref) => RemoteAlbumService(ref.watch(remoteAlbumRepository)),
|
(ref) => RemoteAlbumService(
|
||||||
|
ref.watch(remoteAlbumRepository),
|
||||||
|
ref.watch(driftAlbumApiRepositoryProvider),
|
||||||
|
),
|
||||||
dependencies: [remoteAlbumRepository],
|
dependencies: [remoteAlbumRepository],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -118,4 +118,31 @@ class RemoteAlbumNotifier extends Notifier<RemoteAlbumState> {
|
|||||||
.sortAlbums(state.filteredAlbums, sortMode, isReverse: isReverse);
|
.sortAlbums(state.filteredAlbums, sortMode, isReverse: isReverse);
|
||||||
state = state.copyWith(filteredAlbums: sortedAlbums);
|
state = state.copyWith(filteredAlbums: sortedAlbums);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<RemoteAlbum?> createAlbum({
|
||||||
|
required String title,
|
||||||
|
String? description,
|
||||||
|
List<String> assetIds = const [],
|
||||||
|
}) async {
|
||||||
|
state = state.copyWith(isLoading: true, error: null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
final album = await _remoteAlbumService.createAlbum(
|
||||||
|
title: title,
|
||||||
|
description: description,
|
||||||
|
assetIds: assetIds,
|
||||||
|
);
|
||||||
|
|
||||||
|
state = state.copyWith(
|
||||||
|
albums: [...state.albums, album],
|
||||||
|
filteredAlbums: [...state.filteredAlbums, album],
|
||||||
|
);
|
||||||
|
|
||||||
|
state = state.copyWith(isLoading: false);
|
||||||
|
return album;
|
||||||
|
} catch (e) {
|
||||||
|
state = state.copyWith(isLoading: false, error: e.toString());
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/constants/enums.dart';
|
import 'package:immich_mobile/constants/enums.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/album/album.model.dart'
|
||||||
|
show AlbumAssetOrder, RemoteAlbum;
|
||||||
import 'package:immich_mobile/entities/album.entity.dart';
|
import 'package:immich_mobile/entities/album.entity.dart';
|
||||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||||
import 'package:immich_mobile/infrastructure/entities/user.entity.dart'
|
import 'package:immich_mobile/infrastructure/entities/user.entity.dart'
|
||||||
@ -50,6 +52,25 @@ class AlbumApiRepository extends ApiRepository {
|
|||||||
return _toAlbum(responseDto);
|
return _toAlbum(responseDto);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Change name after removing old method
|
||||||
|
Future<RemoteAlbum> createDriftAlbum(
|
||||||
|
String name, {
|
||||||
|
required Iterable<String> assetIds,
|
||||||
|
String? description,
|
||||||
|
}) async {
|
||||||
|
final responseDto = await checkNull(
|
||||||
|
_api.createAlbum(
|
||||||
|
CreateAlbumDto(
|
||||||
|
albumName: name,
|
||||||
|
description: description,
|
||||||
|
assetIds: assetIds.toList(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
return _toRemoteAlbum(responseDto);
|
||||||
|
}
|
||||||
|
|
||||||
Future<Album> update(
|
Future<Album> update(
|
||||||
String albumId, {
|
String albumId, {
|
||||||
String? name,
|
String? name,
|
||||||
@ -170,4 +191,22 @@ class AlbumApiRepository extends ApiRepository {
|
|||||||
|
|
||||||
return album;
|
return album;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static RemoteAlbum _toRemoteAlbum(AlbumResponseDto dto) {
|
||||||
|
return RemoteAlbum(
|
||||||
|
id: dto.id,
|
||||||
|
name: dto.albumName,
|
||||||
|
ownerId: dto.owner.id,
|
||||||
|
description: dto.description,
|
||||||
|
createdAt: dto.createdAt,
|
||||||
|
updatedAt: dto.updatedAt,
|
||||||
|
thumbnailAssetId: dto.albumThumbnailAssetId,
|
||||||
|
isActivityEnabled: dto.isActivityEnabled,
|
||||||
|
order: dto.order == AssetOrder.asc
|
||||||
|
? AlbumAssetOrder.asc
|
||||||
|
: AlbumAssetOrder.desc,
|
||||||
|
assetCount: dto.assetCount,
|
||||||
|
ownerName: dto.owner.name,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
53
mobile/lib/repositories/drift_album_api_repository.dart
Normal file
53
mobile/lib/repositories/drift_album_api_repository.dart
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/album/album.model.dart';
|
||||||
|
import 'package:immich_mobile/providers/api.provider.dart';
|
||||||
|
import 'package:immich_mobile/repositories/api.repository.dart';
|
||||||
|
// ignore: import_rule_openapi
|
||||||
|
import 'package:openapi/api.dart';
|
||||||
|
|
||||||
|
final driftAlbumApiRepositoryProvider = Provider(
|
||||||
|
(ref) => DriftAlbumApiRepository(ref.watch(apiServiceProvider).albumsApi),
|
||||||
|
);
|
||||||
|
|
||||||
|
class DriftAlbumApiRepository extends ApiRepository {
|
||||||
|
final AlbumsApi _api;
|
||||||
|
|
||||||
|
DriftAlbumApiRepository(this._api);
|
||||||
|
|
||||||
|
Future<RemoteAlbum> createDriftAlbum(
|
||||||
|
String name, {
|
||||||
|
required Iterable<String> assetIds,
|
||||||
|
String? description,
|
||||||
|
}) async {
|
||||||
|
final responseDto = await checkNull(
|
||||||
|
_api.createAlbum(
|
||||||
|
CreateAlbumDto(
|
||||||
|
albumName: name,
|
||||||
|
description: description,
|
||||||
|
assetIds: assetIds.toList(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
return responseDto.toRemoteAlbum();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension on AlbumResponseDto {
|
||||||
|
RemoteAlbum toRemoteAlbum() {
|
||||||
|
return RemoteAlbum(
|
||||||
|
id: id,
|
||||||
|
name: albumName,
|
||||||
|
ownerId: owner.id,
|
||||||
|
description: description,
|
||||||
|
createdAt: createdAt,
|
||||||
|
updatedAt: updatedAt,
|
||||||
|
thumbnailAssetId: albumThumbnailAssetId,
|
||||||
|
isActivityEnabled: isActivityEnabled,
|
||||||
|
order:
|
||||||
|
order == AssetOrder.asc ? AlbumAssetOrder.asc : AlbumAssetOrder.desc,
|
||||||
|
assetCount: assetCount,
|
||||||
|
ownerName: owner.name,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -85,6 +85,7 @@ import 'package:immich_mobile/presentation/pages/dev/remote_timeline.page.dart';
|
|||||||
import 'package:immich_mobile/presentation/pages/drift_album.page.dart';
|
import 'package:immich_mobile/presentation/pages/drift_album.page.dart';
|
||||||
import 'package:immich_mobile/presentation/pages/drift_library.page.dart';
|
import 'package:immich_mobile/presentation/pages/drift_library.page.dart';
|
||||||
import 'package:immich_mobile/presentation/pages/drift_asset_selection_timeline.page.dart';
|
import 'package:immich_mobile/presentation/pages/drift_asset_selection_timeline.page.dart';
|
||||||
|
import 'package:immich_mobile/presentation/pages/drift_create_album.page.dart';
|
||||||
import 'package:immich_mobile/presentation/pages/drift_memory.page.dart';
|
import 'package:immich_mobile/presentation/pages/drift_memory.page.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.page.dart';
|
import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.page.dart';
|
||||||
import 'package:immich_mobile/providers/api.provider.dart';
|
import 'package:immich_mobile/providers/api.provider.dart';
|
||||||
@ -448,6 +449,11 @@ class AppRouter extends RootStackRouter {
|
|||||||
page: DriftLocalAlbumsRoute.page,
|
page: DriftLocalAlbumsRoute.page,
|
||||||
guards: [_authGuard, _duplicateGuard],
|
guards: [_authGuard, _duplicateGuard],
|
||||||
),
|
),
|
||||||
|
AutoRoute(
|
||||||
|
page: DriftCreateAlbumRoute.page,
|
||||||
|
guards: [_authGuard, _duplicateGuard],
|
||||||
|
),
|
||||||
|
|
||||||
// required to handle all deeplinks in deep_link.service.dart
|
// required to handle all deeplinks in deep_link.service.dart
|
||||||
// auto_route_library#1722
|
// auto_route_library#1722
|
||||||
RedirectRoute(path: '*', redirectTo: '/'),
|
RedirectRoute(path: '*', redirectTo: '/'),
|
||||||
|
@ -683,6 +683,22 @@ class DriftAssetSelectionTimelineRouteArgs {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// generated route for
|
||||||
|
/// [DriftCreateAlbumPage]
|
||||||
|
class DriftCreateAlbumRoute extends PageRouteInfo<void> {
|
||||||
|
const DriftCreateAlbumRoute({List<PageRouteInfo>? children})
|
||||||
|
: super(DriftCreateAlbumRoute.name, initialChildren: children);
|
||||||
|
|
||||||
|
static const String name = 'DriftCreateAlbumRoute';
|
||||||
|
|
||||||
|
static PageInfo page = PageInfo(
|
||||||
|
name,
|
||||||
|
builder: (data) {
|
||||||
|
return const DriftCreateAlbumPage();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/// generated route for
|
/// generated route for
|
||||||
/// [DriftFavoritePage]
|
/// [DriftFavoritePage]
|
||||||
class DriftFavoriteRoute extends PageRouteInfo<void> {
|
class DriftFavoriteRoute extends PageRouteInfo<void> {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user