From 06775daacb318e5ec6c38b2159308dfea821eda1 Mon Sep 17 00:00:00 2001 From: Pratik-canopas Date: Thu, 26 Dec 2024 16:36:12 +0530 Subject: [PATCH 01/24] Set up album services --- app/lib/ui/flow/albums/albums_screen.dart | 22 + .../ui/flow/albums/albums_view_notifier.dart | 1 + .../dropbox/dropbox_content_endpoints.dart | 44 +- .../google_drive/google_drive_endpoint.dart | 48 ++- data/lib/domain/config.dart | 8 + data/lib/models/album/album.dart | 22 + data/lib/models/album/album.freezed.dart | 257 +++++++++++ data/lib/models/album/album.g.dart | 32 ++ .../media_process_repository.dart | 6 - data/lib/services/dropbox_services.dart | 120 ++++++ data/lib/services/google_drive_service.dart | 408 +++++++++++++++--- data/lib/services/local_media_service.dart | 87 ++++ 12 files changed, 972 insertions(+), 83 deletions(-) create mode 100644 app/lib/ui/flow/albums/albums_screen.dart create mode 100644 app/lib/ui/flow/albums/albums_view_notifier.dart create mode 100644 data/lib/models/album/album.dart create mode 100644 data/lib/models/album/album.freezed.dart create mode 100644 data/lib/models/album/album.g.dart diff --git a/app/lib/ui/flow/albums/albums_screen.dart b/app/lib/ui/flow/albums/albums_screen.dart new file mode 100644 index 00000000..5f3aa312 --- /dev/null +++ b/app/lib/ui/flow/albums/albums_screen.dart @@ -0,0 +1,22 @@ +import 'package:flutter/cupertino.dart'; + +import '../../../components/app_page.dart'; + +class AlbumsScreen extends StatefulWidget { + const AlbumsScreen({super.key}); + + @override + State createState() => _AlbumsScreenState(); +} + +class _AlbumsScreenState extends State { + @override + Widget build(BuildContext context) { + return AppPage( + title: 'Albums', + body: const Center( + child: Text('Albums Screen'), + ), + ); + } +} diff --git a/app/lib/ui/flow/albums/albums_view_notifier.dart b/app/lib/ui/flow/albums/albums_view_notifier.dart new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/app/lib/ui/flow/albums/albums_view_notifier.dart @@ -0,0 +1 @@ + diff --git a/data/lib/apis/dropbox/dropbox_content_endpoints.dart b/data/lib/apis/dropbox/dropbox_content_endpoints.dart index a35a0b63..80c6be27 100644 --- a/data/lib/apis/dropbox/dropbox_content_endpoints.dart +++ b/data/lib/apis/dropbox/dropbox_content_endpoints.dart @@ -25,7 +25,7 @@ class DropboxCreateFolderEndpoint extends Endpoint { class DropboxListFolderEndpoint extends Endpoint { final bool includeDeleted; - final String appPropertyTemplateId; + final String? appPropertyTemplateId; final bool includeHasExplicitSharedMembers; final int limit; final bool includeMountedFolders; @@ -41,7 +41,7 @@ class DropboxListFolderEndpoint extends Endpoint { this.includeNonDownloadableFiles = false, this.recursive = false, required this.folderPath, - required this.appPropertyTemplateId, + this.appPropertyTemplateId, }); @override @@ -58,10 +58,11 @@ class DropboxListFolderEndpoint extends Endpoint { "include_deleted": includeDeleted, "include_has_explicit_shared_members": includeHasExplicitSharedMembers, "limit": limit, - 'include_property_groups': { - ".tag": "filter_some", - "filter_some": [appPropertyTemplateId], - }, + if (appPropertyTemplateId != null) + 'include_property_groups': { + ".tag": "filter_some", + "filter_some": [appPropertyTemplateId], + }, "include_mounted_folders": includeMountedFolders, "include_non_downloadable_files": includeNonDownloadableFiles, "path": folderPath, @@ -92,7 +93,7 @@ class DropboxListFolderContinueEndpoint extends Endpoint { } class DropboxUploadEndpoint extends Endpoint { - final String appPropertyTemplateId; + final String? appPropertyTemplateId; final String filePath; final String? localRefId; final String mode; @@ -104,7 +105,7 @@ class DropboxUploadEndpoint extends Endpoint { final CancelToken? cancellationToken; const DropboxUploadEndpoint({ - required this.appPropertyTemplateId, + this.appPropertyTemplateId, required this.filePath, this.mode = 'add', this.autoRename = true, @@ -133,17 +134,18 @@ class DropboxUploadEndpoint extends Endpoint { 'autorename': autoRename, 'mute': mute, 'strict_conflict': strictConflict, - 'property_groups': [ - { - "fields": [ - { - "name": ProviderConstants.localRefIdKey, - "value": localRefId ?? '', - }, - ], - "template_id": appPropertyTemplateId, - } - ], + if (appPropertyTemplateId != null && localRefId != null) + 'property_groups': [ + { + "fields": [ + { + "name": ProviderConstants.localRefIdKey, + "value": localRefId ?? '', + }, + ], + "template_id": appPropertyTemplateId, + } + ], }), 'Content-Type': content.contentType, 'Content-Length': content.length, @@ -161,13 +163,13 @@ class DropboxUploadEndpoint extends Endpoint { class DropboxDownloadEndpoint extends DownloadEndpoint { final String filePath; - final String storagePath; + final String? storagePath; final void Function(int chunk, int length)? onProgress; final CancelToken? cancellationToken; const DropboxDownloadEndpoint({ required this.filePath, - required this.storagePath, + this.storagePath, this.cancellationToken, this.onProgress, }); diff --git a/data/lib/apis/google_drive/google_drive_endpoint.dart b/data/lib/apis/google_drive/google_drive_endpoint.dart index 6553bc26..ca354f90 100644 --- a/data/lib/apis/google_drive/google_drive_endpoint.dart +++ b/data/lib/apis/google_drive/google_drive_endpoint.dart @@ -84,12 +84,56 @@ class GoogleDriveUploadEndpoint extends Endpoint { void Function(int p1, int p2)? get onSendProgress => onProgress; } +class GoogleDriveContentUpdateEndpoint extends Endpoint { + final AppMediaContent content; + final String id; + final CancelToken? cancellationToken; + final void Function(int chunk, int length)? onProgress; + + const GoogleDriveContentUpdateEndpoint({ + required this.content, + required this.id, + this.cancellationToken, + this.onProgress, + }); + + @override + String get baseUrl => BaseURL.googleDriveUploadV3; + + @override + CancelToken? get cancelToken => cancellationToken; + + @override + HttpMethod get method => HttpMethod.patch; + + @override + Map get headers => { + 'Content-Type': content.contentType, + 'Content-Length': content.length.toString(), + }; + + @override + Object? get data => content.stream; + + @override + String get path => '/files/$id'; + + @override + Map? get queryParameters => { + 'uploadType': 'media', + 'fields': 'id, name, description, mimeType, thumbnailLink, webContentLink, createdTime, modifiedTime, size, imageMediaMetadata, videoMediaMetadata, appProperties', + }; + + @override + void Function(int p1, int p2)? get onSendProgress => onProgress; +} + class GoogleDriveDownloadEndpoint extends DownloadEndpoint { final String id; final void Function(int received, int total)? onProgress; - final String saveLocation; + final String? saveLocation; final CancelToken? cancellationToken; @@ -97,7 +141,7 @@ class GoogleDriveDownloadEndpoint extends DownloadEndpoint { required this.id, this.cancellationToken, this.onProgress, - required this.saveLocation, + this.saveLocation, }); @override diff --git a/data/lib/domain/config.dart b/data/lib/domain/config.dart index f46eb74f..bb91fcc7 100644 --- a/data/lib/domain/config.dart +++ b/data/lib/domain/config.dart @@ -1,7 +1,15 @@ class ProviderConstants { + static const String albumFileName = 'Album.json'; static const String backupFolderName = 'Cloud Gallery Backup'; static const String backupFolderPath = '/Cloud Gallery Backup'; static const String localRefIdKey = 'local_ref_id'; static const String dropboxAppTemplateName = 'Cloud Gallery Local File Information'; } + +class LocalDatabaseConstants { + static const String databaseName = 'cloud-gallery.db'; + static const String uploadQueueTable = 'UploadQueue'; + static const String downloadQueueTable = 'DownloadQueue'; + static const String albumsTable = 'Albums'; +} diff --git a/data/lib/models/album/album.dart b/data/lib/models/album/album.dart new file mode 100644 index 00000000..a570330c --- /dev/null +++ b/data/lib/models/album/album.dart @@ -0,0 +1,22 @@ +// ignore_for_file: non_constant_identifier_names + +import 'package:freezed_annotation/freezed_annotation.dart'; +import '../../domain/json_converters/date_time_json_converter.dart'; +import '../media/media.dart'; + +part 'album.freezed.dart'; + +part 'album.g.dart'; + +@freezed +class Album with _$Album { + const factory Album({ + required String name, + required String id, + required List medias, + required AppMediaSource source, + @DateTimeJsonConverter() required DateTime created_at, + }) = _Album; + + factory Album.fromJson(Map json) => _$AlbumFromJson(json); +} diff --git a/data/lib/models/album/album.freezed.dart b/data/lib/models/album/album.freezed.dart new file mode 100644 index 00000000..61d00ebb --- /dev/null +++ b/data/lib/models/album/album.freezed.dart @@ -0,0 +1,257 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'album.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); + +Album _$AlbumFromJson(Map json) { + return _Album.fromJson(json); +} + +/// @nodoc +mixin _$Album { + String get name => throw _privateConstructorUsedError; + String get id => throw _privateConstructorUsedError; + List get medias => throw _privateConstructorUsedError; + AppMediaSource get source => throw _privateConstructorUsedError; + @DateTimeJsonConverter() + DateTime get created_at => throw _privateConstructorUsedError; + + /// Serializes this Album to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of Album + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $AlbumCopyWith get copyWith => throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $AlbumCopyWith<$Res> { + factory $AlbumCopyWith(Album value, $Res Function(Album) then) = + _$AlbumCopyWithImpl<$Res, Album>; + @useResult + $Res call( + {String name, + String id, + List medias, + AppMediaSource source, + @DateTimeJsonConverter() DateTime created_at}); +} + +/// @nodoc +class _$AlbumCopyWithImpl<$Res, $Val extends Album> + implements $AlbumCopyWith<$Res> { + _$AlbumCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of Album + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? name = null, + Object? id = null, + Object? medias = null, + Object? source = null, + Object? created_at = null, + }) { + return _then(_value.copyWith( + name: null == name + ? _value.name + : name // ignore: cast_nullable_to_non_nullable + as String, + id: null == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as String, + medias: null == medias + ? _value.medias + : medias // ignore: cast_nullable_to_non_nullable + as List, + source: null == source + ? _value.source + : source // ignore: cast_nullable_to_non_nullable + as AppMediaSource, + created_at: null == created_at + ? _value.created_at + : created_at // ignore: cast_nullable_to_non_nullable + as DateTime, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$AlbumImplCopyWith<$Res> implements $AlbumCopyWith<$Res> { + factory _$$AlbumImplCopyWith( + _$AlbumImpl value, $Res Function(_$AlbumImpl) then) = + __$$AlbumImplCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {String name, + String id, + List medias, + AppMediaSource source, + @DateTimeJsonConverter() DateTime created_at}); +} + +/// @nodoc +class __$$AlbumImplCopyWithImpl<$Res> + extends _$AlbumCopyWithImpl<$Res, _$AlbumImpl> + implements _$$AlbumImplCopyWith<$Res> { + __$$AlbumImplCopyWithImpl( + _$AlbumImpl _value, $Res Function(_$AlbumImpl) _then) + : super(_value, _then); + + /// Create a copy of Album + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? name = null, + Object? id = null, + Object? medias = null, + Object? source = null, + Object? created_at = null, + }) { + return _then(_$AlbumImpl( + name: null == name + ? _value.name + : name // ignore: cast_nullable_to_non_nullable + as String, + id: null == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as String, + medias: null == medias + ? _value._medias + : medias // ignore: cast_nullable_to_non_nullable + as List, + source: null == source + ? _value.source + : source // ignore: cast_nullable_to_non_nullable + as AppMediaSource, + created_at: null == created_at + ? _value.created_at + : created_at // ignore: cast_nullable_to_non_nullable + as DateTime, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$AlbumImpl implements _Album { + const _$AlbumImpl( + {required this.name, + required this.id, + required final List medias, + required this.source, + @DateTimeJsonConverter() required this.created_at}) + : _medias = medias; + + factory _$AlbumImpl.fromJson(Map json) => + _$$AlbumImplFromJson(json); + + @override + final String name; + @override + final String id; + final List _medias; + @override + List get medias { + if (_medias is EqualUnmodifiableListView) return _medias; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_medias); + } + + @override + final AppMediaSource source; + @override + @DateTimeJsonConverter() + final DateTime created_at; + + @override + String toString() { + return 'Album(name: $name, id: $id, medias: $medias, source: $source, created_at: $created_at)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$AlbumImpl && + (identical(other.name, name) || other.name == name) && + (identical(other.id, id) || other.id == id) && + const DeepCollectionEquality().equals(other._medias, _medias) && + (identical(other.source, source) || other.source == source) && + (identical(other.created_at, created_at) || + other.created_at == created_at)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash(runtimeType, name, id, + const DeepCollectionEquality().hash(_medias), source, created_at); + + /// Create a copy of Album + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$AlbumImplCopyWith<_$AlbumImpl> get copyWith => + __$$AlbumImplCopyWithImpl<_$AlbumImpl>(this, _$identity); + + @override + Map toJson() { + return _$$AlbumImplToJson( + this, + ); + } +} + +abstract class _Album implements Album { + const factory _Album( + {required final String name, + required final String id, + required final List medias, + required final AppMediaSource source, + @DateTimeJsonConverter() required final DateTime created_at}) = + _$AlbumImpl; + + factory _Album.fromJson(Map json) = _$AlbumImpl.fromJson; + + @override + String get name; + @override + String get id; + @override + List get medias; + @override + AppMediaSource get source; + @override + @DateTimeJsonConverter() + DateTime get created_at; + + /// Create a copy of Album + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$AlbumImplCopyWith<_$AlbumImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/data/lib/models/album/album.g.dart b/data/lib/models/album/album.g.dart new file mode 100644 index 00000000..770b8710 --- /dev/null +++ b/data/lib/models/album/album.g.dart @@ -0,0 +1,32 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'album.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_$AlbumImpl _$$AlbumImplFromJson(Map json) => _$AlbumImpl( + name: json['name'] as String, + id: json['id'] as String, + medias: + (json['medias'] as List).map((e) => e as String).toList(), + source: $enumDecode(_$AppMediaSourceEnumMap, json['source']), + created_at: + const DateTimeJsonConverter().fromJson(json['created_at'] as String), + ); + +Map _$$AlbumImplToJson(_$AlbumImpl instance) => + { + 'name': instance.name, + 'id': instance.id, + 'medias': instance.medias, + 'source': _$AppMediaSourceEnumMap[instance.source]!, + 'created_at': const DateTimeJsonConverter().toJson(instance.created_at), + }; + +const _$AppMediaSourceEnumMap = { + AppMediaSource.local: 'local', + AppMediaSource.googleDrive: 'google_drive', + AppMediaSource.dropbox: 'dropbox', +}; diff --git a/data/lib/repositories/media_process_repository.dart b/data/lib/repositories/media_process_repository.dart index feebae90..b975ab9c 100644 --- a/data/lib/repositories/media_process_repository.dart +++ b/data/lib/repositories/media_process_repository.dart @@ -39,12 +39,6 @@ final mediaProcessRepoProvider = Provider((ref) { return repo; }); -class LocalDatabaseConstants { - static const String databaseName = 'cloud-gallery.db'; - static const String uploadQueueTable = 'UploadQueue'; - static const String downloadQueueTable = 'DownloadQueue'; -} - class ProcessNotificationConstants { static const String uploadProcessGroupIdentifier = 'cloud_gallery_upload_process'; diff --git a/data/lib/services/dropbox_services.dart b/data/lib/services/dropbox_services.dart index b2f454f6..b5fbfad4 100644 --- a/data/lib/services/dropbox_services.dart +++ b/data/lib/services/dropbox_services.dart @@ -1,9 +1,11 @@ +import 'dart:convert'; import 'dart:io'; import 'package:collection/collection.dart'; import '../apis/dropbox/dropbox_content_endpoints.dart'; import '../apis/network/client.dart'; import '../domain/config.dart'; import '../errors/app_error.dart'; +import '../models/album/album.dart'; import '../models/dropbox/account/dropbox_account.dart'; import '../models/media/media.dart'; import '../models/media_content/media_content.dart'; @@ -47,6 +49,8 @@ class DropboxService extends CloudProviderService { } } + //MEDIA ---------------------------------------------------------------------- + Future setFileIdAppPropertyTemplate() async { // Get all the app property templates final res = await _dropboxAuthenticatedDio @@ -357,4 +361,120 @@ class DropboxService extends CloudProviderService { message: res.statusMessage, ); } + + Future> getAlbums() async { + try { + final res = await _dropboxAuthenticatedDio.req( + DropboxDownloadEndpoint( + filePath: "/${ProviderConstants.backupFolderName}/Albums.json", + ), + ); + if (res.statusCode != 200 || res.data is! ResponseBody) { + throw SomethingWentWrongError( + statusCode: res.statusCode, + message: res.statusMessage, + ); + } + final List bytes = []; + await for (final chunk in (res.data as ResponseBody).stream) { + bytes.addAll(chunk); + } + final json = jsonDecode(utf8.decode(bytes)); + return json is! List + ? [] + : json.map((e) => Album.fromJson(e)).toList(); + } catch (e) { + if (e is DioException && + e.response?.statusCode == 409 && + e.response?.data?['error']?['path']?['.tag'] == 'not_found') { + return []; + } + rethrow; + } + } + + Future createAlbum(Album album) async { + final albums = await getAlbums(); + albums.add(album); + albums.sort((a, b) => b.created_at.compareTo(a.created_at)); + + final res = await _dropboxAuthenticatedDio.req( + DropboxUploadEndpoint( + mode: 'overwrite', + autoRename: false, + content: AppMediaContent( + stream: Stream.value(utf8.encode(jsonEncode(album))), + length: utf8.encode(jsonEncode(album)).length, + contentType: 'application/json', + ), + filePath: "/${ProviderConstants.backupFolderName}/Albums.json", + ), + ); + + if (res.statusCode == 200) return; + + throw SomethingWentWrongError( + statusCode: res.statusCode, + message: res.statusMessage, + ); + } + + Future deleteAlbum(Album album) async { + final albums = await getAlbums(); + albums.removeWhere((a) => a.id == album.id); + + final res = await _dropboxAuthenticatedDio.req( + DropboxUploadEndpoint( + mode: 'overwrite', + autoRename: false, + content: AppMediaContent( + stream: Stream.value(utf8.encode(jsonEncode(albums))), + length: utf8.encode(jsonEncode(albums)).length, + contentType: 'application/json', + ), + filePath: "/${ProviderConstants.backupFolderName}/Albums.json", + ), + ); + + if (res.statusCode == 200) return; + + throw SomethingWentWrongError( + statusCode: res.statusCode, + message: res.statusMessage, + ); + } + + Future updateAlbum(Album album) async { + final albums = await getAlbums(); + final index = albums.indexWhere((a) => a.id == album.id); + if (index == -1) { + throw SomethingWentWrongError( + statusCode: 404, + message: 'Album not found', + ); + } + + albums[index] = album; + albums.sort((a, b) => b.created_at.compareTo(a.created_at)); + + final res = await _dropboxAuthenticatedDio.req( + DropboxUploadEndpoint( + mode: 'overwrite', + autoRename: false, + content: AppMediaContent( + stream: Stream.value(utf8.encode(jsonEncode(albums))), + length: utf8.encode(jsonEncode(albums)).length, + contentType: 'application/json', + ), + filePath: "/${ProviderConstants.backupFolderName}/Albums.json", + ), + ); + + if (res.statusCode == 200) return; + + throw SomethingWentWrongError( + statusCode: res.statusCode, + message: res.statusMessage, + ); + } } diff --git a/data/lib/services/google_drive_service.dart b/data/lib/services/google_drive_service.dart index 521e74ad..896166e0 100644 --- a/data/lib/services/google_drive_service.dart +++ b/data/lib/services/google_drive_service.dart @@ -1,8 +1,11 @@ import 'dart:async'; +import 'dart:convert'; import 'dart:io'; +import 'dart:typed_data'; import '../apis/google_drive/google_drive_endpoint.dart'; import '../apis/network/client.dart'; import '../domain/config.dart'; +import '../models/album/album.dart'; import '../models/media/media.dart'; import '../models/media_content/media_content.dart'; import 'package:dio/dio.dart'; @@ -22,6 +25,62 @@ class GoogleDriveService extends CloudProviderService { GoogleDriveService(this._client); + //FOLDERS -------------------------------------------------------------------- + + Future getBackUpFolderId() async { + final res = await _client.req( + GoogleDriveListEndpoint( + q: "name='${ProviderConstants.backupFolderName}' and trashed=false and mimeType='application/vnd.google-apps.folder'", + pageSize: 1, + ), + ); + + if (res.statusCode == 200) { + final body = drive.FileList.fromJson(res.data); + if (body.files?.isNotEmpty ?? false) { + return body.files?.first.id; + } else { + final createRes = await _client.req( + GoogleDriveCreateFolderEndpoint( + name: ProviderConstants.backupFolderName, + ), + ); + + if (createRes.statusCode == 200) { + return drive.File.fromJson(createRes.data).id; + } + + throw SomethingWentWrongError( + statusCode: createRes.statusCode, + message: createRes.statusMessage, + ); + } + } + + throw SomethingWentWrongError( + statusCode: res.statusCode, + message: res.statusMessage, + ); + } + + @override + Future createFolder(String folderName) async { + final res = await _client.req( + GoogleDriveCreateFolderEndpoint(name: folderName), + ); + + if (res.statusCode == 200) { + return drive.File.fromJson(res.data).id; + } + + throw SomethingWentWrongError( + statusCode: res.statusCode, + message: res.statusMessage, + ); + } + + //MEDIA ---------------------------------------------------------------------- + @override Future> getAllMedias({ required String folder, @@ -33,7 +92,7 @@ class GoogleDriveService extends CloudProviderService { while (hasMore) { final res = await _client.req( GoogleDriveListEndpoint( - q: "'$folder' in parents and trashed=false", + q: "'$folder' in parents and trashed=false and name!='${ProviderConstants.albumFileName}", pageSize: 1000, pageToken: pageToken, ), @@ -70,7 +129,7 @@ class GoogleDriveService extends CloudProviderService { }) async { final res = await _client.req( GoogleDriveListEndpoint( - q: "'$folder' in parents and trashed=false", + q: "'$folder' in parents and trashed=false and name!='${ProviderConstants.albumFileName}'", pageSize: pageSize, pageToken: nextPageToken, ), @@ -110,58 +169,6 @@ class GoogleDriveService extends CloudProviderService { ); } - Future getBackUpFolderId() async { - final res = await _client.req( - GoogleDriveListEndpoint( - q: "name='${ProviderConstants.backupFolderName}' and trashed=false and mimeType='application/vnd.google-apps.folder'", - pageSize: 1, - ), - ); - - if (res.statusCode == 200) { - final body = drive.FileList.fromJson(res.data); - if (body.files?.isNotEmpty ?? false) { - return body.files?.first.id; - } else { - final createRes = await _client.req( - GoogleDriveCreateFolderEndpoint( - name: ProviderConstants.backupFolderName, - ), - ); - - if (createRes.statusCode == 200) { - return drive.File.fromJson(createRes.data).id; - } - - throw SomethingWentWrongError( - statusCode: createRes.statusCode, - message: createRes.statusMessage, - ); - } - } - - throw SomethingWentWrongError( - statusCode: res.statusCode, - message: res.statusMessage, - ); - } - - @override - Future createFolder(String folderName) async { - final res = await _client.req( - GoogleDriveCreateFolderEndpoint(name: folderName), - ); - - if (res.statusCode == 200) { - return drive.File.fromJson(res.data).id; - } - - throw SomethingWentWrongError( - statusCode: res.statusCode, - message: res.statusMessage, - ); - } - @override Future uploadMedia({ required String folderId, @@ -247,4 +254,297 @@ class GoogleDriveService extends CloudProviderService { message: res.statusMessage, ); } + + //ALBUMS --------------------------------------------------------------------- + + Future> getAlbums({required String folderId}) async { + final res = await _client.req( + GoogleDriveListEndpoint( + q: "'$folderId' in parents and trashed=false and name='${ProviderConstants.albumFileName}'", + pageSize: 1, + ), + ); + + if (res.statusCode == 200) { + final body = drive.FileList.fromJson(res.data); + if ((body.files ?? []).isNotEmpty) { + final res = await _client.req( + GoogleDriveDownloadEndpoint(id: body.files!.first.id!), + ); + if (res.statusCode == 200) { + final List bytes = []; + await for (final chunk in (res.data as ResponseBody).stream) { + bytes.addAll(chunk); + } + final json = jsonDecode(utf8.decode(bytes)); + return json is! List + ? [] + : json.map((e) => Album.fromJson(e)).toList(); + } + + throw SomethingWentWrongError( + statusCode: res.statusCode, + message: res.statusMessage, + ); + } + return []; + } + + throw SomethingWentWrongError( + statusCode: res.statusCode, + message: res.statusMessage, + ); + } + + Future createAlbum({ + required String folderId, + required Album newAlbum, + }) async { + // Fetch the album file + final listRes = await _client.req( + GoogleDriveListEndpoint( + q: "'$folderId' in parents and trashed=false and name='${ProviderConstants.albumFileName}'", + pageSize: 1, + ), + ); + + if (listRes.statusCode != 200) { + throw SomethingWentWrongError( + statusCode: listRes.statusCode, + message: listRes.statusMessage, + ); + } + + final body = drive.FileList.fromJson(listRes.data); + + if ((body.files ?? []).isNotEmpty) { + // Download the album file if it exists + final res = await _client.req( + GoogleDriveDownloadEndpoint(id: body.files!.first.id!), + ); + + if (res.statusCode != 200) { + throw SomethingWentWrongError( + statusCode: res.statusCode, + message: res.statusMessage, + ); + } + + // Convert the downloaded album file to a list of albums + final List bytes = []; + await for (final chunk in (res.data as ResponseBody).stream) { + bytes.addAll(chunk); + } + final json = jsonDecode(utf8.decode(bytes)); + final albums = json is! List + ? [] + : json.map((e) => Album.fromJson(e)).toList(); + + // Attach the new album to the list of albums + albums.add(newAlbum); + albums.sort((a, b) => a.created_at.compareTo(b.created_at)); + + // Update the album file with the new list of albums + final updateRes = await _client.req( + GoogleDriveContentUpdateEndpoint( + id: body.files!.first.id!, + content: AppMediaContent( + stream: Stream.value( + Uint8List.fromList(utf8.encode(jsonEncode(albums))), + ), + length: utf8.encode(jsonEncode(albums)).length, + contentType: 'application/json', + ), + ), + ); + + if (updateRes.statusCode == 200) return; + + throw SomethingWentWrongError( + statusCode: updateRes.statusCode, + message: updateRes.statusMessage, + ); + } + + // Create a new album file if it doesn't exist + final res = await _client.req( + GoogleDriveUploadEndpoint( + request: drive.File( + name: ProviderConstants.albumFileName, + mimeType: 'application/json', + parents: [folderId], + ), + content: AppMediaContent( + stream: Stream.value( + Uint8List.fromList(utf8.encode(jsonEncode([newAlbum.toJson()]))), + ), + length: utf8.encode(jsonEncode([newAlbum.toJson()])).length, + contentType: 'application/json', + ), + ), + ); + + if (res.statusCode == 200) return; + + throw SomethingWentWrongError( + statusCode: res.statusCode, + message: res.statusMessage, + ); + } + + Future updateAlbum({ + required String folderId, + required Album album, + }) async { + // Fetch the album file + final listRes = await _client.req( + GoogleDriveListEndpoint( + q: "'$folderId' in parents and trashed=false and name='${ProviderConstants.albumFileName}'", + pageSize: 1, + ), + ); + + if (listRes.statusCode != 200) { + throw SomethingWentWrongError( + statusCode: listRes.statusCode, + message: listRes.statusMessage, + ); + } + + final body = drive.FileList.fromJson(listRes.data); + + if ((body.files ?? []).isNotEmpty) { + // Download the album file if it exists + final res = await _client.req( + GoogleDriveDownloadEndpoint(id: body.files!.first.id!), + ); + + if (res.statusCode != 200) { + throw SomethingWentWrongError( + statusCode: res.statusCode, + message: res.statusMessage, + ); + } + + // Convert the downloaded album file to a list of albums + final List bytes = []; + await for (final chunk in (res.data as ResponseBody).stream) { + bytes.addAll(chunk); + } + final json = jsonDecode(utf8.decode(bytes)); + final albums = json is! List + ? [] + : json.map((e) => Album.fromJson(e)).toList(); + + // Attach the new album to the list of albums + if (albums.where((element) => element.id == album.id).isEmpty) { + throw SomethingWentWrongError( + message: 'Album not found', + ); + } + + albums.removeWhere((element) => element.id == album.id); + albums.add(album); + albums.sort((a, b) => a.created_at.compareTo(b.created_at)); + + // Update the album file with the new list of albums + final updateRes = await _client.req( + GoogleDriveContentUpdateEndpoint( + id: body.files!.first.id!, + content: AppMediaContent( + stream: Stream.value( + Uint8List.fromList(utf8.encode(jsonEncode(albums))), + ), + length: utf8.encode(jsonEncode(albums)).length, + contentType: 'application/json', + ), + ), + ); + + if (updateRes.statusCode == 200) return; + + throw SomethingWentWrongError( + statusCode: updateRes.statusCode, + message: updateRes.statusMessage, + ); + } + + throw SomethingWentWrongError( + message: 'Album file not found', + ); + } + + Future removeAlbum({ + required String folderId, + required String id, + }) async { + // Fetch the album file + final listRes = await _client.req( + GoogleDriveListEndpoint( + q: "'$folderId' in parents and trashed=false and name='${ProviderConstants.albumFileName}'", + pageSize: 1, + ), + ); + + if (listRes.statusCode != 200) { + throw SomethingWentWrongError( + statusCode: listRes.statusCode, + message: listRes.statusMessage, + ); + } + + final body = drive.FileList.fromJson(listRes.data); + + if ((body.files ?? []).isNotEmpty) { + // Download the album file if it exists + final res = await _client.req( + GoogleDriveDownloadEndpoint(id: body.files!.first.id!), + ); + + if (res.statusCode != 200) { + throw SomethingWentWrongError( + statusCode: res.statusCode, + message: res.statusMessage, + ); + } + + // Convert the downloaded album file to a list of albums + final List bytes = []; + await for (final chunk in (res.data as ResponseBody).stream) { + bytes.addAll(chunk); + } + final json = jsonDecode(utf8.decode(bytes)); + final albums = json is! List + ? [] + : json.map((e) => Album.fromJson(e)).toList(); + + // Attach the new album to the list of albums + albums.removeWhere((element) => element.id == id); + + // Update the album file with the new list of albums + final updateRes = await _client.req( + GoogleDriveContentUpdateEndpoint( + id: body.files!.first.id!, + content: AppMediaContent( + stream: Stream.value( + Uint8List.fromList(utf8.encode(jsonEncode(albums))), + ), + length: utf8.encode(jsonEncode(albums)).length, + contentType: 'application/json', + ), + ), + ); + + if (updateRes.statusCode == 200) return; + + throw SomethingWentWrongError( + statusCode: updateRes.statusCode, + message: updateRes.statusMessage, + ); + } + + throw SomethingWentWrongError( + message: 'Album file not found', + ); + } } diff --git a/data/lib/services/local_media_service.dart b/data/lib/services/local_media_service.dart index be92d182..257fc510 100644 --- a/data/lib/services/local_media_service.dart +++ b/data/lib/services/local_media_service.dart @@ -1,5 +1,9 @@ import 'dart:async'; import 'dart:io'; +import 'package:sqflite/sqflite.dart'; +import '../domain/config.dart'; +import '../domain/json_converters/date_time_json_converter.dart'; +import '../models/album/album.dart'; import '../models/media/media.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:photo_manager/photo_manager.dart'; @@ -11,6 +15,8 @@ final localMediaServiceProvider = Provider( class LocalMediaService { const LocalMediaService(); + // MEDIA --------------------------------------------------------------------- + Future isLocalFileExist({ required AppMediaType type, required String id, @@ -90,4 +96,85 @@ class LocalMediaService { } return asset != null ? AppMedia.fromAssetEntity(asset) : null; } + + // ALBUM --------------------------------------------------------------------- + + Future openAlbumDatabase() async { + return await openDatabase( + LocalDatabaseConstants.databaseName, + version: 1, + onCreate: (Database db, int version) async { + await db.execute( + 'CREATE TABLE ${LocalDatabaseConstants.albumsTable} (' + 'id TEXT PRIMARY KEY, ' + 'name TEXT NOT NULL, ' + 'source TEXT NOT NULL, ' + 'created_at TEXT NOT NULL, ' + 'medias TEXT NOT NULL, ' + ')', + ); + }, + ); + } + + Future createAlbum(Album album) async { + final db = await openAlbumDatabase(); + await db.insert( + LocalDatabaseConstants.albumsTable, + { + 'id': album.id, + 'name': album.name, + 'source': album.source.value, + 'created_at': DateTimeJsonConverter().toJson(album.created_at), + 'medias': album.medias.join(','), + }, + ); + await db.close(); + } + + Future updateAlbum(Album album) async { + final db = await openAlbumDatabase(); + await db.update( + LocalDatabaseConstants.albumsTable, + { + 'name': album.name, + 'source': album.source.value, + 'created_at': DateTimeJsonConverter().toJson(album.created_at), + 'medias': album.medias.join(','), + }, + where: 'id = ?', + whereArgs: [album.id], + ); + await db.close(); + } + + Future deleteAlbum(String id) async { + final db = await openAlbumDatabase(); + await db.delete( + LocalDatabaseConstants.albumsTable, + where: 'id = ?', + whereArgs: [id], + ); + await db.close(); + } + + Future> getAllAlbums() async { + final db = await openAlbumDatabase(); + final albums = await db.query(LocalDatabaseConstants.albumsTable); + await db.close(); + return albums + .map( + (album) => Album( + id: album['id'] as String, + name: album['name'] as String, + source: AppMediaSource.values.firstWhere( + (source) => source.value == album['source'], + ), + created_at: + DateTimeJsonConverter().fromJson(album['created_at'] as String), + medias: (album['medias'] as String).split(','), + ), + ) + .toList(); + } } From 865fed64e9582aa3a6132be9af76829e377c5a9d Mon Sep 17 00:00:00 2001 From: Pratik-canopas Date: Tue, 31 Dec 2024 16:08:47 +0530 Subject: [PATCH 02/24] Album support --- app/lib/components/app_page.dart | 2 + .../ui/flow/albums/add/add_album_screen.dart | 1 + .../albums/add/add_album_state_notifier.dart | 44 +++ app/lib/ui/flow/albums/albums_screen.dart | 75 ++++- .../ui/flow/albums/albums_view_notifier.dart | 123 ++++++++ .../albums/albums_view_notifier.freezed.dart | 286 ++++++++++++++++++ app/lib/ui/flow/home/home_screen.dart | 5 - app/lib/ui/flow/main/main_screen.dart | 153 ++++++++++ app/lib/ui/navigation/app_route.dart | 78 ++++- app/lib/ui/navigation/app_route.g.dart | 99 ++++-- data/lib/domain/config.dart | 1 + data/lib/services/dropbox_services.dart | 6 +- data/lib/services/google_drive_service.dart | 4 +- data/lib/services/local_media_service.dart | 12 +- style/lib/callback/on_visible_callback.dart | 28 ++ style/lib/theme/colors.dart | 4 +- 16 files changed, 857 insertions(+), 64 deletions(-) create mode 100644 app/lib/ui/flow/albums/add/add_album_screen.dart create mode 100644 app/lib/ui/flow/albums/add/add_album_state_notifier.dart create mode 100644 app/lib/ui/flow/albums/albums_view_notifier.freezed.dart create mode 100644 app/lib/ui/flow/main/main_screen.dart create mode 100644 style/lib/callback/on_visible_callback.dart diff --git a/app/lib/components/app_page.dart b/app/lib/components/app_page.dart index 6b3985e1..06f51f9f 100644 --- a/app/lib/components/app_page.dart +++ b/app/lib/components/app_page.dart @@ -89,6 +89,7 @@ class AppPage extends StatelessWidget { leading == null ? null : AppBar( + centerTitle: true, backgroundColor: barBackgroundColor, title: titleWidget ?? _title(context), actions: [...?actions, const SizedBox(width: 16)], @@ -150,6 +151,7 @@ class AdaptiveAppBar extends StatelessWidget { : Column( children: [ AppBar( + centerTitle: true, backgroundColor: context.colorScheme.barColor, leading: leading, actions: actions, diff --git a/app/lib/ui/flow/albums/add/add_album_screen.dart b/app/lib/ui/flow/albums/add/add_album_screen.dart new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/app/lib/ui/flow/albums/add/add_album_screen.dart @@ -0,0 +1 @@ + diff --git a/app/lib/ui/flow/albums/add/add_album_state_notifier.dart b/app/lib/ui/flow/albums/add/add_album_state_notifier.dart new file mode 100644 index 00000000..34b227e1 --- /dev/null +++ b/app/lib/ui/flow/albums/add/add_album_state_notifier.dart @@ -0,0 +1,44 @@ +import 'package:data/models/dropbox/account/dropbox_account.dart'; +import 'package:data/models/media/media.dart'; +import 'package:data/services/local_media_service.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:google_sign_in/google_sign_in.dart'; + +class AddAlbumStateNotifier extends StateNotifier { + final GoogleSignInAccount? googleAccount; + final DropboxAccount? dropboxAccount; + + AddAlbumStateNotifier( + super.state, + this.googleAccount, + this.dropboxAccount, + ); + + void onGoogleDriveAccountChange(GoogleSignInAccount? googleAccount) { + state = state.copyWith(googleAccount: googleAccount); + } + + void onDropboxAccountChange(DropboxAccount? dropboxAccount) { + state = state.copyWith(dropboxAccount: dropboxAccount); + } + + void createAlbum(){ + state = state.copyWith(loading: true); + + + } +} + +@freezed +class AddAlbumsState with _$AddAlbumsState { + const factory AddAlbumsState({ + @Default(false) bool loading, + AppMediaSource? mediaSource, + required TextEditingController albumNameController, + GoogleSignInAccount? googleAccount, + DropboxAccount? dropboxAccount, + Object? error, + }) = _AddAlbumsState; +} diff --git a/app/lib/ui/flow/albums/albums_screen.dart b/app/lib/ui/flow/albums/albums_screen.dart index 5f3aa312..e11082d8 100644 --- a/app/lib/ui/flow/albums/albums_screen.dart +++ b/app/lib/ui/flow/albums/albums_screen.dart @@ -1,22 +1,83 @@ import 'package:flutter/cupertino.dart'; - +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:style/animations/fade_in_switcher.dart'; +import 'package:style/buttons/action_button.dart'; +import 'package:style/extensions/context_extensions.dart'; +import 'package:style/indicators/circular_progress_indicator.dart'; import '../../../components/app_page.dart'; +import '../../../components/error_screen.dart'; +import '../../../components/place_holder_screen.dart'; +import 'albums_view_notifier.dart'; -class AlbumsScreen extends StatefulWidget { +class AlbumsScreen extends ConsumerStatefulWidget { const AlbumsScreen({super.key}); @override - State createState() => _AlbumsScreenState(); + ConsumerState createState() => _AlbumsScreenState(); } -class _AlbumsScreenState extends State { +class _AlbumsScreenState extends ConsumerState { + late AlbumStateNotifier _notifier; + + @override + void initState() { + _notifier = ref.read(albumStateNotifierProvider.notifier); + super.initState(); + } + @override Widget build(BuildContext context) { return AppPage( title: 'Albums', - body: const Center( - child: Text('Albums Screen'), - ), + actions: [ + ActionButton( + onPressed: () { + ///TODO:Create Album + }, + icon: Icon( + Icons.add, + color: context.colorScheme.textPrimary, + ), + ), + ], + body: FadeInSwitcher(child: _body(context: context)), + ); + } + + Widget _body({required BuildContext context}) { + final state = ref.watch(albumStateNotifierProvider); + + if (state.loading) { + return const Center(child: AppCircularProgressIndicator()); + } else if (state.error != null) { + return ErrorScreen( + error: state.error!, + onRetryTap: _notifier.loadAlbums, + ); + } else if (state.medias.isEmpty) { + return PlaceHolderScreen( + icon: Icon( + CupertinoIcons.folder, + size: 100, + color: context.colorScheme.containerNormalOnSurface, + ), + title: 'Oops! No Albums Here!', + message: + "It seems like there are no albums to show right now. You can create a new one. We've got you covered!", + ); + } + + return GridView( + gridDelegate: + const SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 2), + children: state.albums + .map( + (album) => Container( + color: context.colorScheme.textSecondary, + ), + ) + .toList(), ); } } diff --git a/app/lib/ui/flow/albums/albums_view_notifier.dart b/app/lib/ui/flow/albums/albums_view_notifier.dart index 8b137891..90680579 100644 --- a/app/lib/ui/flow/albums/albums_view_notifier.dart +++ b/app/lib/ui/flow/albums/albums_view_notifier.dart @@ -1 +1,124 @@ +import 'package:data/log/logger.dart'; +import 'package:data/models/album/album.dart'; +import 'package:data/models/dropbox/account/dropbox_account.dart'; +import 'package:data/models/media/media.dart'; +import 'package:data/services/auth_service.dart'; +import 'package:data/services/dropbox_services.dart'; +import 'package:data/services/google_drive_service.dart'; +import 'package:data/services/local_media_service.dart'; +import 'package:data/storage/app_preferences.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:google_sign_in/google_sign_in.dart'; +import 'package:logger/logger.dart'; +part 'albums_view_notifier.freezed.dart'; + +final albumStateNotifierProvider = + StateNotifierProvider.autoDispose((ref) { + final notifier = AlbumStateNotifier( + ref.read(localMediaServiceProvider), + ref.read(googleDriveServiceProvider), + ref.read(dropboxServiceProvider), + ref.read(loggerProvider), + ref.read(googleUserAccountProvider), + ref.read(AppPreferences.dropboxCurrentUserAccount), + ); + ref.listen(googleUserAccountProvider, (p, c) { + notifier.onGoogleDriveAccountChange(c); + }); + ref.listen(AppPreferences.dropboxCurrentUserAccount, (p, c) { + notifier.onDropboxAccountChange(c); + }); + return notifier; +}); + +class AlbumStateNotifier extends StateNotifier { + final LocalMediaService _localMediaService; + final GoogleDriveService _googleDriveService; + final DropboxService _dropboxService; + final Logger _logger; + String? _backupFolderId; + + AlbumStateNotifier( + this._localMediaService, + this._googleDriveService, + this._dropboxService, + this._logger, + GoogleSignInAccount? googleAccount, + DropboxAccount? dropboxAccount, + ) : super(const AlbumsState()) { + loadAlbums(); + } + + Future onGoogleDriveAccountChange( + GoogleSignInAccount? googleAccount, + ) async { + state = state.copyWith(googleAccount: googleAccount); + if (googleAccount != null) { + _backupFolderId = await _googleDriveService.getBackUpFolderId(); + loadAlbums(); + } else { + _backupFolderId = null; + state = state.copyWith( + albums: state.albums + .where((element) => element.source != AppMediaSource.googleDrive) + .toList(), + ); + } + } + + void onDropboxAccountChange(DropboxAccount? dropboxAccount) { + state = state.copyWith(dropboxAccount: dropboxAccount); + if (dropboxAccount != null) { + loadAlbums(); + } else { + state = state.copyWith( + albums: state.albums + .where((element) => element.source != AppMediaSource.dropbox) + .toList(), + ); + } + } + + Future loadAlbums() async { + if (state.loading) return; + + state = state.copyWith(loading: true, error: null); + try { + final res = await Future.wait([ + _localMediaService.getAlbums(), + (state.googleAccount != null && _backupFolderId != null) + ? _googleDriveService.getAlbums(folderId: _backupFolderId!) + : Future.value([]), + (state.dropboxAccount != null) + ? _dropboxService.getAlbums() + : Future.value([]), + ]); + + state = state.copyWith( + albums: [...res[0], ...res[1], ...res[2]], + loading: false, + ); + } catch (e, s) { + state = state.copyWith(loading: false, error: e); + _logger.e( + "AlbumStateNotifier: Error loading albums", + error: e, + stackTrace: s, + ); + } + } +} + +@freezed +class AlbumsState with _$AlbumsState { + const factory AlbumsState({ + @Default(false) bool loading, + @Default([]) List medias, + @Default([]) List albums, + GoogleSignInAccount? googleAccount, + DropboxAccount? dropboxAccount, + Object? error, + }) = _AlbumsState; +} diff --git a/app/lib/ui/flow/albums/albums_view_notifier.freezed.dart b/app/lib/ui/flow/albums/albums_view_notifier.freezed.dart new file mode 100644 index 00000000..b5a0fe1a --- /dev/null +++ b/app/lib/ui/flow/albums/albums_view_notifier.freezed.dart @@ -0,0 +1,286 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'albums_view_notifier.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); + +/// @nodoc +mixin _$AlbumsState { + bool get loading => throw _privateConstructorUsedError; + List get medias => throw _privateConstructorUsedError; + List get albums => throw _privateConstructorUsedError; + GoogleSignInAccount? get googleAccount => throw _privateConstructorUsedError; + DropboxAccount? get dropboxAccount => throw _privateConstructorUsedError; + Object? get error => throw _privateConstructorUsedError; + + /// Create a copy of AlbumsState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $AlbumsStateCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $AlbumsStateCopyWith<$Res> { + factory $AlbumsStateCopyWith( + AlbumsState value, $Res Function(AlbumsState) then) = + _$AlbumsStateCopyWithImpl<$Res, AlbumsState>; + @useResult + $Res call( + {bool loading, + List medias, + List albums, + GoogleSignInAccount? googleAccount, + DropboxAccount? dropboxAccount, + Object? error}); + + $DropboxAccountCopyWith<$Res>? get dropboxAccount; +} + +/// @nodoc +class _$AlbumsStateCopyWithImpl<$Res, $Val extends AlbumsState> + implements $AlbumsStateCopyWith<$Res> { + _$AlbumsStateCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of AlbumsState + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? loading = null, + Object? medias = null, + Object? albums = null, + Object? googleAccount = freezed, + Object? dropboxAccount = freezed, + Object? error = freezed, + }) { + return _then(_value.copyWith( + loading: null == loading + ? _value.loading + : loading // ignore: cast_nullable_to_non_nullable + as bool, + medias: null == medias + ? _value.medias + : medias // ignore: cast_nullable_to_non_nullable + as List, + albums: null == albums + ? _value.albums + : albums // ignore: cast_nullable_to_non_nullable + as List, + googleAccount: freezed == googleAccount + ? _value.googleAccount + : googleAccount // ignore: cast_nullable_to_non_nullable + as GoogleSignInAccount?, + dropboxAccount: freezed == dropboxAccount + ? _value.dropboxAccount + : dropboxAccount // ignore: cast_nullable_to_non_nullable + as DropboxAccount?, + error: freezed == error ? _value.error : error, + ) as $Val); + } + + /// Create a copy of AlbumsState + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $DropboxAccountCopyWith<$Res>? get dropboxAccount { + if (_value.dropboxAccount == null) { + return null; + } + + return $DropboxAccountCopyWith<$Res>(_value.dropboxAccount!, (value) { + return _then(_value.copyWith(dropboxAccount: value) as $Val); + }); + } +} + +/// @nodoc +abstract class _$$AlbumsStateImplCopyWith<$Res> + implements $AlbumsStateCopyWith<$Res> { + factory _$$AlbumsStateImplCopyWith( + _$AlbumsStateImpl value, $Res Function(_$AlbumsStateImpl) then) = + __$$AlbumsStateImplCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {bool loading, + List medias, + List albums, + GoogleSignInAccount? googleAccount, + DropboxAccount? dropboxAccount, + Object? error}); + + @override + $DropboxAccountCopyWith<$Res>? get dropboxAccount; +} + +/// @nodoc +class __$$AlbumsStateImplCopyWithImpl<$Res> + extends _$AlbumsStateCopyWithImpl<$Res, _$AlbumsStateImpl> + implements _$$AlbumsStateImplCopyWith<$Res> { + __$$AlbumsStateImplCopyWithImpl( + _$AlbumsStateImpl _value, $Res Function(_$AlbumsStateImpl) _then) + : super(_value, _then); + + /// Create a copy of AlbumsState + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? loading = null, + Object? medias = null, + Object? albums = null, + Object? googleAccount = freezed, + Object? dropboxAccount = freezed, + Object? error = freezed, + }) { + return _then(_$AlbumsStateImpl( + loading: null == loading + ? _value.loading + : loading // ignore: cast_nullable_to_non_nullable + as bool, + medias: null == medias + ? _value._medias + : medias // ignore: cast_nullable_to_non_nullable + as List, + albums: null == albums + ? _value._albums + : albums // ignore: cast_nullable_to_non_nullable + as List, + googleAccount: freezed == googleAccount + ? _value.googleAccount + : googleAccount // ignore: cast_nullable_to_non_nullable + as GoogleSignInAccount?, + dropboxAccount: freezed == dropboxAccount + ? _value.dropboxAccount + : dropboxAccount // ignore: cast_nullable_to_non_nullable + as DropboxAccount?, + error: freezed == error ? _value.error : error, + )); + } +} + +/// @nodoc + +class _$AlbumsStateImpl implements _AlbumsState { + const _$AlbumsStateImpl( + {this.loading = false, + final List medias = const [], + final List albums = const [], + this.googleAccount, + this.dropboxAccount, + this.error}) + : _medias = medias, + _albums = albums; + + @override + @JsonKey() + final bool loading; + final List _medias; + @override + @JsonKey() + List get medias { + if (_medias is EqualUnmodifiableListView) return _medias; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_medias); + } + + final List _albums; + @override + @JsonKey() + List get albums { + if (_albums is EqualUnmodifiableListView) return _albums; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_albums); + } + + @override + final GoogleSignInAccount? googleAccount; + @override + final DropboxAccount? dropboxAccount; + @override + final Object? error; + + @override + String toString() { + return 'AlbumsState(loading: $loading, medias: $medias, albums: $albums, googleAccount: $googleAccount, dropboxAccount: $dropboxAccount, error: $error)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$AlbumsStateImpl && + (identical(other.loading, loading) || other.loading == loading) && + const DeepCollectionEquality().equals(other._medias, _medias) && + const DeepCollectionEquality().equals(other._albums, _albums) && + (identical(other.googleAccount, googleAccount) || + other.googleAccount == googleAccount) && + (identical(other.dropboxAccount, dropboxAccount) || + other.dropboxAccount == dropboxAccount) && + const DeepCollectionEquality().equals(other.error, error)); + } + + @override + int get hashCode => Object.hash( + runtimeType, + loading, + const DeepCollectionEquality().hash(_medias), + const DeepCollectionEquality().hash(_albums), + googleAccount, + dropboxAccount, + const DeepCollectionEquality().hash(error)); + + /// Create a copy of AlbumsState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$AlbumsStateImplCopyWith<_$AlbumsStateImpl> get copyWith => + __$$AlbumsStateImplCopyWithImpl<_$AlbumsStateImpl>(this, _$identity); +} + +abstract class _AlbumsState implements AlbumsState { + const factory _AlbumsState( + {final bool loading, + final List medias, + final List albums, + final GoogleSignInAccount? googleAccount, + final DropboxAccount? dropboxAccount, + final Object? error}) = _$AlbumsStateImpl; + + @override + bool get loading; + @override + List get medias; + @override + List get albums; + @override + GoogleSignInAccount? get googleAccount; + @override + DropboxAccount? get dropboxAccount; + @override + Object? get error; + + /// Create a copy of AlbumsState + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$AlbumsStateImplCopyWith<_$AlbumsStateImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/app/lib/ui/flow/home/home_screen.dart b/app/lib/ui/flow/home/home_screen.dart index e2f2d41b..4b013ec9 100644 --- a/app/lib/ui/flow/home/home_screen.dart +++ b/app/lib/ui/flow/home/home_screen.dart @@ -64,11 +64,6 @@ class _HomeScreenState extends ConsumerState { _errorObserver(); return AppPage( titleWidget: const HomeAppTitle(), - actions: const [ - HomeTransferButton(), - SizedBox(width: 8), - HomeAccountButton(), - ], body: FadeInSwitcher(child: _body(context: context)), ); } diff --git a/app/lib/ui/flow/main/main_screen.dart b/app/lib/ui/flow/main/main_screen.dart new file mode 100644 index 00000000..de7a6906 --- /dev/null +++ b/app/lib/ui/flow/main/main_screen.dart @@ -0,0 +1,153 @@ +import 'dart:io'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:style/extensions/context_extensions.dart'; +import '../../navigation/app_route.dart'; + +class MainScreen extends StatefulWidget { + final StatefulNavigationShell navigationShell; + + const MainScreen({ + super.key, + required this.navigationShell, + }); + + @override + State createState() => _MainScreenState(); +} + +class _MainScreenState extends State { + @override + Widget build(BuildContext context) { + final tabs = [ + ( + icon: CupertinoIcons.house_fill, + label: "Home", + activeIcon: CupertinoIcons.house_fill, + ), + ( + icon: CupertinoIcons.folder, + label: "Albums", + activeIcon: CupertinoIcons.folder_fill + ), + ( + icon: CupertinoIcons.arrow_up_arrow_down, + label: "Transfer", + activeIcon: CupertinoIcons.arrow_up_arrow_down + ), + ( + icon: CupertinoIcons.person, + label: "Account", + activeIcon: CupertinoIcons.person_fill + ), + ]; + + if (!kIsWeb && Platform.isIOS) { + return CupertinoTabScaffold( + key: ValueKey(widget.navigationShell.currentIndex), + tabBar: CupertinoTabBar( + currentIndex: widget.navigationShell.currentIndex, + activeColor: context.colorScheme.primary, + inactiveColor: context.colorScheme.textDisabled, + onTap: (index) => _goBranch( + index: index, + context: context, + ), + border: Border( + top: BorderSide( + color: context.colorScheme.outline, + width: 1, + ), + ), + items: tabs + .map( + (e) => BottomNavigationBarItem( + icon: Icon( + e.icon, + color: context.colorScheme.textDisabled, + size: 22, + ), + label: e.label, + activeIcon: Icon( + e.activeIcon, + color: context.colorScheme.primary, + size: 24, + ), + ), + ) + .toList(), + ), + tabBuilder: (context, index) { + return widget.navigationShell; + }, + ); + } else { + return Scaffold( + body: widget.navigationShell, + bottomNavigationBar: Container( + decoration: BoxDecoration( + border: Border( + top: BorderSide( + color: context.colorScheme.outline, + width: 1, + ), + ), + ), + child: BottomNavigationBar( + items: tabs + .map( + (e) => BottomNavigationBarItem( + icon: Icon( + e.icon, + color: context.colorScheme.textDisabled, + size: 24, + ), + label: e.label, + activeIcon: Icon( + e.activeIcon, + color: context.colorScheme.primary, + size: 24, + ), + ), + ) + .toList(), + currentIndex: widget.navigationShell.currentIndex, + selectedItemColor: context.colorScheme.primary, + unselectedItemColor: context.colorScheme.textDisabled, + backgroundColor: context.colorScheme.surface, + type: BottomNavigationBarType.fixed, + selectedFontSize: 12, + unselectedFontSize: 12, + elevation: 0, + onTap: (index) => _goBranch( + index: index, + context: context, + ), + ), + ), + ); + } + } + + void _goBranch({ + required int index, + required BuildContext context, + }) { + switch (index) { + case 0: + HomeRoute().go(context); + break; + case 1: + AlbumsRoute().go(context); + break; + case 2: + TransferRoute().go(context); + break; + case 3: + AccountRoute().go(context); + break; + } + } +} diff --git a/app/lib/ui/navigation/app_route.dart b/app/lib/ui/navigation/app_route.dart index 0457f691..79bc0c1f 100644 --- a/app/lib/ui/navigation/app_route.dart +++ b/app/lib/ui/navigation/app_route.dart @@ -1,4 +1,6 @@ import '../flow/accounts/accounts_screen.dart'; +import '../flow/albums/albums_screen.dart'; +import '../flow/main/main_screen.dart'; import '../flow/media_transfer/media_transfer_screen.dart'; import '../flow/onboard/onboard_screen.dart'; import 'package:data/models/media/media.dart'; @@ -12,6 +14,7 @@ part 'app_route.g.dart'; class AppRoutePath { static const home = '/'; + static const albums = '/albums'; static const onBoard = '/on-board'; static const accounts = '/accounts'; static const preview = '/preview'; @@ -19,14 +22,6 @@ class AppRoutePath { static const metaDataDetails = '/metadata-details'; } -@TypedGoRoute(path: AppRoutePath.home) -class HomeRoute extends GoRouteData { - const HomeRoute(); - - @override - Widget build(BuildContext context, GoRouterState state) => const HomeScreen(); -} - @TypedGoRoute(path: AppRoutePath.onBoard) class OnBoardRoute extends GoRouteData { const OnBoardRoute(); @@ -36,16 +31,65 @@ class OnBoardRoute extends GoRouteData { const OnBoardScreen(); } -@TypedGoRoute(path: AppRoutePath.accounts) -class AccountRoute extends GoRouteData { - const AccountRoute(); +@TypedStatefulShellRoute( + branches: [ + TypedStatefulShellBranch( + routes: [ + TypedGoRoute(path: AppRoutePath.home), + ], + ), + TypedStatefulShellBranch( + routes: [ + TypedGoRoute(path: AppRoutePath.albums), + ], + ), + TypedStatefulShellBranch( + routes: [ + TypedGoRoute(path: AppRoutePath.transfer), + ], + ), + TypedStatefulShellBranch( + routes: [ + TypedGoRoute(path: AppRoutePath.accounts), + ], + ), + ], +) +class MainShellRoute extends StatefulShellRouteData { + const MainShellRoute(); + + @override + Widget builder( + BuildContext context, + GoRouterState state, + StatefulNavigationShell navigationShell, + ) => + MainScreen(navigationShell: navigationShell); +} + +class HomeShellBranch extends StatefulShellBranchData {} + +class AlbumsShellBranch extends StatefulShellBranchData {} + +class TransferShellBranch extends StatefulShellBranchData {} + +class AccountsShellBranch extends StatefulShellBranchData {} + +class HomeRoute extends GoRouteData { + const HomeRoute(); + + @override + Widget build(BuildContext context, GoRouterState state) => const HomeScreen(); +} + +class AlbumsRoute extends GoRouteData { + const AlbumsRoute(); @override Widget build(BuildContext context, GoRouterState state) => - const AccountsScreen(); + const AlbumsScreen(); } -@TypedGoRoute(path: AppRoutePath.transfer) class TransferRoute extends GoRouteData { const TransferRoute(); @@ -54,6 +98,14 @@ class TransferRoute extends GoRouteData { const MediaTransferScreen(); } +class AccountRoute extends GoRouteData { + const AccountRoute(); + + @override + Widget build(BuildContext context, GoRouterState state) => + const AccountsScreen(); +} + class MediaPreviewRouteData { final List medias; final String startFrom; diff --git a/app/lib/ui/navigation/app_route.g.dart b/app/lib/ui/navigation/app_route.g.dart index 1ed07e89..acf3d61f 100644 --- a/app/lib/ui/navigation/app_route.g.dart +++ b/app/lib/ui/navigation/app_route.g.dart @@ -7,24 +7,22 @@ part of 'app_route.dart'; // ************************************************************************** List get $appRoutes => [ - $homeRoute, $onBoardRoute, - $accountRoute, - $transferRoute, + $mainShellRoute, $mediaPreviewRoute, $mediaMetadataDetailsRoute, ]; -RouteBase get $homeRoute => GoRouteData.$route( - path: '/', - factory: $HomeRouteExtension._fromState, +RouteBase get $onBoardRoute => GoRouteData.$route( + path: '/on-board', + factory: $OnBoardRouteExtension._fromState, ); -extension $HomeRouteExtension on HomeRoute { - static HomeRoute _fromState(GoRouterState state) => const HomeRoute(); +extension $OnBoardRouteExtension on OnBoardRoute { + static OnBoardRoute _fromState(GoRouterState state) => const OnBoardRoute(); String get location => GoRouteData.$location( - '/', + '/on-board', ); void go(BuildContext context) => context.go(location); @@ -37,16 +35,54 @@ extension $HomeRouteExtension on HomeRoute { void replace(BuildContext context) => context.replace(location); } -RouteBase get $onBoardRoute => GoRouteData.$route( - path: '/on-board', - factory: $OnBoardRouteExtension._fromState, +RouteBase get $mainShellRoute => StatefulShellRouteData.$route( + factory: $MainShellRouteExtension._fromState, + branches: [ + StatefulShellBranchData.$branch( + routes: [ + GoRouteData.$route( + path: '/', + factory: $HomeRouteExtension._fromState, + ), + ], + ), + StatefulShellBranchData.$branch( + routes: [ + GoRouteData.$route( + path: '/albums', + factory: $AlbumsRouteExtension._fromState, + ), + ], + ), + StatefulShellBranchData.$branch( + routes: [ + GoRouteData.$route( + path: '/transfer', + factory: $TransferRouteExtension._fromState, + ), + ], + ), + StatefulShellBranchData.$branch( + routes: [ + GoRouteData.$route( + path: '/accounts', + factory: $AccountRouteExtension._fromState, + ), + ], + ), + ], ); -extension $OnBoardRouteExtension on OnBoardRoute { - static OnBoardRoute _fromState(GoRouterState state) => const OnBoardRoute(); +extension $MainShellRouteExtension on MainShellRoute { + static MainShellRoute _fromState(GoRouterState state) => + const MainShellRoute(); +} + +extension $HomeRouteExtension on HomeRoute { + static HomeRoute _fromState(GoRouterState state) => const HomeRoute(); String get location => GoRouteData.$location( - '/on-board', + '/', ); void go(BuildContext context) => context.go(location); @@ -59,16 +95,11 @@ extension $OnBoardRouteExtension on OnBoardRoute { void replace(BuildContext context) => context.replace(location); } -RouteBase get $accountRoute => GoRouteData.$route( - path: '/accounts', - factory: $AccountRouteExtension._fromState, - ); - -extension $AccountRouteExtension on AccountRoute { - static AccountRoute _fromState(GoRouterState state) => const AccountRoute(); +extension $AlbumsRouteExtension on AlbumsRoute { + static AlbumsRoute _fromState(GoRouterState state) => const AlbumsRoute(); String get location => GoRouteData.$location( - '/accounts', + '/albums', ); void go(BuildContext context) => context.go(location); @@ -81,11 +112,6 @@ extension $AccountRouteExtension on AccountRoute { void replace(BuildContext context) => context.replace(location); } -RouteBase get $transferRoute => GoRouteData.$route( - path: '/transfer', - factory: $TransferRouteExtension._fromState, - ); - extension $TransferRouteExtension on TransferRoute { static TransferRoute _fromState(GoRouterState state) => const TransferRoute(); @@ -103,6 +129,23 @@ extension $TransferRouteExtension on TransferRoute { void replace(BuildContext context) => context.replace(location); } +extension $AccountRouteExtension on AccountRoute { + static AccountRoute _fromState(GoRouterState state) => const AccountRoute(); + + String get location => GoRouteData.$location( + '/accounts', + ); + + void go(BuildContext context) => context.go(location); + + Future push(BuildContext context) => context.push(location); + + void pushReplacement(BuildContext context) => + context.pushReplacement(location); + + void replace(BuildContext context) => context.replace(location); +} + RouteBase get $mediaPreviewRoute => GoRouteData.$route( path: '/preview', factory: $MediaPreviewRouteExtension._fromState, diff --git a/data/lib/domain/config.dart b/data/lib/domain/config.dart index bb91fcc7..1f7d67a5 100644 --- a/data/lib/domain/config.dart +++ b/data/lib/domain/config.dart @@ -9,6 +9,7 @@ class ProviderConstants { class LocalDatabaseConstants { static const String databaseName = 'cloud-gallery.db'; + static const String albumDatabaseName = 'cloud-gallery-album.db'; static const String uploadQueueTable = 'UploadQueue'; static const String downloadQueueTable = 'DownloadQueue'; static const String albumsTable = 'Albums'; diff --git a/data/lib/services/dropbox_services.dart b/data/lib/services/dropbox_services.dart index b5fbfad4..c0fa11b6 100644 --- a/data/lib/services/dropbox_services.dart +++ b/data/lib/services/dropbox_services.dart @@ -384,9 +384,7 @@ class DropboxService extends CloudProviderService { ? [] : json.map((e) => Album.fromJson(e)).toList(); } catch (e) { - if (e is DioException && - e.response?.statusCode == 409 && - e.response?.data?['error']?['path']?['.tag'] == 'not_found') { + if (e is DioException && e.response?.statusCode == 409) { return []; } rethrow; @@ -405,7 +403,7 @@ class DropboxService extends CloudProviderService { content: AppMediaContent( stream: Stream.value(utf8.encode(jsonEncode(album))), length: utf8.encode(jsonEncode(album)).length, - contentType: 'application/json', + contentType: 'application/octet-stream', ), filePath: "/${ProviderConstants.backupFolderName}/Albums.json", ), diff --git a/data/lib/services/google_drive_service.dart b/data/lib/services/google_drive_service.dart index 896166e0..6f493edc 100644 --- a/data/lib/services/google_drive_service.dart +++ b/data/lib/services/google_drive_service.dart @@ -25,7 +25,7 @@ class GoogleDriveService extends CloudProviderService { GoogleDriveService(this._client); - //FOLDERS -------------------------------------------------------------------- + // FOLDERS ------------------------------------------------------------------- Future getBackUpFolderId() async { final res = await _client.req( @@ -79,7 +79,7 @@ class GoogleDriveService extends CloudProviderService { ); } - //MEDIA ---------------------------------------------------------------------- + // MEDIA --------------------------------------------------------------------- @override Future> getAllMedias({ diff --git a/data/lib/services/local_media_service.dart b/data/lib/services/local_media_service.dart index 257fc510..1906fc23 100644 --- a/data/lib/services/local_media_service.dart +++ b/data/lib/services/local_media_service.dart @@ -74,6 +74,12 @@ class LocalMediaService { return files.nonNulls.toList(); } + Future getMedia({required String id}) async { + final asset = await AssetEntity.fromId(id); + if (asset == null) return null; + return AppMedia.fromAssetEntity(asset); + } + Future> deleteMedias(List medias) async { return await PhotoManager.editor.deleteWithIds(medias); } @@ -101,7 +107,7 @@ class LocalMediaService { Future openAlbumDatabase() async { return await openDatabase( - LocalDatabaseConstants.databaseName, + LocalDatabaseConstants.albumDatabaseName, version: 1, onCreate: (Database db, int version) async { await db.execute( @@ -110,7 +116,7 @@ class LocalMediaService { 'name TEXT NOT NULL, ' 'source TEXT NOT NULL, ' 'created_at TEXT NOT NULL, ' - 'medias TEXT NOT NULL, ' + 'medias TEXT NOT NULL ' ')', ); }, @@ -158,7 +164,7 @@ class LocalMediaService { await db.close(); } - Future> getAllAlbums() async { + Future> getAlbums() async { final db = await openAlbumDatabase(); final albums = await db.query(LocalDatabaseConstants.albumsTable); await db.close(); diff --git a/style/lib/callback/on_visible_callback.dart b/style/lib/callback/on_visible_callback.dart new file mode 100644 index 00000000..be9cae69 --- /dev/null +++ b/style/lib/callback/on_visible_callback.dart @@ -0,0 +1,28 @@ +import 'package:flutter/cupertino.dart'; + +class OnVisibleCallback extends StatefulWidget { + final void Function() onVisible; + final Widget child; + + const OnVisibleCallback({ + super.key, + required this.onVisible, + required this.child, + }); + + @override + State createState() => _OnCreateState(); +} + +class _OnCreateState extends State { + @override + void initState() { + widget.onVisible(); + super.initState(); + } + + @override + Widget build(BuildContext context) { + return widget.child; + } +} diff --git a/style/lib/theme/colors.dart b/style/lib/theme/colors.dart index c41b7bc2..c36d0cc9 100644 --- a/style/lib/theme/colors.dart +++ b/style/lib/theme/colors.dart @@ -20,8 +20,8 @@ class AppColors { static const surfaceLightColor = Color(0xFFFFFFFF); static const surfaceDarkColor = Color(0xFF000000); - static const barLightColor = Color(0xCCFFFFFF); - static const barDarkColor = Color(0xCC000000); + static const barLightColor = Color(0xE6FFFFFF); + static const barDarkColor = Color(0xE6000000); static const textPrimaryLightColor = Color(0xDE000000); static const textSecondaryLightColor = Color(0x99000000); From 65b8b7989726228b8ac4b590dcd40a39d6f969ee Mon Sep 17 00:00:00 2001 From: Pratik-canopas Date: Wed, 1 Jan 2025 10:43:56 +0530 Subject: [PATCH 03/24] test --- .../albums/add/add_album_state_notifier.dart | 1 + .../add/add_album_state_notifier.freezed.dart | 274 ++++++++++++++++++ 2 files changed, 275 insertions(+) create mode 100644 app/lib/ui/flow/albums/add/add_album_state_notifier.freezed.dart diff --git a/app/lib/ui/flow/albums/add/add_album_state_notifier.dart b/app/lib/ui/flow/albums/add/add_album_state_notifier.dart index 34b227e1..917d8c84 100644 --- a/app/lib/ui/flow/albums/add/add_album_state_notifier.dart +++ b/app/lib/ui/flow/albums/add/add_album_state_notifier.dart @@ -5,6 +5,7 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:google_sign_in/google_sign_in.dart'; +part 'add_album_state_notifier.freezed.dart'; class AddAlbumStateNotifier extends StateNotifier { final GoogleSignInAccount? googleAccount; diff --git a/app/lib/ui/flow/albums/add/add_album_state_notifier.freezed.dart b/app/lib/ui/flow/albums/add/add_album_state_notifier.freezed.dart new file mode 100644 index 00000000..e2151b35 --- /dev/null +++ b/app/lib/ui/flow/albums/add/add_album_state_notifier.freezed.dart @@ -0,0 +1,274 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'add_album_state_notifier.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); + +/// @nodoc +mixin _$AddAlbumsState { + bool get loading => throw _privateConstructorUsedError; + AppMediaSource? get mediaSource => throw _privateConstructorUsedError; + TextEditingController get albumNameController => + throw _privateConstructorUsedError; + GoogleSignInAccount? get googleAccount => throw _privateConstructorUsedError; + DropboxAccount? get dropboxAccount => throw _privateConstructorUsedError; + Object? get error => throw _privateConstructorUsedError; + + /// Create a copy of AddAlbumsState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $AddAlbumsStateCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $AddAlbumsStateCopyWith<$Res> { + factory $AddAlbumsStateCopyWith( + AddAlbumsState value, $Res Function(AddAlbumsState) then) = + _$AddAlbumsStateCopyWithImpl<$Res, AddAlbumsState>; + @useResult + $Res call( + {bool loading, + AppMediaSource? mediaSource, + TextEditingController albumNameController, + GoogleSignInAccount? googleAccount, + DropboxAccount? dropboxAccount, + Object? error}); + + $DropboxAccountCopyWith<$Res>? get dropboxAccount; +} + +/// @nodoc +class _$AddAlbumsStateCopyWithImpl<$Res, $Val extends AddAlbumsState> + implements $AddAlbumsStateCopyWith<$Res> { + _$AddAlbumsStateCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of AddAlbumsState + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? loading = null, + Object? mediaSource = freezed, + Object? albumNameController = null, + Object? googleAccount = freezed, + Object? dropboxAccount = freezed, + Object? error = freezed, + }) { + return _then(_value.copyWith( + loading: null == loading + ? _value.loading + : loading // ignore: cast_nullable_to_non_nullable + as bool, + mediaSource: freezed == mediaSource + ? _value.mediaSource + : mediaSource // ignore: cast_nullable_to_non_nullable + as AppMediaSource?, + albumNameController: null == albumNameController + ? _value.albumNameController + : albumNameController // ignore: cast_nullable_to_non_nullable + as TextEditingController, + googleAccount: freezed == googleAccount + ? _value.googleAccount + : googleAccount // ignore: cast_nullable_to_non_nullable + as GoogleSignInAccount?, + dropboxAccount: freezed == dropboxAccount + ? _value.dropboxAccount + : dropboxAccount // ignore: cast_nullable_to_non_nullable + as DropboxAccount?, + error: freezed == error ? _value.error : error, + ) as $Val); + } + + /// Create a copy of AddAlbumsState + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $DropboxAccountCopyWith<$Res>? get dropboxAccount { + if (_value.dropboxAccount == null) { + return null; + } + + return $DropboxAccountCopyWith<$Res>(_value.dropboxAccount!, (value) { + return _then(_value.copyWith(dropboxAccount: value) as $Val); + }); + } +} + +/// @nodoc +abstract class _$$AddAlbumsStateImplCopyWith<$Res> + implements $AddAlbumsStateCopyWith<$Res> { + factory _$$AddAlbumsStateImplCopyWith(_$AddAlbumsStateImpl value, + $Res Function(_$AddAlbumsStateImpl) then) = + __$$AddAlbumsStateImplCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {bool loading, + AppMediaSource? mediaSource, + TextEditingController albumNameController, + GoogleSignInAccount? googleAccount, + DropboxAccount? dropboxAccount, + Object? error}); + + @override + $DropboxAccountCopyWith<$Res>? get dropboxAccount; +} + +/// @nodoc +class __$$AddAlbumsStateImplCopyWithImpl<$Res> + extends _$AddAlbumsStateCopyWithImpl<$Res, _$AddAlbumsStateImpl> + implements _$$AddAlbumsStateImplCopyWith<$Res> { + __$$AddAlbumsStateImplCopyWithImpl( + _$AddAlbumsStateImpl _value, $Res Function(_$AddAlbumsStateImpl) _then) + : super(_value, _then); + + /// Create a copy of AddAlbumsState + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? loading = null, + Object? mediaSource = freezed, + Object? albumNameController = null, + Object? googleAccount = freezed, + Object? dropboxAccount = freezed, + Object? error = freezed, + }) { + return _then(_$AddAlbumsStateImpl( + loading: null == loading + ? _value.loading + : loading // ignore: cast_nullable_to_non_nullable + as bool, + mediaSource: freezed == mediaSource + ? _value.mediaSource + : mediaSource // ignore: cast_nullable_to_non_nullable + as AppMediaSource?, + albumNameController: null == albumNameController + ? _value.albumNameController + : albumNameController // ignore: cast_nullable_to_non_nullable + as TextEditingController, + googleAccount: freezed == googleAccount + ? _value.googleAccount + : googleAccount // ignore: cast_nullable_to_non_nullable + as GoogleSignInAccount?, + dropboxAccount: freezed == dropboxAccount + ? _value.dropboxAccount + : dropboxAccount // ignore: cast_nullable_to_non_nullable + as DropboxAccount?, + error: freezed == error ? _value.error : error, + )); + } +} + +/// @nodoc + +class _$AddAlbumsStateImpl implements _AddAlbumsState { + const _$AddAlbumsStateImpl( + {this.loading = false, + this.mediaSource, + required this.albumNameController, + this.googleAccount, + this.dropboxAccount, + this.error}); + + @override + @JsonKey() + final bool loading; + @override + final AppMediaSource? mediaSource; + @override + final TextEditingController albumNameController; + @override + final GoogleSignInAccount? googleAccount; + @override + final DropboxAccount? dropboxAccount; + @override + final Object? error; + + @override + String toString() { + return 'AddAlbumsState(loading: $loading, mediaSource: $mediaSource, albumNameController: $albumNameController, googleAccount: $googleAccount, dropboxAccount: $dropboxAccount, error: $error)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$AddAlbumsStateImpl && + (identical(other.loading, loading) || other.loading == loading) && + (identical(other.mediaSource, mediaSource) || + other.mediaSource == mediaSource) && + (identical(other.albumNameController, albumNameController) || + other.albumNameController == albumNameController) && + (identical(other.googleAccount, googleAccount) || + other.googleAccount == googleAccount) && + (identical(other.dropboxAccount, dropboxAccount) || + other.dropboxAccount == dropboxAccount) && + const DeepCollectionEquality().equals(other.error, error)); + } + + @override + int get hashCode => Object.hash( + runtimeType, + loading, + mediaSource, + albumNameController, + googleAccount, + dropboxAccount, + const DeepCollectionEquality().hash(error)); + + /// Create a copy of AddAlbumsState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$AddAlbumsStateImplCopyWith<_$AddAlbumsStateImpl> get copyWith => + __$$AddAlbumsStateImplCopyWithImpl<_$AddAlbumsStateImpl>( + this, _$identity); +} + +abstract class _AddAlbumsState implements AddAlbumsState { + const factory _AddAlbumsState( + {final bool loading, + final AppMediaSource? mediaSource, + required final TextEditingController albumNameController, + final GoogleSignInAccount? googleAccount, + final DropboxAccount? dropboxAccount, + final Object? error}) = _$AddAlbumsStateImpl; + + @override + bool get loading; + @override + AppMediaSource? get mediaSource; + @override + TextEditingController get albumNameController; + @override + GoogleSignInAccount? get googleAccount; + @override + DropboxAccount? get dropboxAccount; + @override + Object? get error; + + /// Create a copy of AddAlbumsState + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$AddAlbumsStateImplCopyWith<_$AddAlbumsStateImpl> get copyWith => + throw _privateConstructorUsedError; +} From 3b2e47fb3db29e2929f151cda931f94d980108ff Mon Sep 17 00:00:00 2001 From: Pratik-canopas Date: Fri, 3 Jan 2025 12:21:51 +0530 Subject: [PATCH 04/24] Implement album support --- .idea/libraries/Flutter_Plugins.xml | 8 +- app/assets/locales/app_en.arb | 22 ++ app/lib/components/app_media_thumbnail.dart | 116 ++++++++ app/lib/components/app_page.dart | 9 +- app/lib/components/app_sheet.dart | 1 + app/lib/ui/app.dart | 1 + .../ui/flow/albums/add/add_album_screen.dart | 136 +++++++++ .../albums/add/add_album_state_notifier.dart | 145 +++++++++- .../add/add_album_state_notifier.freezed.dart | 74 ++++- app/lib/ui/flow/albums/albums_screen.dart | 155 +++++++++- .../ui/flow/albums/albums_view_notifier.dart | 34 ++- .../albums/albums_view_notifier.freezed.dart | 61 ++-- .../media_list/album_media_list_screen.dart | 154 ++++++++++ .../album_media_list_state_notifier.dart | 248 ++++++++++++++++ ...bum_media_list_state_notifier.freezed.dart | 270 ++++++++++++++++++ app/lib/ui/flow/main/main_screen.dart | 157 +++++----- .../video_duration_slider.dart | 82 +++--- .../media_selection_screen.dart | 205 +++++++++++++ .../media_selection_state_notifier.dart | 219 ++++++++++++++ ...edia_selection_state_notifier.freezed.dart | 265 +++++++++++++++++ app/lib/ui/navigation/app_route.dart | 56 +++- app/lib/ui/navigation/app_route.g.dart | 83 ++++++ data/.flutter-plugins-dependencies | 2 +- .../google_drive/google_drive_endpoint.dart | 28 +- data/lib/handlers/unique_id_generator.dart | 48 ++++ data/lib/services/dropbox_services.dart | 20 +- data/lib/services/google_drive_service.dart | 15 + data/lib/services/local_media_service.dart | 36 +-- style/lib/buttons/action_button.dart | 4 +- style/lib/buttons/radio_selection_button.dart | 62 ++++ style/lib/text/app_text_field.dart | 195 +++++++++++++ style/lib/theme/app_theme_builder.dart | 3 +- style/lib/theme/colors.dart | 3 - style/lib/theme/theme.dart | 4 - 34 files changed, 2687 insertions(+), 234 deletions(-) create mode 100644 app/lib/components/app_media_thumbnail.dart create mode 100644 app/lib/ui/flow/albums/media_list/album_media_list_screen.dart create mode 100644 app/lib/ui/flow/albums/media_list/album_media_list_state_notifier.dart create mode 100644 app/lib/ui/flow/albums/media_list/album_media_list_state_notifier.freezed.dart create mode 100644 app/lib/ui/flow/media_selection/media_selection_screen.dart create mode 100644 app/lib/ui/flow/media_selection/media_selection_state_notifier.dart create mode 100644 app/lib/ui/flow/media_selection/media_selection_state_notifier.freezed.dart create mode 100644 data/lib/handlers/unique_id_generator.dart create mode 100644 style/lib/buttons/radio_selection_button.dart create mode 100644 style/lib/text/app_text_field.dart diff --git a/.idea/libraries/Flutter_Plugins.xml b/.idea/libraries/Flutter_Plugins.xml index 393d7ece..b294abb3 100644 --- a/.idea/libraries/Flutter_Plugins.xml +++ b/.idea/libraries/Flutter_Plugins.xml @@ -26,6 +26,10 @@ + + + + @@ -51,10 +55,6 @@ - - - - diff --git a/app/assets/locales/app_en.arb b/app/assets/locales/app_en.arb index d45bd015..96bc1db0 100644 --- a/app/assets/locales/app_en.arb +++ b/app/assets/locales/app_en.arb @@ -15,6 +15,7 @@ "common_info":"Info", "common_upload": "Upload", "common_download": "Download", + "common_edit": "Edit", "common_delete": "Delete", "common_share": "Share", "common_cancel": "Cancel", @@ -116,6 +117,27 @@ "empty_media_title": "Oh Snap! No Media Found!", "empty_media_message": "Looks like your gallery is taking a little break.", + "@_ALBUM":{}, + "album_screen_title": "Albums", + "empty_album_title": "Oops! No Albums Here!", + "empty_album_message": "It seems like there are no albums to show right now. You can create a new one. We've got you covered!", + + "@_ADD_ALBUM":{}, + "add_album_screen_title": "Album", + "album_tame_field_title": "Album Name", + "store_in_title": "Store In", + "store_in_device_title": "Device", + + "@_ALBUM_MEDIA_LIST":{}, + "add_media_title": "Add Media", + + "@_MEDIA_SELECTION":{}, + "select_from_device_title": "Select from Device", + "select_from_google_drive_title": "Select from Google Drive", + "select_from_dropbox_title": "Select from Dropbox", + "no_media_access_title": "No cloud media access", + "no_cloud_media_access_message": "You don't have access to view media files, check you sign in with the cloud!", + "@_MEDIA_INFO":{}, "name_text": "Name", diff --git a/app/lib/components/app_media_thumbnail.dart b/app/lib/components/app_media_thumbnail.dart new file mode 100644 index 00000000..ec2cea2d --- /dev/null +++ b/app/lib/components/app_media_thumbnail.dart @@ -0,0 +1,116 @@ +import 'package:data/models/media/media.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:style/extensions/context_extensions.dart'; +import 'package:style/text/app_text_style.dart'; +import '../domain/formatter/duration_formatter.dart'; +import 'thumbnail_builder.dart'; + +class AppMediaThumbnail extends StatelessWidget { + final AppMedia media; + final String heroTag; + final void Function()? onTap; + final void Function()? onLongTap; + final bool selected; + + const AppMediaThumbnail({ + super.key, + required this.media, + required this.heroTag, + this.onTap, + this.onLongTap, + this.selected = false, + }); + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, constraints) => GestureDetector( + onTap: onTap, + onLongPress: onLongTap, + child: Stack( + alignment: Alignment.bottomLeft, + children: [ + AnimatedOpacity( + curve: Curves.easeInOut, + duration: const Duration(milliseconds: 100), + opacity: selected ? 0.6 : 1, + child: AppMediaImage( + radius: selected ? 4 : 0, + size: constraints.biggest, + media: media, + heroTag: "$heroTag${media.toString()}", + ), + ), + if (media.type.isVideo) _videoDuration(context), + if (selected) + Align( + alignment: Alignment.topLeft, + child: Container( + margin: const EdgeInsets.all(4), + padding: const EdgeInsets.all(4), + decoration: BoxDecoration( + shape: BoxShape.circle, + color: context.colorScheme.primary, + ), + child: const Icon( + CupertinoIcons.checkmark_alt, + color: Colors.white, + size: 14, + ), + ), + ), + ], + ), + ), + ); + } + + Widget _videoDuration(BuildContext context) => Align( + alignment: Alignment.topCenter, + child: Container( + decoration: BoxDecoration( + gradient: LinearGradient( + stops: [0.0, 0.9], + begin: Alignment.topRight, + end: Alignment.bottomRight, + colors: [ + Colors.black.withValues(alpha: 0.4), + Colors.transparent, + ], + ), + ), + padding: const EdgeInsets.all(4).copyWith(bottom: 8), + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Text( + (media.videoDuration ?? Duration.zero).format, + style: AppTextStyles.caption.copyWith( + color: Colors.white, + fontSize: 12, + shadows: [ + Shadow( + color: Colors.grey.shade400, + blurRadius: 5, + ), + ], + ), + ), + const SizedBox(width: 2), + Icon( + CupertinoIcons.play_fill, + color: Colors.white, + size: 14, + shadows: [ + Shadow( + color: Colors.grey.shade400, + blurRadius: 5, + ), + ], + ), + ], + ), + ), + ); +} diff --git a/app/lib/components/app_page.dart b/app/lib/components/app_page.dart index 06f51f9f..8b2da124 100644 --- a/app/lib/components/app_page.dart +++ b/app/lib/components/app_page.dart @@ -12,7 +12,7 @@ class AppPage extends StatelessWidget { final Widget? body; final Widget Function(BuildContext context)? bodyBuilder; final bool automaticallyImplyLeading; - final bool? resizeToAvoidBottomInset; + final bool resizeToAvoidBottomInset; final Color? backgroundColor; final Color? barBackgroundColor; @@ -24,7 +24,7 @@ class AppPage extends StatelessWidget { this.leading, this.body, this.floatingActionButton, - this.resizeToAvoidBottomInset, + this.resizeToAvoidBottomInset = true, this.bodyBuilder, this.automaticallyImplyLeading = true, this.barBackgroundColor, @@ -50,6 +50,7 @@ class AppPage extends StatelessWidget { leading: leading, middle: titleWidget ?? _title(context), border: null, + enableBackgroundFilterBlur: false, trailing: actions == null ? null : actions!.length == 1 @@ -63,7 +64,7 @@ class AppPage extends StatelessWidget { ? MaterialLocalizations.of(context).backButtonTooltip : null, ), - resizeToAvoidBottomInset: resizeToAvoidBottomInset ?? true, + resizeToAvoidBottomInset: resizeToAvoidBottomInset, backgroundColor: backgroundColor, child: Stack( alignment: Alignment.bottomRight, @@ -152,7 +153,7 @@ class AdaptiveAppBar extends StatelessWidget { children: [ AppBar( centerTitle: true, - backgroundColor: context.colorScheme.barColor, + backgroundColor: context.colorScheme.surface, leading: leading, actions: actions, automaticallyImplyLeading: automaticallyImplyLeading, diff --git a/app/lib/components/app_sheet.dart b/app/lib/components/app_sheet.dart index 6d56534a..7a69587f 100644 --- a/app/lib/components/app_sheet.dart +++ b/app/lib/components/app_sheet.dart @@ -6,6 +6,7 @@ Future showAppSheet({ required Widget child, }) { return showModalBottomSheet( + useRootNavigator: true, backgroundColor: context.colorScheme.containerNormalOnSurface, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(22), diff --git a/app/lib/ui/app.dart b/app/lib/ui/app.dart index 70a04f57..102180ef 100644 --- a/app/lib/ui/app.dart +++ b/app/lib/ui/app.dart @@ -39,6 +39,7 @@ class _CloudGalleryAppState extends ConsumerState { _handleNotification(); _router = GoRouter( + navigatorKey: rootNavigatorKey, initialLocation: _configureInitialRoute(), routes: $appRoutes, redirect: (context, state) { diff --git a/app/lib/ui/flow/albums/add/add_album_screen.dart b/app/lib/ui/flow/albums/add/add_album_screen.dart index 8b137891..14407f7b 100644 --- a/app/lib/ui/flow/albums/add/add_album_screen.dart +++ b/app/lib/ui/flow/albums/add/add_album_screen.dart @@ -1 +1,137 @@ +import 'package:data/models/album/album.dart'; +import 'package:data/models/media/media.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import 'package:style/buttons/action_button.dart'; +import 'package:style/extensions/context_extensions.dart'; +import 'package:style/text/app_text_field.dart'; +import 'package:style/text/app_text_style.dart'; +import '../../../../components/app_page.dart'; +import '../../../../components/snack_bar.dart'; +import '../../../../domain/extensions/context_extensions.dart'; +import 'add_album_state_notifier.dart'; +import 'package:style/buttons/radio_selection_button.dart'; +class AddAlbumScreen extends ConsumerStatefulWidget { + final Album? editAlbum; + + const AddAlbumScreen({super.key, this.editAlbum}); + + @override + ConsumerState createState() => _AddAlbumScreenState(); +} + +class _AddAlbumScreenState extends ConsumerState { + late AutoDisposeStateNotifierProvider + _provider; + late AddAlbumStateNotifier _notifier; + + @override + void initState() { + _provider = addAlbumStateNotifierProvider(widget.editAlbum); + _notifier = ref.read(_provider.notifier); + super.initState(); + } + + void _observeError(BuildContext context) { + ref.listen( + _provider.select( + (value) => value.error, + ), (previous, error) { + if (error != null) { + showErrorSnackBar(context: context, error: error); + } + }); + } + + void _observeSucceed(BuildContext context) { + ref.listen( + _provider.select( + (value) => value.succeed, + ), (previous, success) { + if (success) { + context.pop(true); + } + }); + } + + @override + Widget build(BuildContext context) { + _observeError(context); + _observeSucceed(context); + final state = ref.watch(_provider); + return AppPage( + resizeToAvoidBottomInset: false, + title: context.l10n.add_album_screen_title, + body: _body(context: context, state: state), + actions: [ + ActionButton( + onPressed: state.allowSave ? _notifier.createAlbum : null, + icon: Icon( + Icons.check, + size: 24, + color: state.allowSave + ? context.colorScheme.textPrimary + : context.colorScheme.textDisabled, + ), + ), + ], + ); + } + + Widget _body({required BuildContext context, required AddAlbumsState state}) { + return ListView( + children: [ + const SizedBox(height: 16), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: AppTextField( + controller: state.albumNameController, + onChanged: _notifier.validateAlbumName, + label: context.l10n.album_tame_field_title, + ), + ), + if ((state.googleAccount != null || state.dropboxAccount != null) && + widget.editAlbum == null) ...[ + const SizedBox(height: 40), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Text( + context.l10n.store_in_title, + style: AppTextStyles.subtitle1.copyWith( + color: context.colorScheme.textPrimary, + ), + ), + ), + const SizedBox(height: 8), + Column( + children: [ + RadioSelectionButton( + value: AppMediaSource.local, + groupValue: state.mediaSource, + onTab: () => _notifier.onSourceChange(AppMediaSource.local), + label: context.l10n.store_in_device_title, + ), + if (state.googleAccount != null) + RadioSelectionButton( + value: AppMediaSource.googleDrive, + groupValue: state.mediaSource, + onTab: () => + _notifier.onSourceChange(AppMediaSource.googleDrive), + label: context.l10n.common_google_drive, + ), + if (state.dropboxAccount != null) + RadioSelectionButton( + value: AppMediaSource.dropbox, + groupValue: state.mediaSource, + onTab: () => _notifier.onSourceChange(AppMediaSource.dropbox), + label: context.l10n.common_dropbox, + ), + ], + ), + ], + ], + ); + } +} diff --git a/app/lib/ui/flow/albums/add/add_album_state_notifier.dart b/app/lib/ui/flow/albums/add/add_album_state_notifier.dart index 917d8c84..51d5730f 100644 --- a/app/lib/ui/flow/albums/add/add_album_state_notifier.dart +++ b/app/lib/ui/flow/albums/add/add_album_state_notifier.dart @@ -1,21 +1,70 @@ +import 'package:data/errors/app_error.dart'; +import 'package:data/log/logger.dart'; +import 'package:data/models/album/album.dart'; import 'package:data/models/dropbox/account/dropbox_account.dart'; import 'package:data/models/media/media.dart'; +import 'package:data/services/auth_service.dart'; +import 'package:data/services/dropbox_services.dart'; +import 'package:data/services/google_drive_service.dart'; import 'package:data/services/local_media_service.dart'; +import 'package:data/storage/app_preferences.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:google_sign_in/google_sign_in.dart'; +import 'package:logger/logger.dart'; +import 'package:data/handlers/unique_id_generator.dart'; + part 'add_album_state_notifier.freezed.dart'; +final addAlbumStateNotifierProvider = StateNotifierProvider.autoDispose + .family((ref, state) { + final notifier = AddAlbumStateNotifier( + ref.read(googleUserAccountProvider), + ref.read(AppPreferences.dropboxCurrentUserAccount), + ref.read(localMediaServiceProvider), + ref.read(googleDriveServiceProvider), + ref.read(dropboxServiceProvider), + ref.read(uniqueIdGeneratorProvider), + ref.read(loggerProvider), + state, + ); + ref.listen( + googleUserAccountProvider, + (_, googleAccount) => notifier.onGoogleDriveAccountChange(googleAccount), + ); + ref.listen( + AppPreferences.dropboxCurrentUserAccount, + (_, dropboxAccount) => notifier.onDropboxAccountChange(dropboxAccount), + ); + return notifier; +}); + class AddAlbumStateNotifier extends StateNotifier { final GoogleSignInAccount? googleAccount; final DropboxAccount? dropboxAccount; + final LocalMediaService _localMediaService; + final GoogleDriveService _googleDriveService; + final DropboxService _dropboxService; + final UniqueIdGenerator _uniqueIdGenerator; + final Logger _logger; + final Album? editAlbum; AddAlbumStateNotifier( - super.state, this.googleAccount, this.dropboxAccount, - ); + this._localMediaService, + this._googleDriveService, + this._dropboxService, + this._uniqueIdGenerator, + this._logger, + this.editAlbum, + ) : super( + AddAlbumsState( + albumNameController: TextEditingController(text: editAlbum?.name), + mediaSource: editAlbum?.source ?? AppMediaSource.local, + ), + ); void onGoogleDriveAccountChange(GoogleSignInAccount? googleAccount) { state = state.copyWith(googleAccount: googleAccount); @@ -25,10 +74,94 @@ class AddAlbumStateNotifier extends StateNotifier { state = state.copyWith(dropboxAccount: dropboxAccount); } - void createAlbum(){ - state = state.copyWith(loading: true); + void onSourceChange(AppMediaSource source) { + state = state.copyWith(mediaSource: source); + } + + Future createAlbum() async { + try { + state = state.copyWith(loading: true); + + if (state.mediaSource == AppMediaSource.local) { + if (editAlbum != null) { + await _localMediaService.updateAlbum( + editAlbum!.copyWith( + name: state.albumNameController.text.trim(), + ), + ); + } else { + await _localMediaService.createAlbum( + id: _uniqueIdGenerator.v4(), + name: state.albumNameController.text.trim(), + ); + } + } else if (state.mediaSource == AppMediaSource.googleDrive && + googleAccount != null) { + final backupFolderId = await _googleDriveService.getBackUpFolderId(); + if (backupFolderId == null) { + throw BackUpFolderNotFound(); + } + if (editAlbum != null) { + final album = editAlbum!.copyWith( + name: state.albumNameController.text.trim(), + ); + await _googleDriveService.updateAlbum( + folderId: backupFolderId, + album: album, + ); + } else { + final album = Album( + id: _uniqueIdGenerator.v4(), + name: state.albumNameController.text.trim(), + source: AppMediaSource.googleDrive, + created_at: DateTime.now(), + medias: [], + ); + await _googleDriveService.createAlbum( + folderId: backupFolderId, + newAlbum: album, + ); + } + } else if (state.mediaSource == AppMediaSource.dropbox) { + if (editAlbum != null) { + await _dropboxService.updateAlbum( + editAlbum!.copyWith( + name: state.albumNameController.text.trim(), + ), + ); + } else { + final album = Album( + id: _uniqueIdGenerator.v4(), + name: state.albumNameController.text.trim(), + source: AppMediaSource.dropbox, + created_at: DateTime.now(), + medias: [], + ); + await _dropboxService.createAlbum(album); + } + } + state = state.copyWith(loading: false, succeed: true); + } catch (e, s) { + state = state.copyWith(loading: false, error: e); + _logger.e( + 'AddAlbumStateNotifier: Error creating album', + error: e, + stackTrace: s, + ); + } + } + + void validateAlbumName(String _) { + state = state.copyWith( + allowSave: state.albumNameController.text.trim().isNotEmpty, + ); + } + @override + void dispose() { + state.albumNameController.dispose(); + super.dispose(); } } @@ -36,7 +169,9 @@ class AddAlbumStateNotifier extends StateNotifier { class AddAlbumsState with _$AddAlbumsState { const factory AddAlbumsState({ @Default(false) bool loading, - AppMediaSource? mediaSource, + @Default(false) bool succeed, + @Default(false) bool allowSave, + @Default(AppMediaSource.local) AppMediaSource mediaSource, required TextEditingController albumNameController, GoogleSignInAccount? googleAccount, DropboxAccount? dropboxAccount, diff --git a/app/lib/ui/flow/albums/add/add_album_state_notifier.freezed.dart b/app/lib/ui/flow/albums/add/add_album_state_notifier.freezed.dart index e2151b35..a7633dd6 100644 --- a/app/lib/ui/flow/albums/add/add_album_state_notifier.freezed.dart +++ b/app/lib/ui/flow/albums/add/add_album_state_notifier.freezed.dart @@ -17,7 +17,9 @@ final _privateConstructorUsedError = UnsupportedError( /// @nodoc mixin _$AddAlbumsState { bool get loading => throw _privateConstructorUsedError; - AppMediaSource? get mediaSource => throw _privateConstructorUsedError; + bool get succeed => throw _privateConstructorUsedError; + bool get allowSave => throw _privateConstructorUsedError; + AppMediaSource get mediaSource => throw _privateConstructorUsedError; TextEditingController get albumNameController => throw _privateConstructorUsedError; GoogleSignInAccount? get googleAccount => throw _privateConstructorUsedError; @@ -39,7 +41,9 @@ abstract class $AddAlbumsStateCopyWith<$Res> { @useResult $Res call( {bool loading, - AppMediaSource? mediaSource, + bool succeed, + bool allowSave, + AppMediaSource mediaSource, TextEditingController albumNameController, GoogleSignInAccount? googleAccount, DropboxAccount? dropboxAccount, @@ -64,7 +68,9 @@ class _$AddAlbumsStateCopyWithImpl<$Res, $Val extends AddAlbumsState> @override $Res call({ Object? loading = null, - Object? mediaSource = freezed, + Object? succeed = null, + Object? allowSave = null, + Object? mediaSource = null, Object? albumNameController = null, Object? googleAccount = freezed, Object? dropboxAccount = freezed, @@ -75,10 +81,18 @@ class _$AddAlbumsStateCopyWithImpl<$Res, $Val extends AddAlbumsState> ? _value.loading : loading // ignore: cast_nullable_to_non_nullable as bool, - mediaSource: freezed == mediaSource + succeed: null == succeed + ? _value.succeed + : succeed // ignore: cast_nullable_to_non_nullable + as bool, + allowSave: null == allowSave + ? _value.allowSave + : allowSave // ignore: cast_nullable_to_non_nullable + as bool, + mediaSource: null == mediaSource ? _value.mediaSource : mediaSource // ignore: cast_nullable_to_non_nullable - as AppMediaSource?, + as AppMediaSource, albumNameController: null == albumNameController ? _value.albumNameController : albumNameController // ignore: cast_nullable_to_non_nullable @@ -120,7 +134,9 @@ abstract class _$$AddAlbumsStateImplCopyWith<$Res> @useResult $Res call( {bool loading, - AppMediaSource? mediaSource, + bool succeed, + bool allowSave, + AppMediaSource mediaSource, TextEditingController albumNameController, GoogleSignInAccount? googleAccount, DropboxAccount? dropboxAccount, @@ -144,7 +160,9 @@ class __$$AddAlbumsStateImplCopyWithImpl<$Res> @override $Res call({ Object? loading = null, - Object? mediaSource = freezed, + Object? succeed = null, + Object? allowSave = null, + Object? mediaSource = null, Object? albumNameController = null, Object? googleAccount = freezed, Object? dropboxAccount = freezed, @@ -155,10 +173,18 @@ class __$$AddAlbumsStateImplCopyWithImpl<$Res> ? _value.loading : loading // ignore: cast_nullable_to_non_nullable as bool, - mediaSource: freezed == mediaSource + succeed: null == succeed + ? _value.succeed + : succeed // ignore: cast_nullable_to_non_nullable + as bool, + allowSave: null == allowSave + ? _value.allowSave + : allowSave // ignore: cast_nullable_to_non_nullable + as bool, + mediaSource: null == mediaSource ? _value.mediaSource : mediaSource // ignore: cast_nullable_to_non_nullable - as AppMediaSource?, + as AppMediaSource, albumNameController: null == albumNameController ? _value.albumNameController : albumNameController // ignore: cast_nullable_to_non_nullable @@ -181,7 +207,9 @@ class __$$AddAlbumsStateImplCopyWithImpl<$Res> class _$AddAlbumsStateImpl implements _AddAlbumsState { const _$AddAlbumsStateImpl( {this.loading = false, - this.mediaSource, + this.succeed = false, + this.allowSave = false, + this.mediaSource = AppMediaSource.local, required this.albumNameController, this.googleAccount, this.dropboxAccount, @@ -191,7 +219,14 @@ class _$AddAlbumsStateImpl implements _AddAlbumsState { @JsonKey() final bool loading; @override - final AppMediaSource? mediaSource; + @JsonKey() + final bool succeed; + @override + @JsonKey() + final bool allowSave; + @override + @JsonKey() + final AppMediaSource mediaSource; @override final TextEditingController albumNameController; @override @@ -203,7 +238,7 @@ class _$AddAlbumsStateImpl implements _AddAlbumsState { @override String toString() { - return 'AddAlbumsState(loading: $loading, mediaSource: $mediaSource, albumNameController: $albumNameController, googleAccount: $googleAccount, dropboxAccount: $dropboxAccount, error: $error)'; + return 'AddAlbumsState(loading: $loading, succeed: $succeed, allowSave: $allowSave, mediaSource: $mediaSource, albumNameController: $albumNameController, googleAccount: $googleAccount, dropboxAccount: $dropboxAccount, error: $error)'; } @override @@ -212,6 +247,9 @@ class _$AddAlbumsStateImpl implements _AddAlbumsState { (other.runtimeType == runtimeType && other is _$AddAlbumsStateImpl && (identical(other.loading, loading) || other.loading == loading) && + (identical(other.succeed, succeed) || other.succeed == succeed) && + (identical(other.allowSave, allowSave) || + other.allowSave == allowSave) && (identical(other.mediaSource, mediaSource) || other.mediaSource == mediaSource) && (identical(other.albumNameController, albumNameController) || @@ -227,6 +265,8 @@ class _$AddAlbumsStateImpl implements _AddAlbumsState { int get hashCode => Object.hash( runtimeType, loading, + succeed, + allowSave, mediaSource, albumNameController, googleAccount, @@ -246,7 +286,9 @@ class _$AddAlbumsStateImpl implements _AddAlbumsState { abstract class _AddAlbumsState implements AddAlbumsState { const factory _AddAlbumsState( {final bool loading, - final AppMediaSource? mediaSource, + final bool succeed, + final bool allowSave, + final AppMediaSource mediaSource, required final TextEditingController albumNameController, final GoogleSignInAccount? googleAccount, final DropboxAccount? dropboxAccount, @@ -255,7 +297,11 @@ abstract class _AddAlbumsState implements AddAlbumsState { @override bool get loading; @override - AppMediaSource? get mediaSource; + bool get succeed; + @override + bool get allowSave; + @override + AppMediaSource get mediaSource; @override TextEditingController get albumNameController; @override diff --git a/app/lib/ui/flow/albums/albums_screen.dart b/app/lib/ui/flow/albums/albums_screen.dart index e11082d8..e134770b 100644 --- a/app/lib/ui/flow/albums/albums_screen.dart +++ b/app/lib/ui/flow/albums/albums_screen.dart @@ -1,13 +1,24 @@ +import 'package:data/models/album/album.dart'; +import 'package:data/models/media/media.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; import 'package:style/animations/fade_in_switcher.dart'; +import 'package:style/animations/on_tap_scale.dart'; import 'package:style/buttons/action_button.dart'; import 'package:style/extensions/context_extensions.dart'; import 'package:style/indicators/circular_progress_indicator.dart'; +import 'package:style/text/app_text_style.dart'; +import '../../../components/action_sheet.dart'; import '../../../components/app_page.dart'; +import '../../../components/app_sheet.dart'; import '../../../components/error_screen.dart'; import '../../../components/place_holder_screen.dart'; +import '../../../components/snack_bar.dart'; +import '../../../components/thumbnail_builder.dart'; +import '../../../domain/extensions/context_extensions.dart'; +import '../../navigation/app_route.dart'; import 'albums_view_notifier.dart'; class AlbumsScreen extends ConsumerStatefulWidget { @@ -26,14 +37,29 @@ class _AlbumsScreenState extends ConsumerState { super.initState(); } + void _observeError(BuildContext context) { + ref.listen( + albumStateNotifierProvider.select( + (value) => value.actionError, + ), (previous, error) { + if (error != null) { + showErrorSnackBar(context: context, error: error); + } + }); + } + @override Widget build(BuildContext context) { + _observeError(context); return AppPage( - title: 'Albums', + title: context.l10n.album_screen_title, actions: [ ActionButton( - onPressed: () { - ///TODO:Create Album + onPressed: () async { + final res = await AddAlbumRoute().push(context); + if (res == true) { + _notifier.loadAlbums(); + } }, icon: Icon( Icons.add, @@ -55,29 +81,136 @@ class _AlbumsScreenState extends ConsumerState { error: state.error!, onRetryTap: _notifier.loadAlbums, ); - } else if (state.medias.isEmpty) { + } else if (state.albums.isEmpty) { return PlaceHolderScreen( icon: Icon( CupertinoIcons.folder, size: 100, color: context.colorScheme.containerNormalOnSurface, ), - title: 'Oops! No Albums Here!', - message: - "It seems like there are no albums to show right now. You can create a new one. We've got you covered!", + title: context.l10n.empty_album_title, + message: context.l10n.empty_album_message, ); } return GridView( - gridDelegate: - const SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 2), + padding: EdgeInsets.all(16), + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 2, + childAspectRatio: 0.9, + crossAxisSpacing: 16, + mainAxisSpacing: 16, + ), children: state.albums .map( - (album) => Container( - color: context.colorScheme.textSecondary, + (album) => AlbumItem( + album: album, + onTap: () { + AlbumMediaListRoute( + $extra: album, + ).push(context); + }, + onLongTap: () { + showAppSheet( + context: context, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + AppSheetAction( + title: context.l10n.common_edit, + onPressed: () async { + context.pop(); + final res = await AddAlbumRoute( + $extra: album, + ).push(context); + if (res == true) { + _notifier.loadAlbums(); + } + }, + icon: Icon( + Icons.edit_outlined, + size: 24, + color: context.colorScheme.textPrimary, + ), + ), + AppSheetAction( + title: context.l10n.common_delete, + onPressed: () { + context.pop(); + _notifier.deleteAlbum(album); + }, + icon: Icon( + CupertinoIcons.delete, + size: 24, + color: context.colorScheme.textPrimary, + ), + ), + ], + ), + ); + }, ), ) .toList(), ); } } + +class AlbumItem extends StatelessWidget { + final Album album; + final void Function() onTap; + final void Function() onLongTap; + + const AlbumItem({ + super.key, + required this.album, + required this.onTap, + required this.onLongTap, + }); + + @override + Widget build(BuildContext context) { + return OnTapScale( + onTap: onTap, + onLongTap: onLongTap, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: album.medias.isEmpty + ? Container( + width: double.infinity, + decoration: BoxDecoration( + color: context.colorScheme.containerLowOnSurface, + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: context.colorScheme.outline, + ), + ), + child: Icon( + CupertinoIcons.folder, + size: 80, + color: context.colorScheme.containerHighOnSurface, + ), + ) + : AppMediaImage( + media: AppMedia( + id: album.medias.first, + path: '', + type: AppMediaType.image, + sources: [album.source], + ), size: Size(300, 300), + ), + ), + const SizedBox(height: 10), + Text( + album.name, + style: AppTextStyles.subtitle1.copyWith( + color: context.colorScheme.textPrimary, + ), + ), + ], + ), + ); + } +} diff --git a/app/lib/ui/flow/albums/albums_view_notifier.dart b/app/lib/ui/flow/albums/albums_view_notifier.dart index 90680579..9ee5c922 100644 --- a/app/lib/ui/flow/albums/albums_view_notifier.dart +++ b/app/lib/ui/flow/albums/albums_view_notifier.dart @@ -1,3 +1,4 @@ +import 'package:data/errors/app_error.dart'; import 'package:data/log/logger.dart'; import 'package:data/models/album/album.dart'; import 'package:data/models/dropbox/account/dropbox_account.dart'; @@ -109,16 +110,47 @@ class AlbumStateNotifier extends StateNotifier { ); } } + + Future deleteAlbum(Album album) async { + try { + state = state.copyWith(actionError: null); + if (album.source == AppMediaSource.local) { + await _localMediaService.deleteAlbum(album.id); + } else if (album.source == AppMediaSource.googleDrive) { + _backupFolderId ??= await _googleDriveService.getBackUpFolderId(); + if (_backupFolderId == null) { + throw BackUpFolderNotFound(); + } + await _googleDriveService.removeAlbum( + folderId: _backupFolderId!, + id: album.id, + ); + } else if (album.source == AppMediaSource.dropbox) { + await _dropboxService.deleteAlbum(album.id); + } + state = state.copyWith( + albums: + state.albums.where((element) => element.id != album.id).toList(), + ); + } catch (e, s) { + state = state.copyWith(actionError: e); + _logger.e( + "AlbumStateNotifier: Error deleting album", + error: e, + stackTrace: s, + ); + } + } } @freezed class AlbumsState with _$AlbumsState { const factory AlbumsState({ @Default(false) bool loading, - @Default([]) List medias, @Default([]) List albums, GoogleSignInAccount? googleAccount, DropboxAccount? dropboxAccount, Object? error, + Object? actionError, }) = _AlbumsState; } diff --git a/app/lib/ui/flow/albums/albums_view_notifier.freezed.dart b/app/lib/ui/flow/albums/albums_view_notifier.freezed.dart index b5a0fe1a..215ac6f7 100644 --- a/app/lib/ui/flow/albums/albums_view_notifier.freezed.dart +++ b/app/lib/ui/flow/albums/albums_view_notifier.freezed.dart @@ -17,11 +17,11 @@ final _privateConstructorUsedError = UnsupportedError( /// @nodoc mixin _$AlbumsState { bool get loading => throw _privateConstructorUsedError; - List get medias => throw _privateConstructorUsedError; List get albums => throw _privateConstructorUsedError; GoogleSignInAccount? get googleAccount => throw _privateConstructorUsedError; DropboxAccount? get dropboxAccount => throw _privateConstructorUsedError; Object? get error => throw _privateConstructorUsedError; + Object? get actionError => throw _privateConstructorUsedError; /// Create a copy of AlbumsState /// with the given fields replaced by the non-null parameter values. @@ -38,11 +38,11 @@ abstract class $AlbumsStateCopyWith<$Res> { @useResult $Res call( {bool loading, - List medias, List albums, GoogleSignInAccount? googleAccount, DropboxAccount? dropboxAccount, - Object? error}); + Object? error, + Object? actionError}); $DropboxAccountCopyWith<$Res>? get dropboxAccount; } @@ -63,21 +63,17 @@ class _$AlbumsStateCopyWithImpl<$Res, $Val extends AlbumsState> @override $Res call({ Object? loading = null, - Object? medias = null, Object? albums = null, Object? googleAccount = freezed, Object? dropboxAccount = freezed, Object? error = freezed, + Object? actionError = freezed, }) { return _then(_value.copyWith( loading: null == loading ? _value.loading : loading // ignore: cast_nullable_to_non_nullable as bool, - medias: null == medias - ? _value.medias - : medias // ignore: cast_nullable_to_non_nullable - as List, albums: null == albums ? _value.albums : albums // ignore: cast_nullable_to_non_nullable @@ -91,6 +87,7 @@ class _$AlbumsStateCopyWithImpl<$Res, $Val extends AlbumsState> : dropboxAccount // ignore: cast_nullable_to_non_nullable as DropboxAccount?, error: freezed == error ? _value.error : error, + actionError: freezed == actionError ? _value.actionError : actionError, ) as $Val); } @@ -119,11 +116,11 @@ abstract class _$$AlbumsStateImplCopyWith<$Res> @useResult $Res call( {bool loading, - List medias, List albums, GoogleSignInAccount? googleAccount, DropboxAccount? dropboxAccount, - Object? error}); + Object? error, + Object? actionError}); @override $DropboxAccountCopyWith<$Res>? get dropboxAccount; @@ -143,21 +140,17 @@ class __$$AlbumsStateImplCopyWithImpl<$Res> @override $Res call({ Object? loading = null, - Object? medias = null, Object? albums = null, Object? googleAccount = freezed, Object? dropboxAccount = freezed, Object? error = freezed, + Object? actionError = freezed, }) { return _then(_$AlbumsStateImpl( loading: null == loading ? _value.loading : loading // ignore: cast_nullable_to_non_nullable as bool, - medias: null == medias - ? _value._medias - : medias // ignore: cast_nullable_to_non_nullable - as List, albums: null == albums ? _value._albums : albums // ignore: cast_nullable_to_non_nullable @@ -171,6 +164,7 @@ class __$$AlbumsStateImplCopyWithImpl<$Res> : dropboxAccount // ignore: cast_nullable_to_non_nullable as DropboxAccount?, error: freezed == error ? _value.error : error, + actionError: freezed == actionError ? _value.actionError : actionError, )); } } @@ -180,26 +174,16 @@ class __$$AlbumsStateImplCopyWithImpl<$Res> class _$AlbumsStateImpl implements _AlbumsState { const _$AlbumsStateImpl( {this.loading = false, - final List medias = const [], final List albums = const [], this.googleAccount, this.dropboxAccount, - this.error}) - : _medias = medias, - _albums = albums; + this.error, + this.actionError}) + : _albums = albums; @override @JsonKey() final bool loading; - final List _medias; - @override - @JsonKey() - List get medias { - if (_medias is EqualUnmodifiableListView) return _medias; - // ignore: implicit_dynamic_type - return EqualUnmodifiableListView(_medias); - } - final List _albums; @override @JsonKey() @@ -215,10 +199,12 @@ class _$AlbumsStateImpl implements _AlbumsState { final DropboxAccount? dropboxAccount; @override final Object? error; + @override + final Object? actionError; @override String toString() { - return 'AlbumsState(loading: $loading, medias: $medias, albums: $albums, googleAccount: $googleAccount, dropboxAccount: $dropboxAccount, error: $error)'; + return 'AlbumsState(loading: $loading, albums: $albums, googleAccount: $googleAccount, dropboxAccount: $dropboxAccount, error: $error, actionError: $actionError)'; } @override @@ -227,24 +213,25 @@ class _$AlbumsStateImpl implements _AlbumsState { (other.runtimeType == runtimeType && other is _$AlbumsStateImpl && (identical(other.loading, loading) || other.loading == loading) && - const DeepCollectionEquality().equals(other._medias, _medias) && const DeepCollectionEquality().equals(other._albums, _albums) && (identical(other.googleAccount, googleAccount) || other.googleAccount == googleAccount) && (identical(other.dropboxAccount, dropboxAccount) || other.dropboxAccount == dropboxAccount) && - const DeepCollectionEquality().equals(other.error, error)); + const DeepCollectionEquality().equals(other.error, error) && + const DeepCollectionEquality() + .equals(other.actionError, actionError)); } @override int get hashCode => Object.hash( runtimeType, loading, - const DeepCollectionEquality().hash(_medias), const DeepCollectionEquality().hash(_albums), googleAccount, dropboxAccount, - const DeepCollectionEquality().hash(error)); + const DeepCollectionEquality().hash(error), + const DeepCollectionEquality().hash(actionError)); /// Create a copy of AlbumsState /// with the given fields replaced by the non-null parameter values. @@ -258,17 +245,15 @@ class _$AlbumsStateImpl implements _AlbumsState { abstract class _AlbumsState implements AlbumsState { const factory _AlbumsState( {final bool loading, - final List medias, final List albums, final GoogleSignInAccount? googleAccount, final DropboxAccount? dropboxAccount, - final Object? error}) = _$AlbumsStateImpl; + final Object? error, + final Object? actionError}) = _$AlbumsStateImpl; @override bool get loading; @override - List get medias; - @override List get albums; @override GoogleSignInAccount? get googleAccount; @@ -276,6 +261,8 @@ abstract class _AlbumsState implements AlbumsState { DropboxAccount? get dropboxAccount; @override Object? get error; + @override + Object? get actionError; /// Create a copy of AlbumsState /// with the given fields replaced by the non-null parameter values. diff --git a/app/lib/ui/flow/albums/media_list/album_media_list_screen.dart b/app/lib/ui/flow/albums/media_list/album_media_list_screen.dart new file mode 100644 index 00000000..75fa6b87 --- /dev/null +++ b/app/lib/ui/flow/albums/media_list/album_media_list_screen.dart @@ -0,0 +1,154 @@ +import 'package:data/models/album/album.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:go_router/go_router.dart'; +import 'package:style/animations/fade_in_switcher.dart'; +import 'package:style/buttons/action_button.dart'; +import 'package:style/extensions/context_extensions.dart'; +import 'package:style/indicators/circular_progress_indicator.dart'; +import '../../../../components/action_sheet.dart'; +import '../../../../components/app_media_thumbnail.dart'; +import '../../../../components/app_page.dart'; +import '../../../../components/app_sheet.dart'; +import '../../../../components/error_screen.dart'; +import '../../../../components/place_holder_screen.dart'; +import '../../../../domain/extensions/context_extensions.dart'; +import '../../../../gen/assets.gen.dart'; +import '../../../navigation/app_route.dart'; +import 'album_media_list_state_notifier.dart'; + +class AlbumMediaListScreen extends ConsumerStatefulWidget { + final Album album; + + const AlbumMediaListScreen({super.key, required this.album}); + + @override + ConsumerState createState() => + _AlbumMediaListScreenState(); +} + +class _AlbumMediaListScreenState extends ConsumerState { + late AutoDisposeStateNotifierProvider _provider; + late AlbumMediaListStateNotifier _notifier; + + @override + void initState() { + _provider = albumMediaListStateNotifierProvider(widget.album); + _notifier = ref.read(_provider.notifier); + super.initState(); + } + + @override + Widget build(BuildContext context) { + final state = ref.watch(_provider); + return AppPage( + title: state.album.name, + actions: [ + ActionButton( + onPressed: () async { + showAppSheet( + context: context, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + AppSheetAction( + title: context.l10n.add_media_title, + onPressed: () async { + context.pop(); + final res = + await MediaSelectionRoute($extra: widget.album.source) + .push(context); + + if (res != null && res is List) { + await _notifier.addMedias(res); + } + }, + icon: Icon( + Icons.add, + size: 24, + color: context.colorScheme.textPrimary, + ), + ), + AppSheetAction( + title: context.l10n.common_edit, + onPressed: () async { + context.pop(); + final res = await AddAlbumRoute($extra: widget.album) + .push(context); + if (res == true) { + await _notifier.reloadAlbum(); + } + }, + icon: Icon( + Icons.edit_outlined, + size: 24, + color: context.colorScheme.textPrimary, + ), + ), + AppSheetAction( + title: context.l10n.common_delete, + onPressed: () async { + context.pop(); + await _notifier.deleteAlbum(); + }, + icon: Icon( + CupertinoIcons.delete, + size: 24, + color: context.colorScheme.textPrimary, + ), + ), + ], + ), + ); + }, + icon: Icon( + Icons.more_vert_rounded, + color: context.colorScheme.textPrimary, + size: 24, + ), + ), + ], + body: FadeInSwitcher(child: _body(context: context, state: state)), + ); + } + + Widget _body({ + required BuildContext context, + required AlbumMediaListState state, + }) { + if (state.loading && state.medias.isEmpty) { + return const Center(child: AppCircularProgressIndicator()); + } else if (state.error != null) { + return ErrorScreen( + error: state.error!, + onRetryTap: () => _notifier.loadMedia(reload: true), + ); + } else if (state.medias.isEmpty) { + return PlaceHolderScreen( + icon: SvgPicture.asset( + Assets.images.ilNoMediaFound, + width: 150, + ), + title: context.l10n.empty_media_title, + message: context.l10n.empty_media_message, + ); + } + return GridView.builder( + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: context.mediaQuerySize.width > 600 + ? context.mediaQuerySize.width ~/ 180 + : context.mediaQuerySize.width ~/ 100, + crossAxisSpacing: 4, + mainAxisSpacing: 4, + ), + itemCount: state.medias.length, + itemBuilder: (context, index) => AppMediaThumbnail( + heroTag: "album_media_list", + media: state.medias[index], + ), + ); + } +} diff --git a/app/lib/ui/flow/albums/media_list/album_media_list_state_notifier.dart b/app/lib/ui/flow/albums/media_list/album_media_list_state_notifier.dart new file mode 100644 index 00000000..ea8f614d --- /dev/null +++ b/app/lib/ui/flow/albums/media_list/album_media_list_state_notifier.dart @@ -0,0 +1,248 @@ +import 'package:collection/collection.dart'; +import 'package:data/errors/app_error.dart'; +import 'package:data/log/logger.dart'; +import 'package:data/models/album/album.dart'; +import 'package:data/models/media/media.dart'; +import 'package:data/services/dropbox_services.dart'; +import 'package:data/services/google_drive_service.dart'; +import 'package:data/services/local_media_service.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:logger/logger.dart'; + +part 'album_media_list_state_notifier.freezed.dart'; + +final albumMediaListStateNotifierProvider = StateNotifierProvider.autoDispose + .family( + (ref, state) => AlbumMediaListStateNotifier( + state, + ref.read(localMediaServiceProvider), + ref.read(googleDriveServiceProvider), + ref.read(dropboxServiceProvider), + ref.read(loggerProvider), + ), +); + +class AlbumMediaListStateNotifier extends StateNotifier { + final LocalMediaService _localMediaService; + final GoogleDriveService _googleDriveService; + final DropboxService _dropboxService; + final Logger _logger; + + AlbumMediaListStateNotifier( + Album album, + this._localMediaService, + this._googleDriveService, + this._dropboxService, + this._logger, + ) : super( + AlbumMediaListState(album: album), + ) { + loadMedia(); + } + + String? _backupFolderId; + + Future loadAlbum() async { + if (state.loading) return; + + state = state.copyWith(actionError: null); + List albums = []; + try { + if (state.album.source == AppMediaSource.googleDrive) { + _backupFolderId ??= await _googleDriveService.getBackUpFolderId(); + if (_backupFolderId == null) { + throw BackUpFolderNotFound(); + } + albums = + await _googleDriveService.getAlbums(folderId: _backupFolderId!); + } else if (state.album.source == AppMediaSource.dropbox) { + albums = await _dropboxService.getAlbums(); + } else { + albums = await _localMediaService.getAlbums(); + } + + state = state.copyWith( + album: albums + .firstWhereOrNull((element) => element.id == state.album.id) ?? + state.album, + ); + } catch (e, s) { + state = state.copyWith(actionError: e); + _logger.e( + "AlbumMediaListStateNotifier: Error loading albums", + error: e, + stackTrace: s, + ); + } + } + + Future reloadAlbum() async { + await loadAlbum(); + await loadMedia(reload: true); + } + + Future deleteAlbum() async { + try { + state = state.copyWith(actionError: null); + if (state.album.source == AppMediaSource.local) { + await _localMediaService.deleteAlbum(state.album.id); + } else if (state.album.source == AppMediaSource.googleDrive) { + _backupFolderId ??= await _googleDriveService.getBackUpFolderId(); + if (_backupFolderId == null) { + throw BackUpFolderNotFound(); + } + await _googleDriveService.removeAlbum( + folderId: _backupFolderId!, + id: state.album.id, + ); + } else if (state.album.source == AppMediaSource.dropbox) { + await _dropboxService.deleteAlbum(state.album.id); + } + state = state.copyWith( + deleteAlbumSuccess: true, + ); + } catch (e, s) { + state = state.copyWith(actionError: e); + _logger.e( + "AlbumMediaListStateNotifier: Error deleting album", + error: e, + stackTrace: s, + ); + } + } + + Future addMedias(List medias) async { + try { + state = state.copyWith(actionError: null); + if (state.album.source == AppMediaSource.local) { + await _localMediaService.updateAlbum( + state.album.copyWith( + medias: [ + ...state.album.medias, + ...medias, + ], + ), + ); + } else if (state.album.source == AppMediaSource.googleDrive) { + _backupFolderId ??= await _googleDriveService.getBackUpFolderId(); + if (_backupFolderId == null) { + throw BackUpFolderNotFound(); + } + await _googleDriveService.updateAlbum( + folderId: _backupFolderId!, + album: state.album.copyWith( + medias: [ + ...state.album.medias, + ...medias, + ], + ), + ); + } else if (state.album.source == AppMediaSource.dropbox) { + await _dropboxService.updateAlbum( + state.album.copyWith( + medias: [ + ...state.album.medias, + ...medias, + ], + ), + ); + } + reloadAlbum(); + } catch (e, s) { + state = state.copyWith(actionError: e); + _logger.e( + "AlbumMediaListStateNotifier: Error adding media", + error: e, + stackTrace: s, + ); + } + } + + Future loadMedia({bool reload = false}) async { + try { + if (state.loading) return; + + if (reload) { + state = state.copyWith(medias: []); + } + + state = state.copyWith(loading: true, error: null, actionError: null); + + List medias = []; + + if (state.album.source == AppMediaSource.local) { + final loadedMediaIds = state.medias.map((e) => e.id).toList(); + final moreMediaIds = state.album.medias + .where((element) => !loadedMediaIds.contains(element)) + .toList(); + + medias = await Future.wait( + moreMediaIds + .take(moreMediaIds.length > 30 ? 30 : moreMediaIds.length) + .map( + (id) => _localMediaService.getMedia(id: id), + ), + ).then( + (value) => value.nonNulls.toList(), + ); + } else if (state.album.source == AppMediaSource.googleDrive) { + final loadedMediaIds = + state.medias.map((e) => e.driveMediaRefId).nonNulls.toList(); + final moreMediaIds = state.album.medias + .where((element) => !loadedMediaIds.contains(element)) + .toList(); + medias = await Future.wait( + moreMediaIds + .take(moreMediaIds.length > 30 ? 30 : moreMediaIds.length) + .map( + (id) => _googleDriveService.getMedia(id: id), + ), + ).then( + (value) => value.nonNulls.toList(), + ); + } else if (state.album.source == AppMediaSource.dropbox) { + final loadedMediaIds = + state.medias.map((e) => e.dropboxMediaRefId).nonNulls.toList(); + final moreMediaIds = state.album.medias + .where((element) => !loadedMediaIds.contains(element)) + .toList(); + medias = await Future.wait( + moreMediaIds + .take(moreMediaIds.length > 30 ? 30 : moreMediaIds.length) + .map( + (id) => _dropboxService.getMedia(id: id), + ), + ).then( + (value) => value.nonNulls.toList(), + ); + } + + state = + state.copyWith(medias: [...state.medias, ...medias], loading: false); + } catch (e, s) { + state = state.copyWith( + loading: false, + error: state.medias.isEmpty ? e : null, + actionError: state.medias.isNotEmpty ? e : null, + ); + _logger.e( + "AlbumMediaListStateNotifier: Error loading medias", + error: e, + stackTrace: s, + ); + } + } +} + +@freezed +class AlbumMediaListState with _$AlbumMediaListState { + const factory AlbumMediaListState({ + @Default([]) List medias, + required Album album, + @Default(false) bool loading, + @Default(false) bool deleteAlbumSuccess, + Object? error, + Object? actionError, + }) = _AlbumMediaListState; +} diff --git a/app/lib/ui/flow/albums/media_list/album_media_list_state_notifier.freezed.dart b/app/lib/ui/flow/albums/media_list/album_media_list_state_notifier.freezed.dart new file mode 100644 index 00000000..7e4aee26 --- /dev/null +++ b/app/lib/ui/flow/albums/media_list/album_media_list_state_notifier.freezed.dart @@ -0,0 +1,270 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'album_media_list_state_notifier.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); + +/// @nodoc +mixin _$AlbumMediaListState { + List get medias => throw _privateConstructorUsedError; + Album get album => throw _privateConstructorUsedError; + bool get loading => throw _privateConstructorUsedError; + bool get deleteAlbumSuccess => throw _privateConstructorUsedError; + Object? get error => throw _privateConstructorUsedError; + Object? get actionError => throw _privateConstructorUsedError; + + /// Create a copy of AlbumMediaListState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $AlbumMediaListStateCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $AlbumMediaListStateCopyWith<$Res> { + factory $AlbumMediaListStateCopyWith( + AlbumMediaListState value, $Res Function(AlbumMediaListState) then) = + _$AlbumMediaListStateCopyWithImpl<$Res, AlbumMediaListState>; + @useResult + $Res call( + {List medias, + Album album, + bool loading, + bool deleteAlbumSuccess, + Object? error, + Object? actionError}); + + $AlbumCopyWith<$Res> get album; +} + +/// @nodoc +class _$AlbumMediaListStateCopyWithImpl<$Res, $Val extends AlbumMediaListState> + implements $AlbumMediaListStateCopyWith<$Res> { + _$AlbumMediaListStateCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of AlbumMediaListState + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? medias = null, + Object? album = null, + Object? loading = null, + Object? deleteAlbumSuccess = null, + Object? error = freezed, + Object? actionError = freezed, + }) { + return _then(_value.copyWith( + medias: null == medias + ? _value.medias + : medias // ignore: cast_nullable_to_non_nullable + as List, + album: null == album + ? _value.album + : album // ignore: cast_nullable_to_non_nullable + as Album, + loading: null == loading + ? _value.loading + : loading // ignore: cast_nullable_to_non_nullable + as bool, + deleteAlbumSuccess: null == deleteAlbumSuccess + ? _value.deleteAlbumSuccess + : deleteAlbumSuccess // ignore: cast_nullable_to_non_nullable + as bool, + error: freezed == error ? _value.error : error, + actionError: freezed == actionError ? _value.actionError : actionError, + ) as $Val); + } + + /// Create a copy of AlbumMediaListState + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $AlbumCopyWith<$Res> get album { + return $AlbumCopyWith<$Res>(_value.album, (value) { + return _then(_value.copyWith(album: value) as $Val); + }); + } +} + +/// @nodoc +abstract class _$$AlbumMediaListStateImplCopyWith<$Res> + implements $AlbumMediaListStateCopyWith<$Res> { + factory _$$AlbumMediaListStateImplCopyWith(_$AlbumMediaListStateImpl value, + $Res Function(_$AlbumMediaListStateImpl) then) = + __$$AlbumMediaListStateImplCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {List medias, + Album album, + bool loading, + bool deleteAlbumSuccess, + Object? error, + Object? actionError}); + + @override + $AlbumCopyWith<$Res> get album; +} + +/// @nodoc +class __$$AlbumMediaListStateImplCopyWithImpl<$Res> + extends _$AlbumMediaListStateCopyWithImpl<$Res, _$AlbumMediaListStateImpl> + implements _$$AlbumMediaListStateImplCopyWith<$Res> { + __$$AlbumMediaListStateImplCopyWithImpl(_$AlbumMediaListStateImpl _value, + $Res Function(_$AlbumMediaListStateImpl) _then) + : super(_value, _then); + + /// Create a copy of AlbumMediaListState + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? medias = null, + Object? album = null, + Object? loading = null, + Object? deleteAlbumSuccess = null, + Object? error = freezed, + Object? actionError = freezed, + }) { + return _then(_$AlbumMediaListStateImpl( + medias: null == medias + ? _value._medias + : medias // ignore: cast_nullable_to_non_nullable + as List, + album: null == album + ? _value.album + : album // ignore: cast_nullable_to_non_nullable + as Album, + loading: null == loading + ? _value.loading + : loading // ignore: cast_nullable_to_non_nullable + as bool, + deleteAlbumSuccess: null == deleteAlbumSuccess + ? _value.deleteAlbumSuccess + : deleteAlbumSuccess // ignore: cast_nullable_to_non_nullable + as bool, + error: freezed == error ? _value.error : error, + actionError: freezed == actionError ? _value.actionError : actionError, + )); + } +} + +/// @nodoc + +class _$AlbumMediaListStateImpl implements _AlbumMediaListState { + const _$AlbumMediaListStateImpl( + {final List medias = const [], + required this.album, + this.loading = false, + this.deleteAlbumSuccess = false, + this.error, + this.actionError}) + : _medias = medias; + + final List _medias; + @override + @JsonKey() + List get medias { + if (_medias is EqualUnmodifiableListView) return _medias; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_medias); + } + + @override + final Album album; + @override + @JsonKey() + final bool loading; + @override + @JsonKey() + final bool deleteAlbumSuccess; + @override + final Object? error; + @override + final Object? actionError; + + @override + String toString() { + return 'AlbumMediaListState(medias: $medias, album: $album, loading: $loading, deleteAlbumSuccess: $deleteAlbumSuccess, error: $error, actionError: $actionError)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$AlbumMediaListStateImpl && + const DeepCollectionEquality().equals(other._medias, _medias) && + (identical(other.album, album) || other.album == album) && + (identical(other.loading, loading) || other.loading == loading) && + (identical(other.deleteAlbumSuccess, deleteAlbumSuccess) || + other.deleteAlbumSuccess == deleteAlbumSuccess) && + const DeepCollectionEquality().equals(other.error, error) && + const DeepCollectionEquality() + .equals(other.actionError, actionError)); + } + + @override + int get hashCode => Object.hash( + runtimeType, + const DeepCollectionEquality().hash(_medias), + album, + loading, + deleteAlbumSuccess, + const DeepCollectionEquality().hash(error), + const DeepCollectionEquality().hash(actionError)); + + /// Create a copy of AlbumMediaListState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$AlbumMediaListStateImplCopyWith<_$AlbumMediaListStateImpl> get copyWith => + __$$AlbumMediaListStateImplCopyWithImpl<_$AlbumMediaListStateImpl>( + this, _$identity); +} + +abstract class _AlbumMediaListState implements AlbumMediaListState { + const factory _AlbumMediaListState( + {final List medias, + required final Album album, + final bool loading, + final bool deleteAlbumSuccess, + final Object? error, + final Object? actionError}) = _$AlbumMediaListStateImpl; + + @override + List get medias; + @override + Album get album; + @override + bool get loading; + @override + bool get deleteAlbumSuccess; + @override + Object? get error; + @override + Object? get actionError; + + /// Create a copy of AlbumMediaListState + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$AlbumMediaListStateImplCopyWith<_$AlbumMediaListStateImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/app/lib/ui/flow/main/main_screen.dart b/app/lib/ui/flow/main/main_screen.dart index de7a6906..688fab5e 100644 --- a/app/lib/ui/flow/main/main_screen.dart +++ b/app/lib/ui/flow/main/main_screen.dart @@ -25,7 +25,7 @@ class _MainScreenState extends State { ( icon: CupertinoIcons.house_fill, label: "Home", - activeIcon: CupertinoIcons.house_fill, + activeIcon: CupertinoIcons.house_fill, ), ( icon: CupertinoIcons.folder, @@ -44,91 +44,86 @@ class _MainScreenState extends State { ), ]; - if (!kIsWeb && Platform.isIOS) { - return CupertinoTabScaffold( - key: ValueKey(widget.navigationShell.currentIndex), - tabBar: CupertinoTabBar( - currentIndex: widget.navigationShell.currentIndex, - activeColor: context.colorScheme.primary, - inactiveColor: context.colorScheme.textDisabled, - onTap: (index) => _goBranch( - index: index, - context: context, - ), - border: Border( - top: BorderSide( - color: context.colorScheme.outline, - width: 1, - ), - ), - items: tabs - .map( - (e) => BottomNavigationBarItem( - icon: Icon( - e.icon, - color: context.colorScheme.textDisabled, - size: 22, - ), - label: e.label, - activeIcon: Icon( - e.activeIcon, - color: context.colorScheme.primary, - size: 24, + return Column( + children: [ + Expanded(child: widget.navigationShell), + (!kIsWeb && Platform.isIOS) + ? CupertinoTabBar( + currentIndex: widget.navigationShell.currentIndex, + activeColor: context.colorScheme.primary, + inactiveColor: context.colorScheme.textDisabled, + onTap: (index) => _goBranch( + index: index, + context: context, + ), + backgroundColor: context.colorScheme.surface, + border: Border( + top: BorderSide( + color: context.colorScheme.outline, + width: 1, ), ), + items: tabs + .map( + (e) => BottomNavigationBarItem( + icon: Icon( + e.icon, + color: context.colorScheme.textDisabled, + size: 22, + ), + label: e.label, + activeIcon: Icon( + e.activeIcon, + color: context.colorScheme.primary, + size: 24, + ), + ), + ) + .toList(), ) - .toList(), - ), - tabBuilder: (context, index) { - return widget.navigationShell; - }, - ); - } else { - return Scaffold( - body: widget.navigationShell, - bottomNavigationBar: Container( - decoration: BoxDecoration( - border: Border( - top: BorderSide( - color: context.colorScheme.outline, - width: 1, - ), - ), - ), - child: BottomNavigationBar( - items: tabs - .map( - (e) => BottomNavigationBarItem( - icon: Icon( - e.icon, - color: context.colorScheme.textDisabled, - size: 24, - ), - label: e.label, - activeIcon: Icon( - e.activeIcon, - color: context.colorScheme.primary, - size: 24, + : Container( + decoration: BoxDecoration( + border: Border( + top: BorderSide( + color: context.colorScheme.outline, + width: 1, ), ), - ) - .toList(), - currentIndex: widget.navigationShell.currentIndex, - selectedItemColor: context.colorScheme.primary, - unselectedItemColor: context.colorScheme.textDisabled, - backgroundColor: context.colorScheme.surface, - type: BottomNavigationBarType.fixed, - selectedFontSize: 12, - unselectedFontSize: 12, - elevation: 0, - onTap: (index) => _goBranch( - index: index, - context: context, - ), - ), - ), - ); - } + ), + child: BottomNavigationBar( + items: tabs + .map( + (e) => BottomNavigationBarItem( + icon: Icon( + e.icon, + color: context.colorScheme.textDisabled, + size: 24, + ), + label: e.label, + activeIcon: Icon( + e.activeIcon, + color: context.colorScheme.primary, + size: 24, + ), + ), + ) + .toList(), + currentIndex: widget.navigationShell.currentIndex, + selectedItemColor: context.colorScheme.primary, + unselectedItemColor: context.colorScheme.textDisabled, + backgroundColor: context.colorScheme.surface, + type: BottomNavigationBarType.fixed, + selectedFontSize: 12, + unselectedFontSize: 12, + elevation: 0, + onTap: (index) => _goBranch( + index: index, + context: context, + ), + ), + ), + ], + ); } void _goBranch({ diff --git a/app/lib/ui/flow/media_preview/components/video_player_components/video_duration_slider.dart b/app/lib/ui/flow/media_preview/components/video_player_components/video_duration_slider.dart index 54b043f6..bccb9b19 100644 --- a/app/lib/ui/flow/media_preview/components/video_player_components/video_duration_slider.dart +++ b/app/lib/ui/flow/media_preview/components/video_player_components/video_duration_slider.dart @@ -1,4 +1,3 @@ -import 'dart:ui'; import '../../../../../domain/formatter/duration_formatter.dart'; import 'package:flutter/material.dart'; import 'package:style/animations/cross_fade_animation.dart'; @@ -34,52 +33,49 @@ class VideoDurationSlider extends StatelessWidget { left: 16, right: 16, ), - color: context.colorScheme.barColor, - child: BackdropFilter( - filter: ImageFilter.blur(sigmaX: 5, sigmaY: 5), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - position.format, - style: AppTextStyles.caption - .copyWith(color: context.colorScheme.textPrimary), - ), - Expanded( - child: SizedBox( - height: 30, - child: Material( - color: Colors.transparent, - child: SliderTheme( - data: SliderTheme.of(context).copyWith( - trackHeight: 4, - trackShape: const RoundedRectSliderTrackShape(), - rangeTrackShape: - const RoundedRectRangeSliderTrackShape(), - thumbShape: SliderComponentShape.noThumb, - ), - child: Slider( - value: position.inSeconds.toDouble(), - max: duration.inSeconds.toDouble(), - min: 0, - activeColor: context.colorScheme.primary, - inactiveColor: context.colorScheme.outline, - onChangeEnd: (value) => onChangeEnd - .call(Duration(seconds: value.toInt())), - onChanged: (double value) => - onChanged.call(Duration(seconds: value.toInt())), - ), + color: context.colorScheme.surface, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + position.format, + style: AppTextStyles.caption + .copyWith(color: context.colorScheme.textPrimary), + ), + Expanded( + child: SizedBox( + height: 30, + child: Material( + color: Colors.transparent, + child: SliderTheme( + data: SliderTheme.of(context).copyWith( + trackHeight: 4, + trackShape: const RoundedRectSliderTrackShape(), + rangeTrackShape: + const RoundedRectRangeSliderTrackShape(), + thumbShape: SliderComponentShape.noThumb, + ), + child: Slider( + value: position.inSeconds.toDouble(), + max: duration.inSeconds.toDouble(), + min: 0, + activeColor: context.colorScheme.primary, + inactiveColor: context.colorScheme.outline, + onChangeEnd: (value) => + onChangeEnd.call(Duration(seconds: value.toInt())), + onChanged: (double value) => + onChanged.call(Duration(seconds: value.toInt())), ), ), ), ), - Text( - duration.format, - style: AppTextStyles.caption - .copyWith(color: context.colorScheme.textPrimary), - ), - ], - ), + ), + Text( + duration.format, + style: AppTextStyles.caption + .copyWith(color: context.colorScheme.textPrimary), + ), + ], ), ), ), diff --git a/app/lib/ui/flow/media_selection/media_selection_screen.dart b/app/lib/ui/flow/media_selection/media_selection_screen.dart new file mode 100644 index 00000000..c9a47eac --- /dev/null +++ b/app/lib/ui/flow/media_selection/media_selection_screen.dart @@ -0,0 +1,205 @@ +import 'package:data/models/media/media.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:go_router/go_router.dart'; +import 'package:style/animations/fade_in_switcher.dart'; +import 'package:style/buttons/action_button.dart'; +import 'package:style/extensions/context_extensions.dart'; +import 'package:style/indicators/circular_progress_indicator.dart'; +import 'package:style/text/app_text_style.dart'; +import '../../../components/app_media_thumbnail.dart'; +import '../../../components/app_page.dart'; +import '../../../components/error_screen.dart'; +import '../../../components/place_holder_screen.dart'; +import '../../../components/snack_bar.dart'; +import '../../../domain/extensions/context_extensions.dart'; +import '../../../domain/extensions/widget_extensions.dart'; +import '../../../domain/formatter/date_formatter.dart'; +import '../../../gen/assets.gen.dart'; +import '../home/components/no_local_medias_access_screen.dart'; +import 'media_selection_state_notifier.dart'; +import 'package:style/callback/on_visible_callback.dart'; + +class MediaSelectionScreen extends ConsumerStatefulWidget { + final AppMediaSource source; + + const MediaSelectionScreen({super.key, required this.source}); + + @override + ConsumerState createState() => _MediaSelectionScreenState(); +} + +class _MediaSelectionScreenState extends ConsumerState { + late AutoDisposeStateNotifierProvider _provider; + late MediaSelectionStateNotifier _notifier; + + void _observeError(BuildContext context) { + ref.listen( + _provider.select( + (value) => value.actionError, + ), + (previous, error) { + if (error != null) { + showErrorSnackBar(context: context, error: error); + } + }, + ); + } + + @override + void initState() { + _provider = mediaSelectionStateNotifierProvider(widget.source); + _notifier = ref.read(_provider.notifier); + super.initState(); + } + + @override + Widget build(BuildContext context) { + _observeError(context); + final state = ref.watch(_provider); + return AppPage( + title: widget.source == AppMediaSource.googleDrive + ? context.l10n.select_from_google_drive_title + : widget.source == AppMediaSource.dropbox + ? context.l10n.select_from_dropbox_title + : context.l10n.select_from_device_title, + actions: [ + ActionButton( + onPressed: () { + context.pop(state.selectedMedias); + }, + icon: Icon( + Icons.check, + color: context.colorScheme.textPrimary, + size: 24, + ), + ), + ], + body: Builder( + builder: (context) { + return FadeInSwitcher(child: _body(context: context, state: state)); + }, + ), + ); + } + + Widget _body({ + required BuildContext context, + required MediaSelectionState state, + }) { + if (state.loading && state.medias.isEmpty) { + return const Center(child: AppCircularProgressIndicator()); + } else if (state.error != null) { + return ErrorScreen( + error: state.error!, + onRetryTap: () => _notifier.loadMedias(reload: true), + ); + } else if (state.medias.isEmpty && state.noAccess) { + return widget.source == AppMediaSource.local + ? NoLocalMediasAccessScreen() + : PlaceHolderScreen( + icon: SvgPicture.asset( + Assets.images.ilNoMediaFound, + width: 150, + ), + title: context.l10n.no_media_access_title, + message: context.l10n.no_cloud_media_access_message, + ); + } else if (state.medias.isEmpty && !state.noAccess) { + return PlaceHolderScreen( + icon: SvgPicture.asset( + Assets.images.ilNoMediaFound, + width: 150, + ), + title: context.l10n.empty_media_title, + message: context.l10n.empty_media_message, + ); + } else { + return ListView.builder( + itemCount: state.medias.length + 1, + itemBuilder: (context, index) { + if (index < state.medias.length) { + final gridEntry = state.medias.entries.elementAt(index); + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + Builder( + builder: (context) { + return Container( + height: 45, + padding: const EdgeInsets.only(left: 16, top: 5), + margin: EdgeInsets.zero, + alignment: Alignment.centerLeft, + decoration: BoxDecoration( + color: context.colorScheme.surface, + ), + child: Text( + gridEntry.key.format(context, DateFormatType.relative), + style: AppTextStyles.subtitle1.copyWith( + color: context.colorScheme.textPrimary, + ), + ), + ); + }, + ), + GridView.builder( + padding: const EdgeInsets.all(4), + physics: const NeverScrollableScrollPhysics(), + shrinkWrap: true, + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: context.mediaQuerySize.width > 600 + ? context.mediaQuerySize.width ~/ 180 + : context.mediaQuerySize.width ~/ 100, + crossAxisSpacing: 4, + mainAxisSpacing: 4, + ), + itemCount: gridEntry.value.length, + itemBuilder: (context, index) => AppMediaThumbnail( + heroTag: "selection", + onTap: () { + _notifier.toggleMediaSelection( + gridEntry.value.elementAt(index), + ); + }, + selected: state.selectedMedias.contains( + widget.source == AppMediaSource.googleDrive + ? gridEntry.value.elementAt(index).driveMediaRefId + : widget.source == AppMediaSource.dropbox + ? gridEntry.value + .elementAt(index) + .dropboxMediaRefId + : gridEntry.value.elementAt(index).id, + ), + media: gridEntry.value.elementAt(index), + ), + ), + ], + ); + } else { + return OnVisibleCallback( + onVisible: () { + runPostFrame(() { + _notifier.loadMedias(); + }); + }, + child: FadeInSwitcher( + child: state.loading + ? const Center( + child: Padding( + padding: EdgeInsets.all(16), + child: AppCircularProgressIndicator( + size: 20, + ), + ), + ) + : const SizedBox(), + ), + ); + } + }, + ); + } + } +} diff --git a/app/lib/ui/flow/media_selection/media_selection_state_notifier.dart b/app/lib/ui/flow/media_selection/media_selection_state_notifier.dart new file mode 100644 index 00000000..ffa25b97 --- /dev/null +++ b/app/lib/ui/flow/media_selection/media_selection_state_notifier.dart @@ -0,0 +1,219 @@ +import 'package:collection/collection.dart'; +import 'package:data/domain/config.dart'; +import 'package:data/errors/app_error.dart'; +import 'package:data/log/logger.dart'; +import 'package:data/models/dropbox/account/dropbox_account.dart'; +import 'package:data/models/media/media.dart'; +import 'package:data/services/auth_service.dart'; +import 'package:data/services/dropbox_services.dart'; +import 'package:data/services/google_drive_service.dart'; +import 'package:data/services/local_media_service.dart'; +import 'package:data/storage/app_preferences.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:google_sign_in/google_sign_in.dart'; +import 'package:logger/logger.dart'; + +part 'media_selection_state_notifier.freezed.dart'; + +final mediaSelectionStateNotifierProvider = StateNotifierProvider.autoDispose + .family( + (ref, state) { + return MediaSelectionStateNotifier( + ref.read(localMediaServiceProvider), + ref.read(googleDriveServiceProvider), + ref.read(dropboxServiceProvider), + ref.read(googleUserAccountProvider), + ref.read(AppPreferences.dropboxCurrentUserAccount), + ref.read(loggerProvider), + state, + ); + }, +); + +class MediaSelectionStateNotifier extends StateNotifier { + final GoogleDriveService _googleDriveService; + final DropboxService _dropboxService; + final LocalMediaService _localMediaService; + final GoogleSignInAccount? _googleAccount; + final DropboxAccount? _dropboxAccount; + final Logger _logger; + final AppMediaSource _source; + + MediaSelectionStateNotifier( + this._localMediaService, + this._googleDriveService, + this._dropboxService, + this._googleAccount, + this._dropboxAccount, + this._logger, + this._source, + ) : super(const MediaSelectionState()) { + loadMedias(reload: true); + } + + String? _pageToken; + String? _backupFolderId; + bool _maxLoaded = false; + + Future loadMedias({bool reload = false}) async { + try { + if (state.loading || _maxLoaded) return; + + if (reload) { + _pageToken = null; + _maxLoaded = false; + } + + state = state.copyWith( + loading: true, + error: null, + actionError: null, + noAccess: false, + medias: reload ? {} : state.medias, + ); + + if (_source == AppMediaSource.googleDrive) { + if (_googleAccount == null) { + state = state.copyWith( + loading: false, + noAccess: true, + ); + return; + } + _backupFolderId ??= await _googleDriveService.getBackUpFolderId(); + if (_backupFolderId == null) { + throw BackUpFolderNotFound(); + } + final res = await _googleDriveService.getPaginatedMedias( + folder: _backupFolderId!, + nextPageToken: _pageToken, + ); + + _pageToken = res.nextPageToken; + if (res.nextPageToken == null) { + _maxLoaded = true; + } + final groupedMedias = groupBy( + [...state.medias.values.expand((element) => element), ...res.medias], + (media) => media.createdTime ?? media.modifiedTime ?? DateTime.now(), + ); + + state = state.copyWith( + medias: groupedMedias, + loading: false, + ); + } else if (_source == AppMediaSource.dropbox) { + if (_dropboxAccount == null) { + state = state.copyWith( + loading: false, + noAccess: true, + ); + return; + } + + final res = await _dropboxService.getPaginatedMedias( + nextPageToken: _pageToken, + folder: ProviderConstants.backupFolderPath, + ); + + _pageToken = res.nextPageToken; + if (res.nextPageToken == null) { + _maxLoaded = true; + } + + final groupedMedias = groupBy( + [...state.medias.values.expand((element) => element), ...res.medias], + (media) => media.createdTime ?? media.modifiedTime ?? DateTime.now(), + ); + + state = state.copyWith( + medias: groupedMedias, + loading: false, + ); + } else if (_source == AppMediaSource.local) { + final hasPermission = await _localMediaService.requestPermission(); + + if (!hasPermission) { + state = state.copyWith( + loading: false, + noAccess: true, + ); + return; + } + final mediasLength = + state.medias.values.expand((element) => element).length; + final medias = await _localMediaService.getLocalMedia( + start: mediasLength, + end: mediasLength + 30, + ); + + if (medias.length < 30) { + _maxLoaded = true; + } + + final groupedMedias = groupBy( + [...state.medias.values.expand((element) => element), ...medias], + (media) => media.createdTime ?? media.modifiedTime ?? DateTime.now(), + ); + + state = state.copyWith( + medias: groupedMedias, + loading: false, + ); + } else { + state = state.copyWith( + loading: false, + ); + } + } catch (e, s) { + state = state.copyWith( + loading: false, + error: state.medias.isEmpty ? e : null, + actionError: state.medias.isNotEmpty ? e : null, + ); + _logger.e( + "MediaSelectionStateNotifier: Error loading medias", + error: e, + stackTrace: s, + ); + } + } + + void toggleMediaSelection(AppMedia media) { + String id = media.id; + + if (_source == AppMediaSource.googleDrive) { + id = media.driveMediaRefId!; + } else if (_source == AppMediaSource.dropbox) { + id = media.dropboxMediaRefId!; + } + + if (state.selectedMedias.contains(id)) { + state = state.copyWith( + selectedMedias: [ + ...state.selectedMedias.where((element) => element != id), + ], + ); + } else { + state = state.copyWith( + selectedMedias: [ + ...state.selectedMedias, + media.id, + ], + ); + } + } +} + +@freezed +class MediaSelectionState with _$MediaSelectionState { + const factory MediaSelectionState({ + @Default({}) Map> medias, + @Default([]) List selectedMedias, + @Default(false) bool loading, + @Default(false) bool noAccess, + Object? error, + Object? actionError, + }) = _MediaSelectionState; +} diff --git a/app/lib/ui/flow/media_selection/media_selection_state_notifier.freezed.dart b/app/lib/ui/flow/media_selection/media_selection_state_notifier.freezed.dart new file mode 100644 index 00000000..5a9ec107 --- /dev/null +++ b/app/lib/ui/flow/media_selection/media_selection_state_notifier.freezed.dart @@ -0,0 +1,265 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'media_selection_state_notifier.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); + +/// @nodoc +mixin _$MediaSelectionState { + Map> get medias => + throw _privateConstructorUsedError; + List get selectedMedias => throw _privateConstructorUsedError; + bool get loading => throw _privateConstructorUsedError; + bool get noAccess => throw _privateConstructorUsedError; + Object? get error => throw _privateConstructorUsedError; + Object? get actionError => throw _privateConstructorUsedError; + + /// Create a copy of MediaSelectionState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $MediaSelectionStateCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $MediaSelectionStateCopyWith<$Res> { + factory $MediaSelectionStateCopyWith( + MediaSelectionState value, $Res Function(MediaSelectionState) then) = + _$MediaSelectionStateCopyWithImpl<$Res, MediaSelectionState>; + @useResult + $Res call( + {Map> medias, + List selectedMedias, + bool loading, + bool noAccess, + Object? error, + Object? actionError}); +} + +/// @nodoc +class _$MediaSelectionStateCopyWithImpl<$Res, $Val extends MediaSelectionState> + implements $MediaSelectionStateCopyWith<$Res> { + _$MediaSelectionStateCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of MediaSelectionState + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? medias = null, + Object? selectedMedias = null, + Object? loading = null, + Object? noAccess = null, + Object? error = freezed, + Object? actionError = freezed, + }) { + return _then(_value.copyWith( + medias: null == medias + ? _value.medias + : medias // ignore: cast_nullable_to_non_nullable + as Map>, + selectedMedias: null == selectedMedias + ? _value.selectedMedias + : selectedMedias // ignore: cast_nullable_to_non_nullable + as List, + loading: null == loading + ? _value.loading + : loading // ignore: cast_nullable_to_non_nullable + as bool, + noAccess: null == noAccess + ? _value.noAccess + : noAccess // ignore: cast_nullable_to_non_nullable + as bool, + error: freezed == error ? _value.error : error, + actionError: freezed == actionError ? _value.actionError : actionError, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$MediaSelectionStateImplCopyWith<$Res> + implements $MediaSelectionStateCopyWith<$Res> { + factory _$$MediaSelectionStateImplCopyWith(_$MediaSelectionStateImpl value, + $Res Function(_$MediaSelectionStateImpl) then) = + __$$MediaSelectionStateImplCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {Map> medias, + List selectedMedias, + bool loading, + bool noAccess, + Object? error, + Object? actionError}); +} + +/// @nodoc +class __$$MediaSelectionStateImplCopyWithImpl<$Res> + extends _$MediaSelectionStateCopyWithImpl<$Res, _$MediaSelectionStateImpl> + implements _$$MediaSelectionStateImplCopyWith<$Res> { + __$$MediaSelectionStateImplCopyWithImpl(_$MediaSelectionStateImpl _value, + $Res Function(_$MediaSelectionStateImpl) _then) + : super(_value, _then); + + /// Create a copy of MediaSelectionState + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? medias = null, + Object? selectedMedias = null, + Object? loading = null, + Object? noAccess = null, + Object? error = freezed, + Object? actionError = freezed, + }) { + return _then(_$MediaSelectionStateImpl( + medias: null == medias + ? _value._medias + : medias // ignore: cast_nullable_to_non_nullable + as Map>, + selectedMedias: null == selectedMedias + ? _value._selectedMedias + : selectedMedias // ignore: cast_nullable_to_non_nullable + as List, + loading: null == loading + ? _value.loading + : loading // ignore: cast_nullable_to_non_nullable + as bool, + noAccess: null == noAccess + ? _value.noAccess + : noAccess // ignore: cast_nullable_to_non_nullable + as bool, + error: freezed == error ? _value.error : error, + actionError: freezed == actionError ? _value.actionError : actionError, + )); + } +} + +/// @nodoc + +class _$MediaSelectionStateImpl implements _MediaSelectionState { + const _$MediaSelectionStateImpl( + {final Map> medias = const {}, + final List selectedMedias = const [], + this.loading = false, + this.noAccess = false, + this.error, + this.actionError}) + : _medias = medias, + _selectedMedias = selectedMedias; + + final Map> _medias; + @override + @JsonKey() + Map> get medias { + if (_medias is EqualUnmodifiableMapView) return _medias; + // ignore: implicit_dynamic_type + return EqualUnmodifiableMapView(_medias); + } + + final List _selectedMedias; + @override + @JsonKey() + List get selectedMedias { + if (_selectedMedias is EqualUnmodifiableListView) return _selectedMedias; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_selectedMedias); + } + + @override + @JsonKey() + final bool loading; + @override + @JsonKey() + final bool noAccess; + @override + final Object? error; + @override + final Object? actionError; + + @override + String toString() { + return 'MediaSelectionState(medias: $medias, selectedMedias: $selectedMedias, loading: $loading, noAccess: $noAccess, error: $error, actionError: $actionError)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$MediaSelectionStateImpl && + const DeepCollectionEquality().equals(other._medias, _medias) && + const DeepCollectionEquality() + .equals(other._selectedMedias, _selectedMedias) && + (identical(other.loading, loading) || other.loading == loading) && + (identical(other.noAccess, noAccess) || + other.noAccess == noAccess) && + const DeepCollectionEquality().equals(other.error, error) && + const DeepCollectionEquality() + .equals(other.actionError, actionError)); + } + + @override + int get hashCode => Object.hash( + runtimeType, + const DeepCollectionEquality().hash(_medias), + const DeepCollectionEquality().hash(_selectedMedias), + loading, + noAccess, + const DeepCollectionEquality().hash(error), + const DeepCollectionEquality().hash(actionError)); + + /// Create a copy of MediaSelectionState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$MediaSelectionStateImplCopyWith<_$MediaSelectionStateImpl> get copyWith => + __$$MediaSelectionStateImplCopyWithImpl<_$MediaSelectionStateImpl>( + this, _$identity); +} + +abstract class _MediaSelectionState implements MediaSelectionState { + const factory _MediaSelectionState( + {final Map> medias, + final List selectedMedias, + final bool loading, + final bool noAccess, + final Object? error, + final Object? actionError}) = _$MediaSelectionStateImpl; + + @override + Map> get medias; + @override + List get selectedMedias; + @override + bool get loading; + @override + bool get noAccess; + @override + Object? get error; + @override + Object? get actionError; + + /// Create a copy of MediaSelectionState + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$MediaSelectionStateImplCopyWith<_$MediaSelectionStateImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/app/lib/ui/navigation/app_route.dart b/app/lib/ui/navigation/app_route.dart index 79bc0c1f..bbba99b4 100644 --- a/app/lib/ui/navigation/app_route.dart +++ b/app/lib/ui/navigation/app_route.dart @@ -1,6 +1,10 @@ +import 'package:data/models/album/album.dart'; import '../flow/accounts/accounts_screen.dart'; +import '../flow/albums/add/add_album_screen.dart'; import '../flow/albums/albums_screen.dart'; +import '../flow/albums/media_list/album_media_list_screen.dart'; import '../flow/main/main_screen.dart'; +import '../flow/media_selection/media_selection_screen.dart'; import '../flow/media_transfer/media_transfer_screen.dart'; import '../flow/onboard/onboard_screen.dart'; import 'package:data/models/media/media.dart'; @@ -13,15 +17,20 @@ import '../flow/media_preview/media_preview_screen.dart'; part 'app_route.g.dart'; class AppRoutePath { + static const onBoard = '/on-board'; static const home = '/'; static const albums = '/albums'; - static const onBoard = '/on-board'; + static const add = 'add'; + static const mediaList = 'media-list'; + static const transfer = '/transfer'; static const accounts = '/accounts'; static const preview = '/preview'; - static const transfer = '/transfer'; static const metaDataDetails = '/metadata-details'; + static const mediaSelection = '/select'; } +final rootNavigatorKey = GlobalKey(); + @TypedGoRoute(path: AppRoutePath.onBoard) class OnBoardRoute extends GoRouteData { const OnBoardRoute(); @@ -40,7 +49,13 @@ class OnBoardRoute extends GoRouteData { ), TypedStatefulShellBranch( routes: [ - TypedGoRoute(path: AppRoutePath.albums), + TypedGoRoute( + path: AppRoutePath.albums, + routes: [ + TypedGoRoute(path: AppRoutePath.add), + TypedGoRoute(path: AppRoutePath.mediaList), + ], + ), ], ), TypedStatefulShellBranch( @@ -90,6 +105,30 @@ class AlbumsRoute extends GoRouteData { const AlbumsScreen(); } +class AddAlbumRoute extends GoRouteData { + static final GlobalKey $parentNavigatorKey = rootNavigatorKey; + + final Album? $extra; + + const AddAlbumRoute({this.$extra}); + + @override + Widget build(BuildContext context, GoRouterState state) => + AddAlbumScreen(editAlbum: $extra); +} + +class AlbumMediaListRoute extends GoRouteData { + static final GlobalKey $parentNavigatorKey = rootNavigatorKey; + + final Album $extra; + + const AlbumMediaListRoute({required this.$extra}); + + @override + Widget build(BuildContext context, GoRouterState state) => + AlbumMediaListScreen(album: $extra); +} + class TransferRoute extends GoRouteData { const TransferRoute(); @@ -142,3 +181,14 @@ class MediaMetadataDetailsRoute extends GoRouteData { Widget build(BuildContext context, GoRouterState state) => MediaMetadataDetailsScreen(media: $extra); } + +@TypedGoRoute(path: AppRoutePath.mediaSelection) +class MediaSelectionRoute extends GoRouteData { + final AppMediaSource $extra; + + const MediaSelectionRoute({required this.$extra}); + + @override + Widget build(BuildContext context, GoRouterState state) => + MediaSelectionScreen(source: $extra); +} diff --git a/app/lib/ui/navigation/app_route.g.dart b/app/lib/ui/navigation/app_route.g.dart index acf3d61f..898cc02d 100644 --- a/app/lib/ui/navigation/app_route.g.dart +++ b/app/lib/ui/navigation/app_route.g.dart @@ -11,6 +11,7 @@ List get $appRoutes => [ $mainShellRoute, $mediaPreviewRoute, $mediaMetadataDetailsRoute, + $mediaSelectionRoute, ]; RouteBase get $onBoardRoute => GoRouteData.$route( @@ -51,6 +52,18 @@ RouteBase get $mainShellRoute => StatefulShellRouteData.$route( GoRouteData.$route( path: '/albums', factory: $AlbumsRouteExtension._fromState, + routes: [ + GoRouteData.$route( + path: 'add', + parentNavigatorKey: AddAlbumRoute.$parentNavigatorKey, + factory: $AddAlbumRouteExtension._fromState, + ), + GoRouteData.$route( + path: 'media-list', + parentNavigatorKey: AlbumMediaListRoute.$parentNavigatorKey, + factory: $AlbumMediaListRouteExtension._fromState, + ), + ], ), ], ), @@ -112,6 +125,49 @@ extension $AlbumsRouteExtension on AlbumsRoute { void replace(BuildContext context) => context.replace(location); } +extension $AddAlbumRouteExtension on AddAlbumRoute { + static AddAlbumRoute _fromState(GoRouterState state) => AddAlbumRoute( + $extra: state.extra as Album?, + ); + + String get location => GoRouteData.$location( + '/albums/add', + ); + + void go(BuildContext context) => context.go(location, extra: $extra); + + Future push(BuildContext context) => + context.push(location, extra: $extra); + + void pushReplacement(BuildContext context) => + context.pushReplacement(location, extra: $extra); + + void replace(BuildContext context) => + context.replace(location, extra: $extra); +} + +extension $AlbumMediaListRouteExtension on AlbumMediaListRoute { + static AlbumMediaListRoute _fromState(GoRouterState state) => + AlbumMediaListRoute( + $extra: state.extra as Album, + ); + + String get location => GoRouteData.$location( + '/albums/media-list', + ); + + void go(BuildContext context) => context.go(location, extra: $extra); + + Future push(BuildContext context) => + context.push(location, extra: $extra); + + void pushReplacement(BuildContext context) => + context.pushReplacement(location, extra: $extra); + + void replace(BuildContext context) => + context.replace(location, extra: $extra); +} + extension $TransferRouteExtension on TransferRoute { static TransferRoute _fromState(GoRouterState state) => const TransferRoute(); @@ -198,3 +254,30 @@ extension $MediaMetadataDetailsRouteExtension on MediaMetadataDetailsRoute { void replace(BuildContext context) => context.replace(location, extra: $extra); } + +RouteBase get $mediaSelectionRoute => GoRouteData.$route( + path: '/select', + factory: $MediaSelectionRouteExtension._fromState, + ); + +extension $MediaSelectionRouteExtension on MediaSelectionRoute { + static MediaSelectionRoute _fromState(GoRouterState state) => + MediaSelectionRoute( + $extra: state.extra as AppMediaSource, + ); + + String get location => GoRouteData.$location( + '/select', + ); + + void go(BuildContext context) => context.go(location, extra: $extra); + + Future push(BuildContext context) => + context.push(location, extra: $extra); + + void pushReplacement(BuildContext context) => + context.pushReplacement(location, extra: $extra); + + void replace(BuildContext context) => + context.replace(location, extra: $extra); +} diff --git a/data/.flutter-plugins-dependencies b/data/.flutter-plugins-dependencies index fd2aaa8b..8979169c 100644 --- a/data/.flutter-plugins-dependencies +++ b/data/.flutter-plugins-dependencies @@ -1 +1 @@ -{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"flutter_local_notifications","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/flutter_local_notifications-18.0.1/","native_build":true,"dependencies":[]},{"name":"google_sign_in_ios","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/google_sign_in_ios-5.7.8/","shared_darwin_source":true,"native_build":true,"dependencies":[]},{"name":"package_info_plus","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/package_info_plus-8.1.1/","native_build":true,"dependencies":[]},{"name":"path_provider_foundation","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/path_provider_foundation-2.4.0/","shared_darwin_source":true,"native_build":true,"dependencies":[]},{"name":"photo_manager","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/photo_manager-3.6.2/","native_build":true,"dependencies":[]},{"name":"shared_preferences_foundation","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/shared_preferences_foundation-2.5.3/","shared_darwin_source":true,"native_build":true,"dependencies":[]},{"name":"sqflite_darwin","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/sqflite_darwin-2.4.1/","shared_darwin_source":true,"native_build":true,"dependencies":[]},{"name":"url_launcher_ios","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/url_launcher_ios-6.3.2/","native_build":true,"dependencies":[]}],"android":[{"name":"flutter_local_notifications","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/flutter_local_notifications-18.0.1/","native_build":true,"dependencies":[]},{"name":"google_sign_in_android","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/google_sign_in_android-6.1.33/","native_build":true,"dependencies":[]},{"name":"package_info_plus","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/package_info_plus-8.1.1/","native_build":true,"dependencies":[]},{"name":"path_provider_android","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/path_provider_android-2.2.12/","native_build":true,"dependencies":[]},{"name":"photo_manager","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/photo_manager-3.6.2/","native_build":true,"dependencies":[]},{"name":"shared_preferences_android","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/shared_preferences_android-2.3.3/","native_build":true,"dependencies":[]},{"name":"sqflite_android","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/sqflite_android-2.4.0/","native_build":true,"dependencies":[]},{"name":"url_launcher_android","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/url_launcher_android-6.3.14/","native_build":true,"dependencies":[]}],"macos":[{"name":"flutter_local_notifications","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/flutter_local_notifications-18.0.1/","native_build":true,"dependencies":[]},{"name":"google_sign_in_ios","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/google_sign_in_ios-5.7.8/","shared_darwin_source":true,"native_build":true,"dependencies":[]},{"name":"package_info_plus","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/package_info_plus-8.1.1/","native_build":true,"dependencies":[]},{"name":"path_provider_foundation","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/path_provider_foundation-2.4.0/","shared_darwin_source":true,"native_build":true,"dependencies":[]},{"name":"photo_manager","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/photo_manager-3.6.2/","native_build":true,"dependencies":[]},{"name":"shared_preferences_foundation","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/shared_preferences_foundation-2.5.3/","shared_darwin_source":true,"native_build":true,"dependencies":[]},{"name":"sqflite_darwin","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/sqflite_darwin-2.4.1/","shared_darwin_source":true,"native_build":true,"dependencies":[]},{"name":"url_launcher_macos","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/url_launcher_macos-3.2.2/","native_build":true,"dependencies":[]}],"linux":[{"name":"flutter_local_notifications_linux","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/flutter_local_notifications_linux-5.0.0/","native_build":false,"dependencies":[]},{"name":"package_info_plus","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/package_info_plus-8.1.1/","native_build":false,"dependencies":[]},{"name":"path_provider_linux","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/path_provider_linux-2.2.1/","native_build":false,"dependencies":[]},{"name":"shared_preferences_linux","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/shared_preferences_linux-2.4.1/","native_build":false,"dependencies":["path_provider_linux"]},{"name":"url_launcher_linux","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/url_launcher_linux-3.2.1/","native_build":true,"dependencies":[]}],"windows":[{"name":"package_info_plus","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/package_info_plus-8.1.1/","native_build":false,"dependencies":[]},{"name":"path_provider_windows","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/path_provider_windows-2.3.0/","native_build":false,"dependencies":[]},{"name":"shared_preferences_windows","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/shared_preferences_windows-2.4.1/","native_build":false,"dependencies":["path_provider_windows"]},{"name":"url_launcher_windows","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/url_launcher_windows-3.1.3/","native_build":true,"dependencies":[]}],"web":[{"name":"google_sign_in_web","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/google_sign_in_web-0.12.4+3/","dependencies":[]},{"name":"package_info_plus","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/package_info_plus-8.1.1/","dependencies":[]},{"name":"shared_preferences_web","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/shared_preferences_web-2.4.2/","dependencies":[]},{"name":"url_launcher_web","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/url_launcher_web-2.3.3/","dependencies":[]}]},"dependencyGraph":[{"name":"flutter_local_notifications","dependencies":["flutter_local_notifications_linux"]},{"name":"flutter_local_notifications_linux","dependencies":[]},{"name":"google_sign_in","dependencies":["google_sign_in_android","google_sign_in_ios","google_sign_in_web"]},{"name":"google_sign_in_android","dependencies":[]},{"name":"google_sign_in_ios","dependencies":[]},{"name":"google_sign_in_web","dependencies":[]},{"name":"package_info_plus","dependencies":[]},{"name":"path_provider","dependencies":["path_provider_android","path_provider_foundation","path_provider_linux","path_provider_windows"]},{"name":"path_provider_android","dependencies":[]},{"name":"path_provider_foundation","dependencies":[]},{"name":"path_provider_linux","dependencies":[]},{"name":"path_provider_windows","dependencies":[]},{"name":"photo_manager","dependencies":[]},{"name":"shared_preferences","dependencies":["shared_preferences_android","shared_preferences_foundation","shared_preferences_linux","shared_preferences_web","shared_preferences_windows"]},{"name":"shared_preferences_android","dependencies":[]},{"name":"shared_preferences_foundation","dependencies":[]},{"name":"shared_preferences_linux","dependencies":["path_provider_linux"]},{"name":"shared_preferences_web","dependencies":[]},{"name":"shared_preferences_windows","dependencies":["path_provider_windows"]},{"name":"sqflite","dependencies":["sqflite_android","sqflite_darwin"]},{"name":"sqflite_android","dependencies":[]},{"name":"sqflite_darwin","dependencies":[]},{"name":"url_launcher","dependencies":["url_launcher_android","url_launcher_ios","url_launcher_linux","url_launcher_macos","url_launcher_web","url_launcher_windows"]},{"name":"url_launcher_android","dependencies":[]},{"name":"url_launcher_ios","dependencies":[]},{"name":"url_launcher_linux","dependencies":[]},{"name":"url_launcher_macos","dependencies":[]},{"name":"url_launcher_web","dependencies":[]},{"name":"url_launcher_windows","dependencies":[]}],"date_created":"2024-12-24 11:19:49.594419","version":"3.27.1","swift_package_manager_enabled":false} \ No newline at end of file +{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"flutter_local_notifications","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/flutter_local_notifications-18.0.1/","native_build":true,"dependencies":[]},{"name":"google_sign_in_ios","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/google_sign_in_ios-5.7.8/","shared_darwin_source":true,"native_build":true,"dependencies":[]},{"name":"package_info_plus","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/package_info_plus-8.1.1/","native_build":true,"dependencies":[]},{"name":"path_provider_foundation","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/path_provider_foundation-2.4.0/","shared_darwin_source":true,"native_build":true,"dependencies":[]},{"name":"photo_manager","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/photo_manager-3.6.2/","native_build":true,"dependencies":[]},{"name":"shared_preferences_foundation","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/shared_preferences_foundation-2.5.3/","shared_darwin_source":true,"native_build":true,"dependencies":[]},{"name":"sqflite_darwin","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/sqflite_darwin-2.4.1/","shared_darwin_source":true,"native_build":true,"dependencies":[]},{"name":"url_launcher_ios","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/url_launcher_ios-6.3.2/","native_build":true,"dependencies":[]}],"android":[{"name":"flutter_local_notifications","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/flutter_local_notifications-18.0.1/","native_build":true,"dependencies":[]},{"name":"google_sign_in_android","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/google_sign_in_android-6.1.33/","native_build":true,"dependencies":[]},{"name":"package_info_plus","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/package_info_plus-8.1.1/","native_build":true,"dependencies":[]},{"name":"path_provider_android","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/path_provider_android-2.2.12/","native_build":true,"dependencies":[]},{"name":"photo_manager","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/photo_manager-3.6.2/","native_build":true,"dependencies":[]},{"name":"shared_preferences_android","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/shared_preferences_android-2.3.3/","native_build":true,"dependencies":[]},{"name":"sqflite_android","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/sqflite_android-2.4.0/","native_build":true,"dependencies":[]},{"name":"url_launcher_android","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/url_launcher_android-6.3.14/","native_build":true,"dependencies":[]}],"macos":[{"name":"flutter_local_notifications","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/flutter_local_notifications-18.0.1/","native_build":true,"dependencies":[]},{"name":"google_sign_in_ios","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/google_sign_in_ios-5.7.8/","shared_darwin_source":true,"native_build":true,"dependencies":[]},{"name":"package_info_plus","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/package_info_plus-8.1.1/","native_build":true,"dependencies":[]},{"name":"path_provider_foundation","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/path_provider_foundation-2.4.0/","shared_darwin_source":true,"native_build":true,"dependencies":[]},{"name":"photo_manager","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/photo_manager-3.6.2/","native_build":true,"dependencies":[]},{"name":"shared_preferences_foundation","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/shared_preferences_foundation-2.5.3/","shared_darwin_source":true,"native_build":true,"dependencies":[]},{"name":"sqflite_darwin","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/sqflite_darwin-2.4.1/","shared_darwin_source":true,"native_build":true,"dependencies":[]},{"name":"url_launcher_macos","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/url_launcher_macos-3.2.2/","native_build":true,"dependencies":[]}],"linux":[{"name":"flutter_local_notifications_linux","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/flutter_local_notifications_linux-5.0.0/","native_build":false,"dependencies":[]},{"name":"package_info_plus","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/package_info_plus-8.1.1/","native_build":false,"dependencies":[]},{"name":"path_provider_linux","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/path_provider_linux-2.2.1/","native_build":false,"dependencies":[]},{"name":"shared_preferences_linux","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/shared_preferences_linux-2.4.1/","native_build":false,"dependencies":["path_provider_linux"]},{"name":"url_launcher_linux","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/url_launcher_linux-3.2.1/","native_build":true,"dependencies":[]}],"windows":[{"name":"package_info_plus","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/package_info_plus-8.1.1/","native_build":false,"dependencies":[]},{"name":"path_provider_windows","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/path_provider_windows-2.3.0/","native_build":false,"dependencies":[]},{"name":"shared_preferences_windows","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/shared_preferences_windows-2.4.1/","native_build":false,"dependencies":["path_provider_windows"]},{"name":"url_launcher_windows","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/url_launcher_windows-3.1.3/","native_build":true,"dependencies":[]}],"web":[{"name":"google_sign_in_web","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/google_sign_in_web-0.12.4+3/","dependencies":[]},{"name":"package_info_plus","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/package_info_plus-8.1.1/","dependencies":[]},{"name":"shared_preferences_web","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/shared_preferences_web-2.4.2/","dependencies":[]},{"name":"url_launcher_web","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/url_launcher_web-2.3.3/","dependencies":[]}]},"dependencyGraph":[{"name":"flutter_local_notifications","dependencies":["flutter_local_notifications_linux"]},{"name":"flutter_local_notifications_linux","dependencies":[]},{"name":"google_sign_in","dependencies":["google_sign_in_android","google_sign_in_ios","google_sign_in_web"]},{"name":"google_sign_in_android","dependencies":[]},{"name":"google_sign_in_ios","dependencies":[]},{"name":"google_sign_in_web","dependencies":[]},{"name":"package_info_plus","dependencies":[]},{"name":"path_provider","dependencies":["path_provider_android","path_provider_foundation","path_provider_linux","path_provider_windows"]},{"name":"path_provider_android","dependencies":[]},{"name":"path_provider_foundation","dependencies":[]},{"name":"path_provider_linux","dependencies":[]},{"name":"path_provider_windows","dependencies":[]},{"name":"photo_manager","dependencies":[]},{"name":"shared_preferences","dependencies":["shared_preferences_android","shared_preferences_foundation","shared_preferences_linux","shared_preferences_web","shared_preferences_windows"]},{"name":"shared_preferences_android","dependencies":[]},{"name":"shared_preferences_foundation","dependencies":[]},{"name":"shared_preferences_linux","dependencies":["path_provider_linux"]},{"name":"shared_preferences_web","dependencies":[]},{"name":"shared_preferences_windows","dependencies":["path_provider_windows"]},{"name":"sqflite","dependencies":["sqflite_android","sqflite_darwin"]},{"name":"sqflite_android","dependencies":[]},{"name":"sqflite_darwin","dependencies":[]},{"name":"url_launcher","dependencies":["url_launcher_android","url_launcher_ios","url_launcher_linux","url_launcher_macos","url_launcher_web","url_launcher_windows"]},{"name":"url_launcher_android","dependencies":[]},{"name":"url_launcher_ios","dependencies":[]},{"name":"url_launcher_linux","dependencies":[]},{"name":"url_launcher_macos","dependencies":[]},{"name":"url_launcher_web","dependencies":[]},{"name":"url_launcher_windows","dependencies":[]}],"date_created":"2025-01-02 09:46:53.659174","version":"3.27.1","swift_package_manager_enabled":false} \ No newline at end of file diff --git a/data/lib/apis/google_drive/google_drive_endpoint.dart b/data/lib/apis/google_drive/google_drive_endpoint.dart index ca354f90..73e45e72 100644 --- a/data/lib/apis/google_drive/google_drive_endpoint.dart +++ b/data/lib/apis/google_drive/google_drive_endpoint.dart @@ -121,7 +121,8 @@ class GoogleDriveContentUpdateEndpoint extends Endpoint { @override Map? get queryParameters => { 'uploadType': 'media', - 'fields': 'id, name, description, mimeType, thumbnailLink, webContentLink, createdTime, modifiedTime, size, imageMediaMetadata, videoMediaMetadata, appProperties', + 'fields': + 'id, name, description, mimeType, thumbnailLink, webContentLink, createdTime, modifiedTime, size, imageMediaMetadata, videoMediaMetadata, appProperties', }; @override @@ -215,6 +216,31 @@ class GoogleDriveListEndpoint extends Endpoint { }; } +class GoogleDriveGetEndpoint extends Endpoint { + final String fields; + final String id; + + const GoogleDriveGetEndpoint({ + required this.id, + this.fields = + 'nextPageToken, files(id, name, description, mimeType, thumbnailLink, webContentLink, createdTime, modifiedTime, size, imageMediaMetadata, videoMediaMetadata, appProperties)', + }); + + @override + String get baseUrl => BaseURL.googleDriveV3; + + @override + String get path => '/files/$id'; + + @override + HttpMethod get method => HttpMethod.get; + + @override + Map? get queryParameters => { + 'fields': fields, + }; +} + class GoogleDriveUpdateAppPropertiesEndpoint extends Endpoint { final String id; final String localFileId; diff --git a/data/lib/handlers/unique_id_generator.dart b/data/lib/handlers/unique_id_generator.dart new file mode 100644 index 00000000..eb9116bd --- /dev/null +++ b/data/lib/handlers/unique_id_generator.dart @@ -0,0 +1,48 @@ +import 'dart:math' as math; + +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +final uniqueIdGeneratorProvider = Provider((ref) { + return UniqueIdGenerator(); +}); + +class UniqueIdGenerator { + /// Generate a cryptographically secure unique integer ID + /// Response: 15697651741157933000 + int num() { + return int.parse( + List.generate( + 4, + (index) => math.Random.secure().nextInt(1 << 16).toRadixString(16), + ).join(), + radix: 16, + ); + } + + ///Generate Cryptographically secure unique ID in UUIDv4 format + ///https://en.wikipedia.org/wiki/Universally_unique_identifier#Version_4_(random) + String v4() { + final random = math.Random.secure(); + // Generate 16 random bytes + final bytes = List.generate(16, (_) => random.nextInt(256)); + + // Set version to 4 (random UUID) + bytes[6] = (bytes[6] & 0x0F) | 0x40; + + // Set variant to 10xx (RFC 4122) + bytes[8] = (bytes[8] & 0x3F) | 0x80; + + // Convert bytes to UUID string + return _bytesToUuidString(bytes); + } + + /// Helper method to convert byte list to UUID string + String _bytesToUuidString(List bytes) { + final buffer = StringBuffer(); + for (int i = 0; i < bytes.length; i++) { + buffer.write(bytes[i].toRadixString(16).padLeft(2, '0')); + if (i == 3 || i == 5 || i == 7 || i == 9) buffer.write('-'); + } + return buffer.toString(); + } +} diff --git a/data/lib/services/dropbox_services.dart b/data/lib/services/dropbox_services.dart index c0fa11b6..4c2b5bab 100644 --- a/data/lib/services/dropbox_services.dart +++ b/data/lib/services/dropbox_services.dart @@ -225,6 +225,22 @@ class DropboxService extends CloudProviderService { } } + Future getMedia({ + required String id, + }) async { + final res = await _dropboxAuthenticatedDio.req( + DropboxGetFileMetadata(id: id), + ); + + if (res.statusCode == 200) { + return AppMedia.fromDropboxJson(json: res.data, metadataJson: res.data); + } + throw SomethingWentWrongError( + statusCode: res.statusCode, + message: res.statusMessage ?? '', + ); + } + @override Future createFolder(String folderName) async { final response = await _dropboxAuthenticatedDio.req( @@ -417,9 +433,9 @@ class DropboxService extends CloudProviderService { ); } - Future deleteAlbum(Album album) async { + Future deleteAlbum(String id) async { final albums = await getAlbums(); - albums.removeWhere((a) => a.id == album.id); + albums.removeWhere((a) => a.id == id); final res = await _dropboxAuthenticatedDio.req( DropboxUploadEndpoint( diff --git a/data/lib/services/google_drive_service.dart b/data/lib/services/google_drive_service.dart index 6f493edc..8f4f902e 100644 --- a/data/lib/services/google_drive_service.dart +++ b/data/lib/services/google_drive_service.dart @@ -154,6 +154,21 @@ class GoogleDriveService extends CloudProviderService { ); } + Future getMedia({ + required String id, + }) async { + final res = await _client.req(GoogleDriveGetEndpoint(id: id)); + + if (res.statusCode == 200) { + return AppMedia.fromGoogleDriveFile(drive.File.fromJson(res.data)); + } + + throw SomethingWentWrongError( + statusCode: res.statusCode, + message: res.statusMessage, + ); + } + @override Future deleteMedia({ required String id, diff --git a/data/lib/services/local_media_service.dart b/data/lib/services/local_media_service.dart index 1906fc23..3cc87ed0 100644 --- a/data/lib/services/local_media_service.dart +++ b/data/lib/services/local_media_service.dart @@ -123,16 +123,16 @@ class LocalMediaService { ); } - Future createAlbum(Album album) async { + Future createAlbum({required String id, required String name}) async { final db = await openAlbumDatabase(); await db.insert( LocalDatabaseConstants.albumsTable, { - 'id': album.id, - 'name': album.name, - 'source': album.source.value, - 'created_at': DateTimeJsonConverter().toJson(album.created_at), - 'medias': album.medias.join(','), + 'id': id, + 'name': name, + 'source': AppMediaSource.local.value, + 'created_at': DateTimeJsonConverter().toJson(DateTime.now()), + 'medias': '', }, ); await db.close(); @@ -140,16 +140,16 @@ class LocalMediaService { Future updateAlbum(Album album) async { final db = await openAlbumDatabase(); - await db.update( - LocalDatabaseConstants.albumsTable, - { - 'name': album.name, - 'source': album.source.value, - 'created_at': DateTimeJsonConverter().toJson(album.created_at), - 'medias': album.medias.join(','), - }, - where: 'id = ?', - whereArgs: [album.id], + await db.rawUpdate( + 'UPDATE ${LocalDatabaseConstants.albumsTable} SET ' + 'name = ?, ' + 'medias = ? ' + 'WHERE id = ?', + [ + album.name, + album.medias.join(','), + album.id, + ], ); await db.close(); } @@ -178,7 +178,9 @@ class LocalMediaService { ), created_at: DateTimeJsonConverter().fromJson(album['created_at'] as String), - medias: (album['medias'] as String).split(','), + medias: (album['medias'] as String).trim().isEmpty + ? [] + : (album['medias'] as String).trim().split(','), ), ) .toList(); diff --git a/style/lib/buttons/action_button.dart b/style/lib/buttons/action_button.dart index 43031408..2086b429 100644 --- a/style/lib/buttons/action_button.dart +++ b/style/lib/buttons/action_button.dart @@ -4,7 +4,7 @@ import 'package:flutter/material.dart'; import '../indicators/circular_progress_indicator.dart'; class ActionButton extends StatelessWidget { - final void Function() onPressed; + final void Function()? onPressed; final Widget icon; final bool progress; final MaterialTapTargetSize tapTargetSize; @@ -14,7 +14,7 @@ class ActionButton extends StatelessWidget { const ActionButton({ super.key, - required this.onPressed, + this.onPressed, required this.icon, this.size = 40, this.tapTargetSize = MaterialTapTargetSize.padded, diff --git a/style/lib/buttons/radio_selection_button.dart b/style/lib/buttons/radio_selection_button.dart new file mode 100644 index 00000000..d207be82 --- /dev/null +++ b/style/lib/buttons/radio_selection_button.dart @@ -0,0 +1,62 @@ +import 'package:flutter/material.dart'; +import '../animations/on_tap_scale.dart'; +import '../extensions/context_extensions.dart'; +import '../text/app_text_style.dart'; + +class RadioSelectionButton extends StatelessWidget { + final T value; + final T groupValue; + final String label; + final void Function() onTab; + + const RadioSelectionButton({ + super.key, + required this.value, + required this.groupValue, + required this.onTab, + required this.label, + }); + + @override + Widget build(BuildContext context) { + return OnTapScale( + onTap: onTab, + child: Container( + margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8), + decoration: BoxDecoration( + border: Border.all( + color: value == groupValue + ? context.colorScheme.primary + : context.colorScheme.outline, + ), + borderRadius: BorderRadius.circular(8), + ), + child: Row( + children: [ + IgnorePointer( + child: Material( + color: Colors.transparent, + child: Radio( + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + visualDensity: VisualDensity.compact, + value: value, + groupValue: groupValue, + onChanged: (_) {}, + ), + ), + ), + Text( + label, + style: AppTextStyles.subtitle1.copyWith( + color: value == groupValue + ? context.colorScheme.textPrimary + : context.colorScheme.textSecondary, + ), + ), + ], + ), + ), + ); + } +} diff --git a/style/lib/text/app_text_field.dart b/style/lib/text/app_text_field.dart new file mode 100644 index 00000000..74e9188f --- /dev/null +++ b/style/lib/text/app_text_field.dart @@ -0,0 +1,195 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import '../../extensions/context_extensions.dart'; +import 'app_text_style.dart'; + +class AppTextField extends StatelessWidget { + final String? label; + final TextStyle? labelStyle; + final TextEditingController? controller; + final int? maxLines; + final int? minLines; + final TextStyle? style; + final bool expands; + final bool enabled; + final BorderRadius? borderRadius; + final AppTextFieldBorderType borderType; + final double borderWidth; + final TextInputAction? textInputAction; + final String? hintText; + final String? errorText; + final bool? isDense; + final EdgeInsetsGeometry? contentPadding; + final bool? isCollapsed; + final TextStyle? hintStyle; + final Function(String)? onChanged; + final bool autoFocus; + final TextInputType? keyboardType; + final FocusNode? focusNode; + final bool? filled; + final Color? fillColor; + final TextAlign textAlign; + final List? inputFormatters; + final Widget? prefix; + final Widget? suffix; + final Function(PointerDownEvent)? onTapOutside; + final Function()? onTap; + final void Function(String)? onSubmitted; + + const AppTextField({ + super.key, + this.label, + this.labelStyle, + this.controller, + this.maxLines = 1, + this.minLines, + this.style, + this.expands = false, + this.enabled = true, + this.onChanged, + this.borderType = AppTextFieldBorderType.outline, + this.borderWidth = 1, + this.textInputAction, + this.borderRadius, + this.hintText, + this.hintStyle, + this.errorText, + this.contentPadding, + this.isDense, + this.isCollapsed, + this.autoFocus = false, + this.keyboardType, + this.focusNode, + this.textAlign = TextAlign.start, + this.onTapOutside, + this.filled = true, + this.fillColor, + this.prefix, + this.suffix, + this.inputFormatters, + this.onTap, + this.onSubmitted, + }); + + @override + Widget build(BuildContext context) => + label != null ? _textFieldWithLabel(context) : _textField(context); + + Widget _textFieldWithLabel(BuildContext context) => Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.only(left: 12.0), + child: Text( + label!, + style: labelStyle ?? + AppTextStyles.body2.copyWith( + color: context.colorScheme.textDisabled, + ), + ), + ), + const SizedBox(height: 6), + _textField(context), + ], + ); + + Widget _textField(BuildContext context) => Material( + color: Colors.transparent, + child: TextField( + contextMenuBuilder: + (BuildContext context, EditableTextState editableTextState) { + // If supported, show the system context menu. + if (SystemContextMenu.isSupported(context)) { + return SystemContextMenu.editableText( + editableTextState: editableTextState, + ); + } + // Otherwise, show the flutter-rendered context menu for the current + // platform. + return AdaptiveTextSelectionToolbar.editableText( + editableTextState: editableTextState, + ); + }, + onSubmitted: onSubmitted, + inputFormatters: inputFormatters, + controller: controller, + onChanged: onChanged, + onTap: onTap, + enabled: enabled, + maxLines: maxLines, + minLines: minLines, + expands: expands, + textInputAction: textInputAction, + autofocus: autoFocus, + keyboardType: keyboardType, + focusNode: focusNode, + textAlign: textAlign, + textCapitalization: TextCapitalization.sentences, + style: style ?? + AppTextStyles.subtitle1.copyWith( + color: context.colorScheme.textPrimary, + ), + onTapOutside: onTapOutside ?? + (event) { + FocusManager.instance.primaryFocus?.unfocus(); + }, + decoration: InputDecoration( + hintFadeDuration: const Duration(milliseconds: 300), + filled: filled, + fillColor: fillColor ?? context.colorScheme.containerLow, + isDense: isDense, + isCollapsed: isCollapsed, + hintText: hintText, + hintStyle: hintStyle ?? + AppTextStyles.body.copyWith( + color: context.colorScheme.textDisabled, + ), + focusedBorder: _border(context, true), + enabledBorder: _border(context, false), + disabledBorder: _border(context, false), + contentPadding: contentPadding ?? + const EdgeInsets.symmetric( + horizontal: 16, + vertical: 13.5, + ), + prefixIcon: prefix, + suffix: suffix, + errorText: errorText, + ), + ), + ); + + InputBorder _border(BuildContext context, bool focused) { + switch (borderType) { + case AppTextFieldBorderType.none: + return const UnderlineInputBorder( + borderSide: BorderSide.none, + ); + case AppTextFieldBorderType.outline: + return OutlineInputBorder( + borderRadius: borderRadius ?? BorderRadius.circular(12), + borderSide: BorderSide( + color: focused + ? context.colorScheme.primary + : context.colorScheme.outline, + width: borderWidth, + ), + ); + case AppTextFieldBorderType.underline: + return UnderlineInputBorder( + borderSide: BorderSide( + color: focused + ? context.colorScheme.primary + : context.colorScheme.outline, + width: borderWidth, + ), + ); + } + } +} + +enum AppTextFieldBorderType { + none, + outline, + underline, +} diff --git a/style/lib/theme/app_theme_builder.dart b/style/lib/theme/app_theme_builder.dart index 03b7e2e5..fdfa6fa7 100644 --- a/style/lib/theme/app_theme_builder.dart +++ b/style/lib/theme/app_theme_builder.dart @@ -29,6 +29,7 @@ class AppThemeBuilder { scaffoldBackgroundColor: colorScheme.surface, appBarTheme: AppBarTheme( backgroundColor: colorScheme.surface, + centerTitle: true, surfaceTintColor: colorScheme.surface, foregroundColor: colorScheme.textPrimary, scrolledUnderElevation: 3, @@ -43,7 +44,7 @@ class AppThemeBuilder { brightness: colorScheme.brightness, primaryColor: colorScheme.primary, primaryContrastingColor: colorScheme.onPrimary, - barBackgroundColor: colorScheme.barColor, + barBackgroundColor: colorScheme.surface, scaffoldBackgroundColor: colorScheme.surface, textTheme: CupertinoTextThemeData( primaryColor: colorScheme.textPrimary, diff --git a/style/lib/theme/colors.dart b/style/lib/theme/colors.dart index c36d0cc9..09a4215b 100644 --- a/style/lib/theme/colors.dart +++ b/style/lib/theme/colors.dart @@ -20,9 +20,6 @@ class AppColors { static const surfaceLightColor = Color(0xFFFFFFFF); static const surfaceDarkColor = Color(0xFF000000); - static const barLightColor = Color(0xE6FFFFFF); - static const barDarkColor = Color(0xE6000000); - static const textPrimaryLightColor = Color(0xDE000000); static const textSecondaryLightColor = Color(0x99000000); static const textDisabledLightColor = Color(0x66000000); diff --git a/style/lib/theme/theme.dart b/style/lib/theme/theme.dart index 71531a21..e9a320ef 100644 --- a/style/lib/theme/theme.dart +++ b/style/lib/theme/theme.dart @@ -32,7 +32,6 @@ class AppColorScheme { final Color outline; final Color textPrimary; final Color textSecondary; - final Color barColor; final Color textDisabled; final Color outlineInverse; final Color textInversePrimary; @@ -66,7 +65,6 @@ class AppColorScheme { required this.textDisabled, required this.outlineInverse, required this.textInversePrimary, - required this.barColor, required this.textInverseSecondary, required this.textInverseDisabled, required this.containerNormalInverse, @@ -120,7 +118,6 @@ final appColorSchemeLight = AppColorScheme( onPrimary: AppColors.textPrimaryDarkColor, onSecondary: AppColors.textSecondaryDarkColor, onDisabled: AppColors.textDisabledLightColor, - barColor: AppColors.barLightColor, brightness: Brightness.light, ); @@ -152,6 +149,5 @@ final appColorSchemeDark = AppColorScheme( onPrimary: AppColors.textPrimaryDarkColor, onSecondary: AppColors.textSecondaryDarkColor, onDisabled: AppColors.textDisabledDarkColor, - barColor: AppColors.barDarkColor, brightness: Brightness.dark, ); From 73cccba1c16895c8cb92974a0becd33e267e08d6 Mon Sep 17 00:00:00 2001 From: Pratik-canopas Date: Fri, 3 Jan 2025 12:28:05 +0530 Subject: [PATCH 05/24] Update dependencies --- .idea/libraries/Flutter_Plugins.xml | 25 ----------------------- app/lib/ui/flow/albums/albums_screen.dart | 3 ++- data/.flutter-plugins-dependencies | 2 +- 3 files changed, 3 insertions(+), 27 deletions(-) diff --git a/.idea/libraries/Flutter_Plugins.xml b/.idea/libraries/Flutter_Plugins.xml index b294abb3..a29bb00d 100644 --- a/.idea/libraries/Flutter_Plugins.xml +++ b/.idea/libraries/Flutter_Plugins.xml @@ -30,31 +30,6 @@ - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/lib/ui/flow/albums/albums_screen.dart b/app/lib/ui/flow/albums/albums_screen.dart index e134770b..cbef8f3b 100644 --- a/app/lib/ui/flow/albums/albums_screen.dart +++ b/app/lib/ui/flow/albums/albums_screen.dart @@ -199,7 +199,8 @@ class AlbumItem extends StatelessWidget { path: '', type: AppMediaType.image, sources: [album.source], - ), size: Size(300, 300), + ), + size: Size(300, 300), ), ), const SizedBox(height: 10), diff --git a/data/.flutter-plugins-dependencies b/data/.flutter-plugins-dependencies index 8979169c..5f2bffce 100644 --- a/data/.flutter-plugins-dependencies +++ b/data/.flutter-plugins-dependencies @@ -1 +1 @@ -{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"flutter_local_notifications","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/flutter_local_notifications-18.0.1/","native_build":true,"dependencies":[]},{"name":"google_sign_in_ios","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/google_sign_in_ios-5.7.8/","shared_darwin_source":true,"native_build":true,"dependencies":[]},{"name":"package_info_plus","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/package_info_plus-8.1.1/","native_build":true,"dependencies":[]},{"name":"path_provider_foundation","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/path_provider_foundation-2.4.0/","shared_darwin_source":true,"native_build":true,"dependencies":[]},{"name":"photo_manager","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/photo_manager-3.6.2/","native_build":true,"dependencies":[]},{"name":"shared_preferences_foundation","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/shared_preferences_foundation-2.5.3/","shared_darwin_source":true,"native_build":true,"dependencies":[]},{"name":"sqflite_darwin","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/sqflite_darwin-2.4.1/","shared_darwin_source":true,"native_build":true,"dependencies":[]},{"name":"url_launcher_ios","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/url_launcher_ios-6.3.2/","native_build":true,"dependencies":[]}],"android":[{"name":"flutter_local_notifications","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/flutter_local_notifications-18.0.1/","native_build":true,"dependencies":[]},{"name":"google_sign_in_android","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/google_sign_in_android-6.1.33/","native_build":true,"dependencies":[]},{"name":"package_info_plus","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/package_info_plus-8.1.1/","native_build":true,"dependencies":[]},{"name":"path_provider_android","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/path_provider_android-2.2.12/","native_build":true,"dependencies":[]},{"name":"photo_manager","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/photo_manager-3.6.2/","native_build":true,"dependencies":[]},{"name":"shared_preferences_android","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/shared_preferences_android-2.3.3/","native_build":true,"dependencies":[]},{"name":"sqflite_android","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/sqflite_android-2.4.0/","native_build":true,"dependencies":[]},{"name":"url_launcher_android","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/url_launcher_android-6.3.14/","native_build":true,"dependencies":[]}],"macos":[{"name":"flutter_local_notifications","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/flutter_local_notifications-18.0.1/","native_build":true,"dependencies":[]},{"name":"google_sign_in_ios","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/google_sign_in_ios-5.7.8/","shared_darwin_source":true,"native_build":true,"dependencies":[]},{"name":"package_info_plus","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/package_info_plus-8.1.1/","native_build":true,"dependencies":[]},{"name":"path_provider_foundation","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/path_provider_foundation-2.4.0/","shared_darwin_source":true,"native_build":true,"dependencies":[]},{"name":"photo_manager","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/photo_manager-3.6.2/","native_build":true,"dependencies":[]},{"name":"shared_preferences_foundation","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/shared_preferences_foundation-2.5.3/","shared_darwin_source":true,"native_build":true,"dependencies":[]},{"name":"sqflite_darwin","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/sqflite_darwin-2.4.1/","shared_darwin_source":true,"native_build":true,"dependencies":[]},{"name":"url_launcher_macos","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/url_launcher_macos-3.2.2/","native_build":true,"dependencies":[]}],"linux":[{"name":"flutter_local_notifications_linux","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/flutter_local_notifications_linux-5.0.0/","native_build":false,"dependencies":[]},{"name":"package_info_plus","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/package_info_plus-8.1.1/","native_build":false,"dependencies":[]},{"name":"path_provider_linux","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/path_provider_linux-2.2.1/","native_build":false,"dependencies":[]},{"name":"shared_preferences_linux","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/shared_preferences_linux-2.4.1/","native_build":false,"dependencies":["path_provider_linux"]},{"name":"url_launcher_linux","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/url_launcher_linux-3.2.1/","native_build":true,"dependencies":[]}],"windows":[{"name":"package_info_plus","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/package_info_plus-8.1.1/","native_build":false,"dependencies":[]},{"name":"path_provider_windows","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/path_provider_windows-2.3.0/","native_build":false,"dependencies":[]},{"name":"shared_preferences_windows","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/shared_preferences_windows-2.4.1/","native_build":false,"dependencies":["path_provider_windows"]},{"name":"url_launcher_windows","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/url_launcher_windows-3.1.3/","native_build":true,"dependencies":[]}],"web":[{"name":"google_sign_in_web","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/google_sign_in_web-0.12.4+3/","dependencies":[]},{"name":"package_info_plus","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/package_info_plus-8.1.1/","dependencies":[]},{"name":"shared_preferences_web","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/shared_preferences_web-2.4.2/","dependencies":[]},{"name":"url_launcher_web","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/url_launcher_web-2.3.3/","dependencies":[]}]},"dependencyGraph":[{"name":"flutter_local_notifications","dependencies":["flutter_local_notifications_linux"]},{"name":"flutter_local_notifications_linux","dependencies":[]},{"name":"google_sign_in","dependencies":["google_sign_in_android","google_sign_in_ios","google_sign_in_web"]},{"name":"google_sign_in_android","dependencies":[]},{"name":"google_sign_in_ios","dependencies":[]},{"name":"google_sign_in_web","dependencies":[]},{"name":"package_info_plus","dependencies":[]},{"name":"path_provider","dependencies":["path_provider_android","path_provider_foundation","path_provider_linux","path_provider_windows"]},{"name":"path_provider_android","dependencies":[]},{"name":"path_provider_foundation","dependencies":[]},{"name":"path_provider_linux","dependencies":[]},{"name":"path_provider_windows","dependencies":[]},{"name":"photo_manager","dependencies":[]},{"name":"shared_preferences","dependencies":["shared_preferences_android","shared_preferences_foundation","shared_preferences_linux","shared_preferences_web","shared_preferences_windows"]},{"name":"shared_preferences_android","dependencies":[]},{"name":"shared_preferences_foundation","dependencies":[]},{"name":"shared_preferences_linux","dependencies":["path_provider_linux"]},{"name":"shared_preferences_web","dependencies":[]},{"name":"shared_preferences_windows","dependencies":["path_provider_windows"]},{"name":"sqflite","dependencies":["sqflite_android","sqflite_darwin"]},{"name":"sqflite_android","dependencies":[]},{"name":"sqflite_darwin","dependencies":[]},{"name":"url_launcher","dependencies":["url_launcher_android","url_launcher_ios","url_launcher_linux","url_launcher_macos","url_launcher_web","url_launcher_windows"]},{"name":"url_launcher_android","dependencies":[]},{"name":"url_launcher_ios","dependencies":[]},{"name":"url_launcher_linux","dependencies":[]},{"name":"url_launcher_macos","dependencies":[]},{"name":"url_launcher_web","dependencies":[]},{"name":"url_launcher_windows","dependencies":[]}],"date_created":"2025-01-02 09:46:53.659174","version":"3.27.1","swift_package_manager_enabled":false} \ No newline at end of file +{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"flutter_local_notifications","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/flutter_local_notifications-18.0.1/","native_build":true,"dependencies":[]},{"name":"google_sign_in_ios","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/google_sign_in_ios-5.7.8/","shared_darwin_source":true,"native_build":true,"dependencies":[]},{"name":"package_info_plus","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/package_info_plus-8.1.1/","native_build":true,"dependencies":[]},{"name":"path_provider_foundation","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/path_provider_foundation-2.4.0/","shared_darwin_source":true,"native_build":true,"dependencies":[]},{"name":"photo_manager","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/photo_manager-3.6.2/","native_build":true,"dependencies":[]},{"name":"shared_preferences_foundation","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/shared_preferences_foundation-2.5.3/","shared_darwin_source":true,"native_build":true,"dependencies":[]},{"name":"sqflite_darwin","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/sqflite_darwin-2.4.1/","shared_darwin_source":true,"native_build":true,"dependencies":[]},{"name":"url_launcher_ios","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/url_launcher_ios-6.3.2/","native_build":true,"dependencies":[]}],"android":[{"name":"flutter_local_notifications","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/flutter_local_notifications-18.0.1/","native_build":true,"dependencies":[]},{"name":"google_sign_in_android","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/google_sign_in_android-6.1.33/","native_build":true,"dependencies":[]},{"name":"package_info_plus","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/package_info_plus-8.1.1/","native_build":true,"dependencies":[]},{"name":"path_provider_android","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/path_provider_android-2.2.12/","native_build":true,"dependencies":[]},{"name":"photo_manager","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/photo_manager-3.6.2/","native_build":true,"dependencies":[]},{"name":"shared_preferences_android","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/shared_preferences_android-2.3.3/","native_build":true,"dependencies":[]},{"name":"sqflite_android","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/sqflite_android-2.4.0/","native_build":true,"dependencies":[]},{"name":"url_launcher_android","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/url_launcher_android-6.3.14/","native_build":true,"dependencies":[]}],"macos":[{"name":"flutter_local_notifications","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/flutter_local_notifications-18.0.1/","native_build":true,"dependencies":[]},{"name":"google_sign_in_ios","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/google_sign_in_ios-5.7.8/","shared_darwin_source":true,"native_build":true,"dependencies":[]},{"name":"package_info_plus","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/package_info_plus-8.1.1/","native_build":true,"dependencies":[]},{"name":"path_provider_foundation","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/path_provider_foundation-2.4.0/","shared_darwin_source":true,"native_build":true,"dependencies":[]},{"name":"photo_manager","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/photo_manager-3.6.2/","native_build":true,"dependencies":[]},{"name":"shared_preferences_foundation","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/shared_preferences_foundation-2.5.3/","shared_darwin_source":true,"native_build":true,"dependencies":[]},{"name":"sqflite_darwin","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/sqflite_darwin-2.4.1/","shared_darwin_source":true,"native_build":true,"dependencies":[]},{"name":"url_launcher_macos","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/url_launcher_macos-3.2.2/","native_build":true,"dependencies":[]}],"linux":[{"name":"flutter_local_notifications_linux","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/flutter_local_notifications_linux-5.0.0/","native_build":false,"dependencies":[]},{"name":"package_info_plus","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/package_info_plus-8.1.1/","native_build":false,"dependencies":[]},{"name":"path_provider_linux","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/path_provider_linux-2.2.1/","native_build":false,"dependencies":[]},{"name":"shared_preferences_linux","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/shared_preferences_linux-2.4.1/","native_build":false,"dependencies":["path_provider_linux"]},{"name":"url_launcher_linux","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/url_launcher_linux-3.2.1/","native_build":true,"dependencies":[]}],"windows":[{"name":"package_info_plus","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/package_info_plus-8.1.1/","native_build":false,"dependencies":[]},{"name":"path_provider_windows","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/path_provider_windows-2.3.0/","native_build":false,"dependencies":[]},{"name":"shared_preferences_windows","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/shared_preferences_windows-2.4.1/","native_build":false,"dependencies":["path_provider_windows"]},{"name":"url_launcher_windows","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/url_launcher_windows-3.1.3/","native_build":true,"dependencies":[]}],"web":[{"name":"google_sign_in_web","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/google_sign_in_web-0.12.4+3/","dependencies":[]},{"name":"package_info_plus","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/package_info_plus-8.1.1/","dependencies":[]},{"name":"shared_preferences_web","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/shared_preferences_web-2.4.2/","dependencies":[]},{"name":"url_launcher_web","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/url_launcher_web-2.3.3/","dependencies":[]}]},"dependencyGraph":[{"name":"flutter_local_notifications","dependencies":["flutter_local_notifications_linux"]},{"name":"flutter_local_notifications_linux","dependencies":[]},{"name":"google_sign_in","dependencies":["google_sign_in_android","google_sign_in_ios","google_sign_in_web"]},{"name":"google_sign_in_android","dependencies":[]},{"name":"google_sign_in_ios","dependencies":[]},{"name":"google_sign_in_web","dependencies":[]},{"name":"package_info_plus","dependencies":[]},{"name":"path_provider","dependencies":["path_provider_android","path_provider_foundation","path_provider_linux","path_provider_windows"]},{"name":"path_provider_android","dependencies":[]},{"name":"path_provider_foundation","dependencies":[]},{"name":"path_provider_linux","dependencies":[]},{"name":"path_provider_windows","dependencies":[]},{"name":"photo_manager","dependencies":[]},{"name":"shared_preferences","dependencies":["shared_preferences_android","shared_preferences_foundation","shared_preferences_linux","shared_preferences_web","shared_preferences_windows"]},{"name":"shared_preferences_android","dependencies":[]},{"name":"shared_preferences_foundation","dependencies":[]},{"name":"shared_preferences_linux","dependencies":["path_provider_linux"]},{"name":"shared_preferences_web","dependencies":[]},{"name":"shared_preferences_windows","dependencies":["path_provider_windows"]},{"name":"sqflite","dependencies":["sqflite_android","sqflite_darwin"]},{"name":"sqflite_android","dependencies":[]},{"name":"sqflite_darwin","dependencies":[]},{"name":"url_launcher","dependencies":["url_launcher_android","url_launcher_ios","url_launcher_linux","url_launcher_macos","url_launcher_web","url_launcher_windows"]},{"name":"url_launcher_android","dependencies":[]},{"name":"url_launcher_ios","dependencies":[]},{"name":"url_launcher_linux","dependencies":[]},{"name":"url_launcher_macos","dependencies":[]},{"name":"url_launcher_web","dependencies":[]},{"name":"url_launcher_windows","dependencies":[]}],"date_created":"2025-01-03 12:26:22.948749","version":"3.27.1","swift_package_manager_enabled":false} \ No newline at end of file From db3077aa2eae9a4519041a36235e0967a79646bd Mon Sep 17 00:00:00 2001 From: Pratik-canopas Date: Fri, 3 Jan 2025 17:57:31 +0530 Subject: [PATCH 06/24] Improvement --- .idea/libraries/Flutter_Plugins.xml | 25 ++++ app/lib/components/app_page.dart | 2 + app/lib/ui/app.dart | 2 +- .../ui/flow/albums/add/add_album_screen.dart | 27 +++-- .../albums/add/add_album_state_notifier.dart | 2 + app/lib/ui/flow/albums/albums_screen.dart | 47 ++++++-- .../ui/flow/albums/albums_view_notifier.dart | 7 +- .../media_list/album_media_list_screen.dart | 4 +- .../album_media_list_state_notifier.dart | 48 ++++---- app/lib/ui/flow/main/main_screen.dart | 111 +++++++++--------- .../media_selection_state_notifier.dart | 6 +- data/lib/services/dropbox_services.dart | 19 ++- 12 files changed, 187 insertions(+), 113 deletions(-) diff --git a/.idea/libraries/Flutter_Plugins.xml b/.idea/libraries/Flutter_Plugins.xml index a29bb00d..b294abb3 100644 --- a/.idea/libraries/Flutter_Plugins.xml +++ b/.idea/libraries/Flutter_Plugins.xml @@ -30,6 +30,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/lib/components/app_page.dart b/app/lib/components/app_page.dart index 8b2da124..aa2704dd 100644 --- a/app/lib/components/app_page.dart +++ b/app/lib/components/app_page.dart @@ -92,6 +92,8 @@ class AppPage extends StatelessWidget { : AppBar( centerTitle: true, backgroundColor: barBackgroundColor, + scrolledUnderElevation: 0.5, + shadowColor: context.colorScheme.textDisabled, title: titleWidget ?? _title(context), actions: [...?actions, const SizedBox(width: 16)], leading: leading, diff --git a/app/lib/ui/app.dart b/app/lib/ui/app.dart index 102180ef..b60a23bf 100644 --- a/app/lib/ui/app.dart +++ b/app/lib/ui/app.dart @@ -44,7 +44,7 @@ class _CloudGalleryAppState extends ConsumerState { routes: $appRoutes, redirect: (context, state) { if (state.uri.path.contains('/auth')) { - return '/'; + return AppRoutePath.accounts; } return null; }, diff --git a/app/lib/ui/flow/albums/add/add_album_screen.dart b/app/lib/ui/flow/albums/add/add_album_screen.dart index 14407f7b..cd7000ec 100644 --- a/app/lib/ui/flow/albums/add/add_album_screen.dart +++ b/app/lib/ui/flow/albums/add/add_album_screen.dart @@ -5,6 +5,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:style/buttons/action_button.dart'; import 'package:style/extensions/context_extensions.dart'; +import 'package:style/indicators/circular_progress_indicator.dart'; import 'package:style/text/app_text_field.dart'; import 'package:style/text/app_text_style.dart'; import '../../../../components/app_page.dart'; @@ -66,16 +67,22 @@ class _AddAlbumScreenState extends ConsumerState { title: context.l10n.add_album_screen_title, body: _body(context: context, state: state), actions: [ - ActionButton( - onPressed: state.allowSave ? _notifier.createAlbum : null, - icon: Icon( - Icons.check, - size: 24, - color: state.allowSave - ? context.colorScheme.textPrimary - : context.colorScheme.textDisabled, - ), - ), + state.loading + ? const SizedBox( + height: 24, + width: 24, + child: AppCircularProgressIndicator(), + ) + : ActionButton( + onPressed: state.allowSave ? _notifier.createAlbum : null, + icon: Icon( + Icons.check, + size: 24, + color: state.allowSave + ? context.colorScheme.textPrimary + : context.colorScheme.textDisabled, + ), + ), ], ); } diff --git a/app/lib/ui/flow/albums/add/add_album_state_notifier.dart b/app/lib/ui/flow/albums/add/add_album_state_notifier.dart index 51d5730f..5654556d 100644 --- a/app/lib/ui/flow/albums/add/add_album_state_notifier.dart +++ b/app/lib/ui/flow/albums/add/add_album_state_notifier.dart @@ -63,6 +63,8 @@ class AddAlbumStateNotifier extends StateNotifier { AddAlbumsState( albumNameController: TextEditingController(text: editAlbum?.name), mediaSource: editAlbum?.source ?? AppMediaSource.local, + googleAccount: googleAccount, + dropboxAccount: dropboxAccount, ), ); diff --git a/app/lib/ui/flow/albums/albums_screen.dart b/app/lib/ui/flow/albums/albums_screen.dart index cbef8f3b..2f969dd4 100644 --- a/app/lib/ui/flow/albums/albums_screen.dart +++ b/app/lib/ui/flow/albums/albums_screen.dart @@ -3,6 +3,7 @@ import 'package:data/models/media/media.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_svg/flutter_svg.dart'; import 'package:go_router/go_router.dart'; import 'package:style/animations/fade_in_switcher.dart'; import 'package:style/animations/on_tap_scale.dart'; @@ -18,6 +19,7 @@ import '../../../components/place_holder_screen.dart'; import '../../../components/snack_bar.dart'; import '../../../components/thumbnail_builder.dart'; import '../../../domain/extensions/context_extensions.dart'; +import '../../../gen/assets.gen.dart'; import '../../navigation/app_route.dart'; import 'albums_view_notifier.dart'; @@ -74,7 +76,7 @@ class _AlbumsScreenState extends ConsumerState { Widget _body({required BuildContext context}) { final state = ref.watch(albumStateNotifierProvider); - if (state.loading) { + if (state.loading && state.albums.isEmpty) { return const Center(child: AppCircularProgressIndicator()); } else if (state.error != null) { return ErrorScreen( @@ -94,21 +96,22 @@ class _AlbumsScreenState extends ConsumerState { } return GridView( - padding: EdgeInsets.all(16), + padding: EdgeInsets.all(8), gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 2, childAspectRatio: 0.9, - crossAxisSpacing: 16, + crossAxisSpacing: 8, mainAxisSpacing: 16, ), children: state.albums .map( (album) => AlbumItem( album: album, - onTap: () { - AlbumMediaListRoute( + onTap: () async { + await AlbumMediaListRoute( $extra: album, ).push(context); + _notifier.loadAlbums(); }, onLongTap: () { showAppSheet( @@ -204,10 +207,36 @@ class AlbumItem extends StatelessWidget { ), ), const SizedBox(height: 10), - Text( - album.name, - style: AppTextStyles.subtitle1.copyWith( - color: context.colorScheme.textPrimary, + Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: Row( + children: [ + if (album.source == AppMediaSource.dropbox) ...[ + SvgPicture.asset( + Assets.images.icDropbox, + width: 18, + height: 18, + ), + const SizedBox(width: 4), + ], + if (album.source == AppMediaSource.googleDrive) ...[ + SvgPicture.asset( + Assets.images.icGoogleDrive, + width: 18, + height: 18, + ), + const SizedBox(width: 4), + ], + Expanded( + child: Text( + album.name, + style: AppTextStyles.subtitle1.copyWith( + color: context.colorScheme.textPrimary, + ), + overflow: TextOverflow.ellipsis, + ), + ), + ], ), ), ], diff --git a/app/lib/ui/flow/albums/albums_view_notifier.dart b/app/lib/ui/flow/albums/albums_view_notifier.dart index 9ee5c922..0f0e9f14 100644 --- a/app/lib/ui/flow/albums/albums_view_notifier.dart +++ b/app/lib/ui/flow/albums/albums_view_notifier.dart @@ -48,7 +48,12 @@ class AlbumStateNotifier extends StateNotifier { this._logger, GoogleSignInAccount? googleAccount, DropboxAccount? dropboxAccount, - ) : super(const AlbumsState()) { + ) : super( + AlbumsState( + googleAccount: googleAccount, + dropboxAccount: dropboxAccount, + ), + ) { loadAlbums(); } diff --git a/app/lib/ui/flow/albums/media_list/album_media_list_screen.dart b/app/lib/ui/flow/albums/media_list/album_media_list_screen.dart index 75fa6b87..7bcbdb43 100644 --- a/app/lib/ui/flow/albums/media_list/album_media_list_screen.dart +++ b/app/lib/ui/flow/albums/media_list/album_media_list_screen.dart @@ -63,7 +63,7 @@ class _AlbumMediaListScreenState extends ConsumerState { .push(context); if (res != null && res is List) { - await _notifier.addMedias(res); + await _notifier.updateAlbumMedias(medias: res); } }, icon: Icon( @@ -76,7 +76,7 @@ class _AlbumMediaListScreenState extends ConsumerState { title: context.l10n.common_edit, onPressed: () async { context.pop(); - final res = await AddAlbumRoute($extra: widget.album) + final res = await AddAlbumRoute($extra: state.album) .push(context); if (res == true) { await _notifier.reloadAlbum(); diff --git a/app/lib/ui/flow/albums/media_list/album_media_list_state_notifier.dart b/app/lib/ui/flow/albums/media_list/album_media_list_state_notifier.dart index ea8f614d..608a60d4 100644 --- a/app/lib/ui/flow/albums/media_list/album_media_list_state_notifier.dart +++ b/app/lib/ui/flow/albums/media_list/album_media_list_state_notifier.dart @@ -112,16 +112,16 @@ class AlbumMediaListStateNotifier extends StateNotifier { } } - Future addMedias(List medias) async { + Future updateAlbumMedias({ + required List medias, + bool append = true, + }) async { try { state = state.copyWith(actionError: null); if (state.album.source == AppMediaSource.local) { await _localMediaService.updateAlbum( state.album.copyWith( - medias: [ - ...state.album.medias, - ...medias, - ], + medias: append ? [...state.album.medias, ...medias] : medias, ), ); } else if (state.album.source == AppMediaSource.googleDrive) { @@ -132,19 +132,13 @@ class AlbumMediaListStateNotifier extends StateNotifier { await _googleDriveService.updateAlbum( folderId: _backupFolderId!, album: state.album.copyWith( - medias: [ - ...state.album.medias, - ...medias, - ], + medias: append ? [...state.album.medias, ...medias] : medias, ), ); } else if (state.album.source == AppMediaSource.dropbox) { await _dropboxService.updateAlbum( state.album.copyWith( - medias: [ - ...state.album.medias, - ...medias, - ], + medias: append ? [...state.album.medias, ...medias] : medias, ), ); } @@ -160,6 +154,7 @@ class AlbumMediaListStateNotifier extends StateNotifier { } Future loadMedia({bool reload = false}) async { + ///TODO: remove deleted media try { if (state.loading) return; @@ -175,14 +170,13 @@ class AlbumMediaListStateNotifier extends StateNotifier { final loadedMediaIds = state.medias.map((e) => e.id).toList(); final moreMediaIds = state.album.medias .where((element) => !loadedMediaIds.contains(element)) + .take(30) .toList(); medias = await Future.wait( - moreMediaIds - .take(moreMediaIds.length > 30 ? 30 : moreMediaIds.length) - .map( - (id) => _localMediaService.getMedia(id: id), - ), + moreMediaIds.map( + (id) => _localMediaService.getMedia(id: id), + ), ).then( (value) => value.nonNulls.toList(), ); @@ -191,13 +185,12 @@ class AlbumMediaListStateNotifier extends StateNotifier { state.medias.map((e) => e.driveMediaRefId).nonNulls.toList(); final moreMediaIds = state.album.medias .where((element) => !loadedMediaIds.contains(element)) + .take(30) .toList(); medias = await Future.wait( - moreMediaIds - .take(moreMediaIds.length > 30 ? 30 : moreMediaIds.length) - .map( - (id) => _googleDriveService.getMedia(id: id), - ), + moreMediaIds.map( + (id) => _googleDriveService.getMedia(id: id), + ), ).then( (value) => value.nonNulls.toList(), ); @@ -206,13 +199,12 @@ class AlbumMediaListStateNotifier extends StateNotifier { state.medias.map((e) => e.dropboxMediaRefId).nonNulls.toList(); final moreMediaIds = state.album.medias .where((element) => !loadedMediaIds.contains(element)) + .take(30) .toList(); medias = await Future.wait( - moreMediaIds - .take(moreMediaIds.length > 30 ? 30 : moreMediaIds.length) - .map( - (id) => _dropboxService.getMedia(id: id), - ), + moreMediaIds.map( + (id) => _dropboxService.getMedia(id: id), + ), ).then( (value) => value.nonNulls.toList(), ); diff --git a/app/lib/ui/flow/main/main_screen.dart b/app/lib/ui/flow/main/main_screen.dart index 688fab5e..a1375dc3 100644 --- a/app/lib/ui/flow/main/main_screen.dart +++ b/app/lib/ui/flow/main/main_screen.dart @@ -44,60 +44,34 @@ class _MainScreenState extends State { ), ]; - return Column( - children: [ - Expanded(child: widget.navigationShell), - (!kIsWeb && Platform.isIOS) - ? CupertinoTabBar( - currentIndex: widget.navigationShell.currentIndex, - activeColor: context.colorScheme.primary, - inactiveColor: context.colorScheme.textDisabled, - onTap: (index) => _goBranch( - index: index, - context: context, - ), - backgroundColor: context.colorScheme.surface, - border: Border( - top: BorderSide( - color: context.colorScheme.outline, - width: 1, + return Material( + color: context.colorScheme.surface, + child: Column( + children: [ + Expanded(child: widget.navigationShell), + (!kIsWeb && Platform.isIOS) + ? CupertinoTabBar( + currentIndex: widget.navigationShell.currentIndex, + activeColor: context.colorScheme.primary, + inactiveColor: context.colorScheme.textDisabled, + onTap: (index) => _goBranch( + index: index, + context: context, ), - ), - items: tabs - .map( - (e) => BottomNavigationBarItem( - icon: Icon( - e.icon, - color: context.colorScheme.textDisabled, - size: 22, - ), - label: e.label, - activeIcon: Icon( - e.activeIcon, - color: context.colorScheme.primary, - size: 24, - ), - ), - ) - .toList(), - ) - : Container( - decoration: BoxDecoration( + backgroundColor: context.colorScheme.surface, border: Border( top: BorderSide( color: context.colorScheme.outline, width: 1, ), ), - ), - child: BottomNavigationBar( items: tabs .map( (e) => BottomNavigationBarItem( icon: Icon( e.icon, color: context.colorScheme.textDisabled, - size: 24, + size: 22, ), label: e.label, activeIcon: Icon( @@ -108,21 +82,50 @@ class _MainScreenState extends State { ), ) .toList(), - currentIndex: widget.navigationShell.currentIndex, - selectedItemColor: context.colorScheme.primary, - unselectedItemColor: context.colorScheme.textDisabled, - backgroundColor: context.colorScheme.surface, - type: BottomNavigationBarType.fixed, - selectedFontSize: 12, - unselectedFontSize: 12, - elevation: 0, - onTap: (index) => _goBranch( - index: index, - context: context, + ) + : Container( + decoration: BoxDecoration( + color: context.colorScheme.surface, + border: Border( + top: BorderSide( + color: context.colorScheme.outline, + ), + ), + ), + child: BottomNavigationBar( + items: tabs + .map( + (e) => BottomNavigationBarItem( + icon: Icon( + e.icon, + color: context.colorScheme.textDisabled, + size: 24, + ), + label: e.label, + activeIcon: Icon( + e.activeIcon, + color: context.colorScheme.primary, + size: 24, + ), + ), + ) + .toList(), + currentIndex: widget.navigationShell.currentIndex, + selectedItemColor: context.colorScheme.primary, + unselectedItemColor: context.colorScheme.textDisabled, + backgroundColor: context.colorScheme.surface, + type: BottomNavigationBarType.fixed, + selectedFontSize: 12, + unselectedFontSize: 12, + elevation: 0, + onTap: (index) => _goBranch( + index: index, + context: context, + ), ), ), - ), - ], + ], + ), ); } diff --git a/app/lib/ui/flow/media_selection/media_selection_state_notifier.dart b/app/lib/ui/flow/media_selection/media_selection_state_notifier.dart index ffa25b97..3caa9dd2 100644 --- a/app/lib/ui/flow/media_selection/media_selection_state_notifier.dart +++ b/app/lib/ui/flow/media_selection/media_selection_state_notifier.dart @@ -181,12 +181,14 @@ class MediaSelectionStateNotifier extends StateNotifier { } void toggleMediaSelection(AppMedia media) { - String id = media.id; + String id; if (_source == AppMediaSource.googleDrive) { id = media.driveMediaRefId!; } else if (_source == AppMediaSource.dropbox) { id = media.dropboxMediaRefId!; + } else { + id = media.id; } if (state.selectedMedias.contains(id)) { @@ -199,7 +201,7 @@ class MediaSelectionStateNotifier extends StateNotifier { state = state.copyWith( selectedMedias: [ ...state.selectedMedias, - media.id, + id, ], ); } diff --git a/data/lib/services/dropbox_services.dart b/data/lib/services/dropbox_services.dart index 4c2b5bab..11b83213 100644 --- a/data/lib/services/dropbox_services.dart +++ b/data/lib/services/dropbox_services.dart @@ -131,7 +131,11 @@ class DropboxService extends CloudProviderService { nextPageToken = response.data['cursor']; medias.addAll( (response.data['entries'] as List) - .where((element) => element['.tag'] == 'file') + .where( + (element) => + element['.tag'] == 'file' && + element['name'] != 'Albums.json', + ) .map((e) => AppMedia.fromDropboxJson(json: e)) .toList(), ); @@ -177,7 +181,8 @@ class DropboxService extends CloudProviderService { ); if (response.statusCode == 200) { final files = (response.data['entries'] as List).where( - (element) => element['.tag'] == 'file', + (element) => + element['.tag'] == 'file' && element['name'] != 'Albums.json', ); final metadataResponses = await Future.wait( @@ -378,6 +383,8 @@ class DropboxService extends CloudProviderService { ); } + // ALBUM --------------------------------------------------------------------- + Future> getAlbums() async { try { final res = await _dropboxAuthenticatedDio.req( @@ -417,8 +424,8 @@ class DropboxService extends CloudProviderService { mode: 'overwrite', autoRename: false, content: AppMediaContent( - stream: Stream.value(utf8.encode(jsonEncode(album))), - length: utf8.encode(jsonEncode(album)).length, + stream: Stream.value(utf8.encode(jsonEncode(albums))), + length: utf8.encode(jsonEncode(albums)).length, contentType: 'application/octet-stream', ), filePath: "/${ProviderConstants.backupFolderName}/Albums.json", @@ -444,7 +451,7 @@ class DropboxService extends CloudProviderService { content: AppMediaContent( stream: Stream.value(utf8.encode(jsonEncode(albums))), length: utf8.encode(jsonEncode(albums)).length, - contentType: 'application/json', + contentType: 'application/octet-stream', ), filePath: "/${ProviderConstants.backupFolderName}/Albums.json", ), @@ -478,7 +485,7 @@ class DropboxService extends CloudProviderService { content: AppMediaContent( stream: Stream.value(utf8.encode(jsonEncode(albums))), length: utf8.encode(jsonEncode(albums)).length, - contentType: 'application/json', + contentType: 'application/octet-stream', ), filePath: "/${ProviderConstants.backupFolderName}/Albums.json", ), From 70f33d894415d86a3df5bbf6d11786e6393792e5 Mon Sep 17 00:00:00 2001 From: Pratik-canopas Date: Tue, 7 Jan 2025 11:31:14 +0530 Subject: [PATCH 07/24] Fix google drive album thumbnail load --- app/lib/ui/flow/albums/albums_screen.dart | 12 +++--- .../ui/flow/albums/albums_view_notifier.dart | 42 +++++++++++++++++++ .../albums/albums_view_notifier.freezed.dart | 33 ++++++++++++++- 3 files changed, 78 insertions(+), 9 deletions(-) diff --git a/app/lib/ui/flow/albums/albums_screen.dart b/app/lib/ui/flow/albums/albums_screen.dart index 2f969dd4..8c65864d 100644 --- a/app/lib/ui/flow/albums/albums_screen.dart +++ b/app/lib/ui/flow/albums/albums_screen.dart @@ -107,6 +107,7 @@ class _AlbumsScreenState extends ConsumerState { .map( (album) => AlbumItem( album: album, + media: state.medias[album.id], onTap: () async { await AlbumMediaListRoute( $extra: album, @@ -161,12 +162,14 @@ class _AlbumsScreenState extends ConsumerState { class AlbumItem extends StatelessWidget { final Album album; + final AppMedia? media; final void Function() onTap; final void Function() onLongTap; const AlbumItem({ super.key, required this.album, + required this.media, required this.onTap, required this.onLongTap, }); @@ -180,7 +183,7 @@ class AlbumItem extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded( - child: album.medias.isEmpty + child: media == null ? Container( width: double.infinity, decoration: BoxDecoration( @@ -197,12 +200,7 @@ class AlbumItem extends StatelessWidget { ), ) : AppMediaImage( - media: AppMedia( - id: album.medias.first, - path: '', - type: AppMediaType.image, - sources: [album.source], - ), + media: media!, size: Size(300, 300), ), ), diff --git a/app/lib/ui/flow/albums/albums_view_notifier.dart b/app/lib/ui/flow/albums/albums_view_notifier.dart index 0f0e9f14..da6a206c 100644 --- a/app/lib/ui/flow/albums/albums_view_notifier.dart +++ b/app/lib/ui/flow/albums/albums_view_notifier.dart @@ -87,11 +87,28 @@ class AlbumStateNotifier extends StateNotifier { } } + /// Lookups for the first media in the album that is available + Future<({String id, AppMedia media})?> _getThumbnailMedia({ + required Album album, + required Future Function(String id) fetchMedia, + }) async { + if (album.medias.isEmpty) return null; + + for (final id in album.medias) { + final media = await fetchMedia.call(id); + if (media != null) { + return (id: album.id, media: media); + } + } + return null; + } + Future loadAlbums() async { if (state.loading) return; state = state.copyWith(loading: true, error: null); try { + _backupFolderId ??= await _googleDriveService.getBackUpFolderId(); final res = await Future.wait([ _localMediaService.getAlbums(), (state.googleAccount != null && _backupFolderId != null) @@ -102,8 +119,32 @@ class AlbumStateNotifier extends StateNotifier { : Future.value([]), ]); + final medias = await Future.wait([ + for (Album album in res[0]) + _getThumbnailMedia( + album: album, + fetchMedia: (id) => _localMediaService.getMedia(id: id), + ), + for (final album in res[1]) + _getThumbnailMedia( + album: album, + fetchMedia: (id) => _googleDriveService.getMedia(id: id), + ), + for (final album in res[2]) + _getThumbnailMedia( + album: album, + fetchMedia: (id) => _dropboxService.getMedia(id: id), + ), + ]).then( + (value) => { + for (final item in value) + if (item != null) item.id: item.media, + }, + ); + state = state.copyWith( albums: [...res[0], ...res[1], ...res[2]], + medias: medias, loading: false, ); } catch (e, s) { @@ -153,6 +194,7 @@ class AlbumsState with _$AlbumsState { const factory AlbumsState({ @Default(false) bool loading, @Default([]) List albums, + @Default({}) Map medias, GoogleSignInAccount? googleAccount, DropboxAccount? dropboxAccount, Object? error, diff --git a/app/lib/ui/flow/albums/albums_view_notifier.freezed.dart b/app/lib/ui/flow/albums/albums_view_notifier.freezed.dart index 215ac6f7..9fa76e8b 100644 --- a/app/lib/ui/flow/albums/albums_view_notifier.freezed.dart +++ b/app/lib/ui/flow/albums/albums_view_notifier.freezed.dart @@ -18,6 +18,7 @@ final _privateConstructorUsedError = UnsupportedError( mixin _$AlbumsState { bool get loading => throw _privateConstructorUsedError; List get albums => throw _privateConstructorUsedError; + Map get medias => throw _privateConstructorUsedError; GoogleSignInAccount? get googleAccount => throw _privateConstructorUsedError; DropboxAccount? get dropboxAccount => throw _privateConstructorUsedError; Object? get error => throw _privateConstructorUsedError; @@ -39,6 +40,7 @@ abstract class $AlbumsStateCopyWith<$Res> { $Res call( {bool loading, List albums, + Map medias, GoogleSignInAccount? googleAccount, DropboxAccount? dropboxAccount, Object? error, @@ -64,6 +66,7 @@ class _$AlbumsStateCopyWithImpl<$Res, $Val extends AlbumsState> $Res call({ Object? loading = null, Object? albums = null, + Object? medias = null, Object? googleAccount = freezed, Object? dropboxAccount = freezed, Object? error = freezed, @@ -78,6 +81,10 @@ class _$AlbumsStateCopyWithImpl<$Res, $Val extends AlbumsState> ? _value.albums : albums // ignore: cast_nullable_to_non_nullable as List, + medias: null == medias + ? _value.medias + : medias // ignore: cast_nullable_to_non_nullable + as Map, googleAccount: freezed == googleAccount ? _value.googleAccount : googleAccount // ignore: cast_nullable_to_non_nullable @@ -117,6 +124,7 @@ abstract class _$$AlbumsStateImplCopyWith<$Res> $Res call( {bool loading, List albums, + Map medias, GoogleSignInAccount? googleAccount, DropboxAccount? dropboxAccount, Object? error, @@ -141,6 +149,7 @@ class __$$AlbumsStateImplCopyWithImpl<$Res> $Res call({ Object? loading = null, Object? albums = null, + Object? medias = null, Object? googleAccount = freezed, Object? dropboxAccount = freezed, Object? error = freezed, @@ -155,6 +164,10 @@ class __$$AlbumsStateImplCopyWithImpl<$Res> ? _value._albums : albums // ignore: cast_nullable_to_non_nullable as List, + medias: null == medias + ? _value._medias + : medias // ignore: cast_nullable_to_non_nullable + as Map, googleAccount: freezed == googleAccount ? _value.googleAccount : googleAccount // ignore: cast_nullable_to_non_nullable @@ -175,11 +188,13 @@ class _$AlbumsStateImpl implements _AlbumsState { const _$AlbumsStateImpl( {this.loading = false, final List albums = const [], + final Map medias = const {}, this.googleAccount, this.dropboxAccount, this.error, this.actionError}) - : _albums = albums; + : _albums = albums, + _medias = medias; @override @JsonKey() @@ -193,6 +208,15 @@ class _$AlbumsStateImpl implements _AlbumsState { return EqualUnmodifiableListView(_albums); } + final Map _medias; + @override + @JsonKey() + Map get medias { + if (_medias is EqualUnmodifiableMapView) return _medias; + // ignore: implicit_dynamic_type + return EqualUnmodifiableMapView(_medias); + } + @override final GoogleSignInAccount? googleAccount; @override @@ -204,7 +228,7 @@ class _$AlbumsStateImpl implements _AlbumsState { @override String toString() { - return 'AlbumsState(loading: $loading, albums: $albums, googleAccount: $googleAccount, dropboxAccount: $dropboxAccount, error: $error, actionError: $actionError)'; + return 'AlbumsState(loading: $loading, albums: $albums, medias: $medias, googleAccount: $googleAccount, dropboxAccount: $dropboxAccount, error: $error, actionError: $actionError)'; } @override @@ -214,6 +238,7 @@ class _$AlbumsStateImpl implements _AlbumsState { other is _$AlbumsStateImpl && (identical(other.loading, loading) || other.loading == loading) && const DeepCollectionEquality().equals(other._albums, _albums) && + const DeepCollectionEquality().equals(other._medias, _medias) && (identical(other.googleAccount, googleAccount) || other.googleAccount == googleAccount) && (identical(other.dropboxAccount, dropboxAccount) || @@ -228,6 +253,7 @@ class _$AlbumsStateImpl implements _AlbumsState { runtimeType, loading, const DeepCollectionEquality().hash(_albums), + const DeepCollectionEquality().hash(_medias), googleAccount, dropboxAccount, const DeepCollectionEquality().hash(error), @@ -246,6 +272,7 @@ abstract class _AlbumsState implements AlbumsState { const factory _AlbumsState( {final bool loading, final List albums, + final Map medias, final GoogleSignInAccount? googleAccount, final DropboxAccount? dropboxAccount, final Object? error, @@ -256,6 +283,8 @@ abstract class _AlbumsState implements AlbumsState { @override List get albums; @override + Map get medias; + @override GoogleSignInAccount? get googleAccount; @override DropboxAccount? get dropboxAccount; From 67b14e60cabe51b6ea950afe782dfa4eb96e0059 Mon Sep 17 00:00:00 2001 From: Pratik-canopas Date: Tue, 7 Jan 2025 11:31:43 +0530 Subject: [PATCH 08/24] Fix google drive album thumbnail --- data/lib/apis/google_drive/google_drive_endpoint.dart | 2 +- data/lib/domain/config.dart | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/data/lib/apis/google_drive/google_drive_endpoint.dart b/data/lib/apis/google_drive/google_drive_endpoint.dart index 73e45e72..78f819f7 100644 --- a/data/lib/apis/google_drive/google_drive_endpoint.dart +++ b/data/lib/apis/google_drive/google_drive_endpoint.dart @@ -223,7 +223,7 @@ class GoogleDriveGetEndpoint extends Endpoint { const GoogleDriveGetEndpoint({ required this.id, this.fields = - 'nextPageToken, files(id, name, description, mimeType, thumbnailLink, webContentLink, createdTime, modifiedTime, size, imageMediaMetadata, videoMediaMetadata, appProperties)', + 'id, name, description, mimeType, thumbnailLink, webContentLink, createdTime, modifiedTime, size, imageMediaMetadata, videoMediaMetadata, appProperties', }); @override diff --git a/data/lib/domain/config.dart b/data/lib/domain/config.dart index 6227587f..4a454f6f 100644 --- a/data/lib/domain/config.dart +++ b/data/lib/domain/config.dart @@ -16,5 +16,5 @@ class LocalDatabaseConstants { } class FeatureFlag { - static final googleDriveSupport = false; + static final googleDriveSupport = true; } From 72107312d517d90e6e01a88129b2b8bc7fe61887 Mon Sep 17 00:00:00 2001 From: Pratik-canopas Date: Tue, 7 Jan 2025 17:57:32 +0530 Subject: [PATCH 09/24] Add preview on albums --- app/lib/ui/flow/albums/component/album_item.dart | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 app/lib/ui/flow/albums/component/album_item.dart diff --git a/app/lib/ui/flow/albums/component/album_item.dart b/app/lib/ui/flow/albums/component/album_item.dart new file mode 100644 index 00000000..e69de29b From 4a986eeaf190205dc47c0287d856ec29e060a5fd Mon Sep 17 00:00:00 2001 From: Pratik-canopas Date: Wed, 8 Jan 2025 10:01:08 +0530 Subject: [PATCH 10/24] Album screen --- app/assets/locales/app_en.arb | 1 + app/lib/components/app_media_thumbnail.dart | 2 +- app/lib/ui/flow/albums/albums_screen.dart | 91 +---------------- .../ui/flow/albums/albums_view_notifier.dart | 15 +-- .../ui/flow/albums/component/album_item.dart | 97 +++++++++++++++++++ .../media_list/album_media_list_screen.dart | 34 ++++++- .../album_media_list_state_notifier.dart | 57 ++++++++++- .../flow/home/components/app_media_item.dart | 4 +- app/lib/ui/flow/home/home_screen.dart | 15 +-- .../ui/flow/home/home_screen_view_model.dart | 10 +- .../components/download_require_view.dart | 4 +- .../components/local_media_image_preview.dart | 4 +- .../network_image_preview.dart | 31 +++--- .../network_image_preview_view_model.dart | 25 ++++- ...work_image_preview_view_model.freezed.dart | 60 ++++++++++-- .../media_preview/media_preview_screen.dart | 16 ++- .../media_preview_view_model.dart | 22 +---- app/lib/ui/navigation/app_route.dart | 16 ++- 18 files changed, 339 insertions(+), 165 deletions(-) diff --git a/app/assets/locales/app_en.arb b/app/assets/locales/app_en.arb index 96bc1db0..d3453bb4 100644 --- a/app/assets/locales/app_en.arb +++ b/app/assets/locales/app_en.arb @@ -20,6 +20,7 @@ "common_share": "Share", "common_cancel": "Cancel", "common_retry": "Retry", + "common_remove": "Remove", "common_done": "Done", "common_not_available": "N/A", "common_open_settings": "Open Settings", diff --git a/app/lib/components/app_media_thumbnail.dart b/app/lib/components/app_media_thumbnail.dart index ec2cea2d..67659c8b 100644 --- a/app/lib/components/app_media_thumbnail.dart +++ b/app/lib/components/app_media_thumbnail.dart @@ -39,7 +39,7 @@ class AppMediaThumbnail extends StatelessWidget { radius: selected ? 4 : 0, size: constraints.biggest, media: media, - heroTag: "$heroTag${media.toString()}", + heroTag: heroTag, ), ), if (media.type.isVideo) _videoDuration(context), diff --git a/app/lib/ui/flow/albums/albums_screen.dart b/app/lib/ui/flow/albums/albums_screen.dart index 8c65864d..fc69db5c 100644 --- a/app/lib/ui/flow/albums/albums_screen.dart +++ b/app/lib/ui/flow/albums/albums_screen.dart @@ -1,27 +1,21 @@ -import 'package:data/models/album/album.dart'; -import 'package:data/models/media/media.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:flutter_svg/flutter_svg.dart'; import 'package:go_router/go_router.dart'; import 'package:style/animations/fade_in_switcher.dart'; -import 'package:style/animations/on_tap_scale.dart'; import 'package:style/buttons/action_button.dart'; import 'package:style/extensions/context_extensions.dart'; import 'package:style/indicators/circular_progress_indicator.dart'; -import 'package:style/text/app_text_style.dart'; import '../../../components/action_sheet.dart'; import '../../../components/app_page.dart'; import '../../../components/app_sheet.dart'; import '../../../components/error_screen.dart'; import '../../../components/place_holder_screen.dart'; import '../../../components/snack_bar.dart'; -import '../../../components/thumbnail_builder.dart'; import '../../../domain/extensions/context_extensions.dart'; -import '../../../gen/assets.gen.dart'; import '../../navigation/app_route.dart'; import 'albums_view_notifier.dart'; +import 'component/album_item.dart'; class AlbumsScreen extends ConsumerStatefulWidget { const AlbumsScreen({super.key}); @@ -159,86 +153,3 @@ class _AlbumsScreenState extends ConsumerState { ); } } - -class AlbumItem extends StatelessWidget { - final Album album; - final AppMedia? media; - final void Function() onTap; - final void Function() onLongTap; - - const AlbumItem({ - super.key, - required this.album, - required this.media, - required this.onTap, - required this.onLongTap, - }); - - @override - Widget build(BuildContext context) { - return OnTapScale( - onTap: onTap, - onLongTap: onLongTap, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded( - child: media == null - ? Container( - width: double.infinity, - decoration: BoxDecoration( - color: context.colorScheme.containerLowOnSurface, - borderRadius: BorderRadius.circular(8), - border: Border.all( - color: context.colorScheme.outline, - ), - ), - child: Icon( - CupertinoIcons.folder, - size: 80, - color: context.colorScheme.containerHighOnSurface, - ), - ) - : AppMediaImage( - media: media!, - size: Size(300, 300), - ), - ), - const SizedBox(height: 10), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 8.0), - child: Row( - children: [ - if (album.source == AppMediaSource.dropbox) ...[ - SvgPicture.asset( - Assets.images.icDropbox, - width: 18, - height: 18, - ), - const SizedBox(width: 4), - ], - if (album.source == AppMediaSource.googleDrive) ...[ - SvgPicture.asset( - Assets.images.icGoogleDrive, - width: 18, - height: 18, - ), - const SizedBox(width: 4), - ], - Expanded( - child: Text( - album.name, - style: AppTextStyles.subtitle1.copyWith( - color: context.colorScheme.textPrimary, - ), - overflow: TextOverflow.ellipsis, - ), - ), - ], - ), - ), - ], - ), - ); - } -} diff --git a/app/lib/ui/flow/albums/albums_view_notifier.dart b/app/lib/ui/flow/albums/albums_view_notifier.dart index da6a206c..3f265edb 100644 --- a/app/lib/ui/flow/albums/albums_view_notifier.dart +++ b/app/lib/ui/flow/albums/albums_view_notifier.dart @@ -108,7 +108,9 @@ class AlbumStateNotifier extends StateNotifier { state = state.copyWith(loading: true, error: null); try { - _backupFolderId ??= await _googleDriveService.getBackUpFolderId(); + if(state.googleAccount != null) { + _backupFolderId ??= await _googleDriveService.getBackUpFolderId(); + } final res = await Future.wait([ _localMediaService.getAlbums(), (state.googleAccount != null && _backupFolderId != null) @@ -119,6 +121,11 @@ class AlbumStateNotifier extends StateNotifier { : Future.value([]), ]); + state = state.copyWith( + albums: [...res[0], ...res[1], ...res[2]], + loading: false, + ); + final medias = await Future.wait([ for (Album album in res[0]) _getThumbnailMedia( @@ -142,11 +149,7 @@ class AlbumStateNotifier extends StateNotifier { }, ); - state = state.copyWith( - albums: [...res[0], ...res[1], ...res[2]], - medias: medias, - loading: false, - ); + state = state.copyWith(medias: medias); } catch (e, s) { state = state.copyWith(loading: false, error: e); _logger.e( diff --git a/app/lib/ui/flow/albums/component/album_item.dart b/app/lib/ui/flow/albums/component/album_item.dart index e69de29b..ed638d23 100644 --- a/app/lib/ui/flow/albums/component/album_item.dart +++ b/app/lib/ui/flow/albums/component/album_item.dart @@ -0,0 +1,97 @@ +import 'package:data/models/album/album.dart'; +import 'package:data/models/media/media.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:style/animations/fade_in_switcher.dart'; +import 'package:style/animations/on_tap_scale.dart'; +import 'package:style/extensions/context_extensions.dart'; +import 'package:style/text/app_text_style.dart'; +import '../../../../components/thumbnail_builder.dart'; +import '../../../../gen/assets.gen.dart'; + +class AlbumItem extends StatelessWidget { + final Album album; + final AppMedia? media; + final void Function() onTap; + final void Function() onLongTap; + + const AlbumItem({ + super.key, + required this.album, + required this.media, + required this.onTap, + required this.onLongTap, + }); + + @override + Widget build(BuildContext context) { + return OnTapScale( + onTap: onTap, + onLongTap: onLongTap, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: FadeInSwitcher( + child: media == null + ? Container( + height: double.infinity, + width: double.infinity, + decoration: BoxDecoration( + color: context.colorScheme.containerLowOnSurface, + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: context.colorScheme.outline, + ), + ), + child: Icon( + CupertinoIcons.folder, + size: 80, + color: context.colorScheme.containerHighOnSurface, + ), + ) + : AppMediaImage( + radius: 8, + media: media!, + size: Size(double.infinity, double.infinity), + ), + ), + ), + const SizedBox(height: 10), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: Row( + children: [ + if (album.source == AppMediaSource.dropbox) ...[ + SvgPicture.asset( + Assets.images.icDropbox, + width: 18, + height: 18, + ), + const SizedBox(width: 4), + ], + if (album.source == AppMediaSource.googleDrive) ...[ + SvgPicture.asset( + Assets.images.icGoogleDrive, + width: 18, + height: 18, + ), + const SizedBox(width: 4), + ], + Expanded( + child: Text( + album.name, + style: AppTextStyles.subtitle1.copyWith( + color: context.colorScheme.textPrimary, + ), + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ), + ], + ), + ); + } +} diff --git a/app/lib/ui/flow/albums/media_list/album_media_list_screen.dart b/app/lib/ui/flow/albums/media_list/album_media_list_screen.dart index 7bcbdb43..3e1e5489 100644 --- a/app/lib/ui/flow/albums/media_list/album_media_list_screen.dart +++ b/app/lib/ui/flow/albums/media_list/album_media_list_screen.dart @@ -146,7 +146,39 @@ class _AlbumMediaListScreenState extends ConsumerState { ), itemCount: state.medias.length, itemBuilder: (context, index) => AppMediaThumbnail( - heroTag: "album_media_list", + onTap: () async { + await MediaPreviewRoute( + $extra: MediaPreviewRouteData( + onLoadMore: _notifier.loadMedia, + heroTag: "album_media_list", + medias: state.medias, + startFrom: state.medias[index].id, + ), + ).push(context); + }, + onLongTap: () { + showAppSheet( + context: context, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + AppSheetAction( + title: context.l10n.common_remove, + onPressed: () async { + context.pop(); + await _notifier.removeMediaFromAlbum(state.medias[index]); + }, + icon: Icon( + CupertinoIcons.delete, + size: 24, + color: context.colorScheme.textPrimary, + ), + ), + ], + ), + ); + }, + heroTag: "album_media_list${state.medias[index].toString()}", media: state.medias[index], ), ); diff --git a/app/lib/ui/flow/albums/media_list/album_media_list_state_notifier.dart b/app/lib/ui/flow/albums/media_list/album_media_list_state_notifier.dart index 608a60d4..4d4ca7a2 100644 --- a/app/lib/ui/flow/albums/media_list/album_media_list_state_notifier.dart +++ b/app/lib/ui/flow/albums/media_list/album_media_list_state_notifier.dart @@ -153,10 +153,61 @@ class AlbumMediaListStateNotifier extends StateNotifier { } } - Future loadMedia({bool reload = false}) async { + Future removeMediaFromAlbum(AppMedia media) async { + try { + state = state.copyWith(actionError: null); + if (state.album.source == AppMediaSource.local) { + await _localMediaService.updateAlbum( + state.album.copyWith( + medias: state.album.medias + .where((element) => element != media.id) + .toList(), + ), + ); + } else if (state.album.source == AppMediaSource.googleDrive) { + _backupFolderId ??= await _googleDriveService.getBackUpFolderId(); + if (_backupFolderId == null) { + throw BackUpFolderNotFound(); + } + await _googleDriveService.updateAlbum( + folderId: _backupFolderId!, + album: state.album.copyWith( + medias: state.album.medias + .where((element) => element != media.driveMediaRefId!) + .toList(), + ), + ); + } else if (state.album.source == AppMediaSource.dropbox) { + await _dropboxService.updateAlbum( + state.album.copyWith( + medias: state.album.medias + .where((element) => element != media.dropboxMediaRefId!) + .toList(), + ), + ); + } + state = state.copyWith( + medias: state.medias.where((element) => element != media).toList(), + album: state.album.copyWith( + medias: state.album.medias + .where((element) => element != media.id) + .toList(), + ), + ); + } catch (e, s) { + state = state.copyWith(actionError: e); + _logger.e( + "AlbumMediaListStateNotifier: Error deleting album", + error: e, + stackTrace: s, + ); + } + } + + Future> loadMedia({bool reload = false}) async { ///TODO: remove deleted media try { - if (state.loading) return; + if (state.loading) state.medias; if (reload) { state = state.copyWith(medias: []); @@ -212,6 +263,7 @@ class AlbumMediaListStateNotifier extends StateNotifier { state = state.copyWith(medias: [...state.medias, ...medias], loading: false); + return [...state.medias, ...medias]; } catch (e, s) { state = state.copyWith( loading: false, @@ -223,6 +275,7 @@ class AlbumMediaListStateNotifier extends StateNotifier { error: e, stackTrace: s, ); + return state.medias; } } } diff --git a/app/lib/ui/flow/home/components/app_media_item.dart b/app/lib/ui/flow/home/components/app_media_item.dart index c47c341f..0c0efab2 100644 --- a/app/lib/ui/flow/home/components/app_media_item.dart +++ b/app/lib/ui/flow/home/components/app_media_item.dart @@ -13,6 +13,7 @@ import '../../../../gen/assets.gen.dart'; class AppMediaItem extends StatelessWidget { final AppMedia media; + final String heroTag; final void Function()? onTap; final void Function()? onLongTap; final bool isSelected; @@ -22,6 +23,7 @@ class AppMediaItem extends StatelessWidget { const AppMediaItem({ super.key, required this.media, + required this.heroTag, this.onTap, this.onLongTap, this.isSelected = false, @@ -46,7 +48,7 @@ class AppMediaItem extends StatelessWidget { radius: isSelected ? 4 : 0, size: constraints.biggest, media: media, - heroTag: media, + heroTag: heroTag, ), ), if (media.type.isVideo) _videoDuration(context), diff --git a/app/lib/ui/flow/home/home_screen.dart b/app/lib/ui/flow/home/home_screen.dart index 4b013ec9..1488b0d6 100644 --- a/app/lib/ui/flow/home/home_screen.dart +++ b/app/lib/ui/flow/home/home_screen.dart @@ -179,9 +179,10 @@ class _HomeScreenState extends ConsumerState { physics: const NeverScrollableScrollPhysics(), shrinkWrap: true, gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: context.mediaQuerySize.width > 600 - ? context.mediaQuerySize.width ~/ 180 - : context.mediaQuerySize.width ~/ 100, + crossAxisCount: (context.mediaQuerySize.width > 600 + ? context.mediaQuerySize.width ~/ 180 + : context.mediaQuerySize.width ~/ 100) + .clamp(1, 6), crossAxisSpacing: 4, mainAxisSpacing: 4, ), @@ -194,9 +195,9 @@ class _HomeScreenState extends ConsumerState { _notifier.loadMedias(); }); } - return AppMediaItem( - key: ValueKey(media.id), + media: media, + heroTag: "home${media.toString()}", onTap: () async { if (state.selectedMedias.isNotEmpty) { _notifier.toggleMediaSelection(media); @@ -204,6 +205,8 @@ class _HomeScreenState extends ConsumerState { } else { await MediaPreviewRoute( $extra: MediaPreviewRouteData( + onLoadMore: _notifier.loadMedias, + heroTag: "home", medias: state.medias.values .expand((element) => element.values) .toList(), @@ -220,7 +223,7 @@ class _HomeScreenState extends ConsumerState { uploadMediaProcess: state.uploadMediaProcesses[media.id], downloadMediaProcess: state.downloadMediaProcesses[media.id], - media: media, + ); }, ), diff --git a/app/lib/ui/flow/home/home_screen_view_model.dart b/app/lib/ui/flow/home/home_screen_view_model.dart index 3ed21abb..ace5a891 100644 --- a/app/lib/ui/flow/home/home_screen_view_model.dart +++ b/app/lib/ui/flow/home/home_screen_view_model.dart @@ -269,8 +269,13 @@ class HomeViewStateNotifier extends StateNotifier /// Loads medias from local, google drive and dropbox. /// it append the medias to the existing medias if reload is false. /// force will load media event its already loading - Future loadMedias({bool reload = false, bool force = false}) async { - if (state.cloudLoading && !force) return; + Future> loadMedias({ + bool reload = false, + bool force = false, + }) async { + if (state.cloudLoading && !force) { + return state.medias.values.expand((element) => element.values).toList(); + } state = state.copyWith(loading: true, cloudLoading: true, error: null); try { // Reset all the variables if reload is true @@ -449,6 +454,7 @@ class HomeViewStateNotifier extends StateNotifier stackTrace: s, ); } + return state.medias.values.expand((element) => element.values).toList(); } Future<({List onlyCloudBasedMedias, List localRefMedias})> diff --git a/app/lib/ui/flow/media_preview/components/download_require_view.dart b/app/lib/ui/flow/media_preview/components/download_require_view.dart index e4c8b8d5..f7fe4013 100644 --- a/app/lib/ui/flow/media_preview/components/download_require_view.dart +++ b/app/lib/ui/flow/media_preview/components/download_require_view.dart @@ -11,6 +11,7 @@ import '../../../../domain/image_providers/app_media_image_provider.dart'; class DownloadRequireView extends StatelessWidget { final AppMedia media; + final String heroTag; final String? dropboxAccessToken; final DownloadMediaProcess? downloadProcess; final void Function() onDownload; @@ -18,6 +19,7 @@ class DownloadRequireView extends StatelessWidget { const DownloadRequireView({ super.key, required this.media, + required this.heroTag, this.downloadProcess, required this.onDownload, this.dropboxAccessToken, @@ -30,7 +32,7 @@ class DownloadRequireView extends StatelessWidget { alignment: Alignment.center, children: [ Hero( - tag: media, + tag: "$heroTag${media.toString()}", child: Image( gaplessPlayback: true, image: AppMediaImageProvider( diff --git a/app/lib/ui/flow/media_preview/components/local_media_image_preview.dart b/app/lib/ui/flow/media_preview/components/local_media_image_preview.dart index 001f544f..8b70627c 100644 --- a/app/lib/ui/flow/media_preview/components/local_media_image_preview.dart +++ b/app/lib/ui/flow/media_preview/components/local_media_image_preview.dart @@ -9,10 +9,12 @@ import '../../../../domain/image_providers/app_media_image_provider.dart'; class LocalMediaImagePreview extends StatelessWidget { final AppMedia media; + final String heroTag; const LocalMediaImagePreview({ super.key, required this.media, + required this.heroTag, }); @override @@ -27,7 +29,7 @@ class LocalMediaImagePreview extends StatelessWidget { : width; return Center( child: Hero( - tag: media, + tag: "$heroTag${media.toString()}", child: Image.file( width: width, height: height, diff --git a/app/lib/ui/flow/media_preview/components/network_image_preview/network_image_preview.dart b/app/lib/ui/flow/media_preview/components/network_image_preview/network_image_preview.dart index 07ec49a2..4849c8d0 100644 --- a/app/lib/ui/flow/media_preview/components/network_image_preview/network_image_preview.dart +++ b/app/lib/ui/flow/media_preview/components/network_image_preview/network_image_preview.dart @@ -1,5 +1,4 @@ import 'dart:io'; -import 'package:data/models/media/media_extension.dart'; import 'package:style/extensions/context_extensions.dart'; import '../../../../../components/place_holder_screen.dart'; import '../../../../../domain/extensions/context_extensions.dart'; @@ -7,14 +6,18 @@ import 'package:data/models/media/media.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../../../../components/app_page.dart'; -import '../../../../../domain/extensions/widget_extensions.dart'; import '../../../../../domain/image_providers/app_media_image_provider.dart'; import 'network_image_preview_view_model.dart'; class NetworkImagePreview extends ConsumerStatefulWidget { final AppMedia media; + final String heroTag; - const NetworkImagePreview({super.key, required this.media}); + const NetworkImagePreview({ + super.key, + required this.media, + required this.heroTag, + }); @override ConsumerState createState() => @@ -22,32 +25,20 @@ class NetworkImagePreview extends ConsumerStatefulWidget { } class _NetworkImagePreviewState extends ConsumerState { - late NetworkImagePreviewStateNotifier notifier; + late AutoDisposeStateNotifierProvider _provider; @override void initState() { if (!widget.media.sources.contains(AppMediaSource.local)) { - notifier = ref.read(networkImagePreviewStateNotifierProvider.notifier); - runPostFrame(() async { - if (widget.media.driveMediaRefId != null) { - await notifier.loadImageFromGoogleDrive( - id: widget.media.driveMediaRefId!, - extension: widget.media.extension, - ); - } else if (widget.media.dropboxMediaRefId != null) { - await notifier.loadImageFromDropbox( - id: widget.media.dropboxMediaRefId!, - extension: widget.media.extension, - ); - } - }); + _provider = networkImagePreviewStateNotifierProvider(widget.media); } super.initState(); } @override Widget build(BuildContext context) { - final state = ref.watch(networkImagePreviewStateNotifierProvider); + final state = ref.watch(_provider); final width = context.mediaQuerySize.width; double multiplier = 1; if (widget.media.displayWidth != null && widget.media.displayWidth! > 0) { @@ -60,7 +51,7 @@ class _NetworkImagePreviewState extends ConsumerState { return Center( child: Hero( - tag: widget.media, + tag: "${widget.heroTag}${widget.media.toString()}", child: Image( image: state.filePath != null ? FileImage(File(state.filePath!)) as ImageProvider diff --git a/app/lib/ui/flow/media_preview/components/network_image_preview/network_image_preview_view_model.dart b/app/lib/ui/flow/media_preview/components/network_image_preview/network_image_preview_view_model.dart index cff93f0a..c43bbfd6 100644 --- a/app/lib/ui/flow/media_preview/components/network_image_preview/network_image_preview_view_model.dart +++ b/app/lib/ui/flow/media_preview/components/network_image_preview/network_image_preview_view_model.dart @@ -1,5 +1,7 @@ import 'dart:async'; import 'dart:io'; +import 'package:data/models/media/media.dart'; +import 'package:data/models/media/media_extension.dart'; import 'package:data/services/dropbox_services.dart'; import 'package:data/services/google_drive_service.dart'; import 'package:dio/dio.dart' show CancelToken; @@ -9,12 +11,13 @@ import 'package:path_provider/path_provider.dart'; part 'network_image_preview_view_model.freezed.dart'; -final networkImagePreviewStateNotifierProvider = - StateNotifierProvider.autoDispose((ref) { +final networkImagePreviewStateNotifierProvider = StateNotifierProvider.family + .autoDispose((ref, media) { return NetworkImagePreviewStateNotifier( ref.read(googleDriveServiceProvider), ref.read(dropboxServiceProvider), + media, ); }); @@ -26,7 +29,20 @@ class NetworkImagePreviewStateNotifier NetworkImagePreviewStateNotifier( this._googleDriveServices, this._dropboxService, - ) : super(const NetworkImagePreviewState()); + AppMedia media, + ) : super(NetworkImagePreviewState(media: media)) { + if (media.driveMediaRefId != null) { + loadImageFromGoogleDrive( + id: media.driveMediaRefId!, + extension: media.extension, + ); + } else if (media.dropboxMediaRefId != null) { + loadImageFromDropbox( + id: media.dropboxMediaRefId!, + extension: media.extension, + ); + } + } File? tempFile; CancelToken? cancelToken; @@ -102,6 +118,7 @@ class NetworkImagePreviewStateNotifier @freezed class NetworkImagePreviewState with _$NetworkImagePreviewState { const factory NetworkImagePreviewState({ + required AppMedia media, @Default(false) bool loading, double? progress, String? filePath, diff --git a/app/lib/ui/flow/media_preview/components/network_image_preview/network_image_preview_view_model.freezed.dart b/app/lib/ui/flow/media_preview/components/network_image_preview/network_image_preview_view_model.freezed.dart index 1d9b4542..85b7303d 100644 --- a/app/lib/ui/flow/media_preview/components/network_image_preview/network_image_preview_view_model.freezed.dart +++ b/app/lib/ui/flow/media_preview/components/network_image_preview/network_image_preview_view_model.freezed.dart @@ -16,6 +16,7 @@ final _privateConstructorUsedError = UnsupportedError( /// @nodoc mixin _$NetworkImagePreviewState { + AppMedia get media => throw _privateConstructorUsedError; bool get loading => throw _privateConstructorUsedError; double? get progress => throw _privateConstructorUsedError; String? get filePath => throw _privateConstructorUsedError; @@ -34,7 +35,14 @@ abstract class $NetworkImagePreviewStateCopyWith<$Res> { $Res Function(NetworkImagePreviewState) then) = _$NetworkImagePreviewStateCopyWithImpl<$Res, NetworkImagePreviewState>; @useResult - $Res call({bool loading, double? progress, String? filePath, Object? error}); + $Res call( + {AppMedia media, + bool loading, + double? progress, + String? filePath, + Object? error}); + + $AppMediaCopyWith<$Res> get media; } /// @nodoc @@ -53,12 +61,17 @@ class _$NetworkImagePreviewStateCopyWithImpl<$Res, @pragma('vm:prefer-inline') @override $Res call({ + Object? media = null, Object? loading = null, Object? progress = freezed, Object? filePath = freezed, Object? error = freezed, }) { return _then(_value.copyWith( + media: null == media + ? _value.media + : media // ignore: cast_nullable_to_non_nullable + as AppMedia, loading: null == loading ? _value.loading : loading // ignore: cast_nullable_to_non_nullable @@ -74,6 +87,16 @@ class _$NetworkImagePreviewStateCopyWithImpl<$Res, error: freezed == error ? _value.error : error, ) as $Val); } + + /// Create a copy of NetworkImagePreviewState + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $AppMediaCopyWith<$Res> get media { + return $AppMediaCopyWith<$Res>(_value.media, (value) { + return _then(_value.copyWith(media: value) as $Val); + }); + } } /// @nodoc @@ -85,7 +108,15 @@ abstract class _$$NetworkImagePreviewStateImplCopyWith<$Res> __$$NetworkImagePreviewStateImplCopyWithImpl<$Res>; @override @useResult - $Res call({bool loading, double? progress, String? filePath, Object? error}); + $Res call( + {AppMedia media, + bool loading, + double? progress, + String? filePath, + Object? error}); + + @override + $AppMediaCopyWith<$Res> get media; } /// @nodoc @@ -103,12 +134,17 @@ class __$$NetworkImagePreviewStateImplCopyWithImpl<$Res> @pragma('vm:prefer-inline') @override $Res call({ + Object? media = null, Object? loading = null, Object? progress = freezed, Object? filePath = freezed, Object? error = freezed, }) { return _then(_$NetworkImagePreviewStateImpl( + media: null == media + ? _value.media + : media // ignore: cast_nullable_to_non_nullable + as AppMedia, loading: null == loading ? _value.loading : loading // ignore: cast_nullable_to_non_nullable @@ -130,8 +166,14 @@ class __$$NetworkImagePreviewStateImplCopyWithImpl<$Res> class _$NetworkImagePreviewStateImpl implements _NetworkImagePreviewState { const _$NetworkImagePreviewStateImpl( - {this.loading = false, this.progress, this.filePath, this.error}); + {required this.media, + this.loading = false, + this.progress, + this.filePath, + this.error}); + @override + final AppMedia media; @override @JsonKey() final bool loading; @@ -144,7 +186,7 @@ class _$NetworkImagePreviewStateImpl implements _NetworkImagePreviewState { @override String toString() { - return 'NetworkImagePreviewState(loading: $loading, progress: $progress, filePath: $filePath, error: $error)'; + return 'NetworkImagePreviewState(media: $media, loading: $loading, progress: $progress, filePath: $filePath, error: $error)'; } @override @@ -152,6 +194,7 @@ class _$NetworkImagePreviewStateImpl implements _NetworkImagePreviewState { return identical(this, other) || (other.runtimeType == runtimeType && other is _$NetworkImagePreviewStateImpl && + (identical(other.media, media) || other.media == media) && (identical(other.loading, loading) || other.loading == loading) && (identical(other.progress, progress) || other.progress == progress) && @@ -161,8 +204,8 @@ class _$NetworkImagePreviewStateImpl implements _NetworkImagePreviewState { } @override - int get hashCode => Object.hash(runtimeType, loading, progress, filePath, - const DeepCollectionEquality().hash(error)); + int get hashCode => Object.hash(runtimeType, media, loading, progress, + filePath, const DeepCollectionEquality().hash(error)); /// Create a copy of NetworkImagePreviewState /// with the given fields replaced by the non-null parameter values. @@ -176,11 +219,14 @@ class _$NetworkImagePreviewStateImpl implements _NetworkImagePreviewState { abstract class _NetworkImagePreviewState implements NetworkImagePreviewState { const factory _NetworkImagePreviewState( - {final bool loading, + {required final AppMedia media, + final bool loading, final double? progress, final String? filePath, final Object? error}) = _$NetworkImagePreviewStateImpl; + @override + AppMedia get media; @override bool get loading; @override diff --git a/app/lib/ui/flow/media_preview/media_preview_screen.dart b/app/lib/ui/flow/media_preview/media_preview_screen.dart index 28b8e620..4ef708ac 100644 --- a/app/lib/ui/flow/media_preview/media_preview_screen.dart +++ b/app/lib/ui/flow/media_preview/media_preview_screen.dart @@ -28,11 +28,15 @@ import 'components/video_player_components/video_duration_slider.dart'; class MediaPreview extends ConsumerStatefulWidget { final List medias; + final String heroTag; + final Future> Function() onLoadMore; final String startFrom; const MediaPreview({ super.key, required this.medias, + required this.heroTag, + required this.onLoadMore, required this.startFrom, }); @@ -190,7 +194,10 @@ class _MediaPreviewState extends ConsumerState { physics: state.isImageZoomed ? const NeverScrollableScrollPhysics() : null, - onPageChanged: _notifier.changeVisibleMediaIndex, + onPageChanged: (value) => _notifier.changeVisibleMediaIndex( + value, + widget.onLoadMore, + ), controller: _pageController, itemCount: state.medias.length, itemBuilder: (context, index) => _preview( @@ -243,7 +250,7 @@ class _MediaPreviewState extends ConsumerState { ); return Hero( - tag: media, + tag: "${widget.heroTag}${media.toString()}", child: Stack( alignment: Alignment.center, children: [ @@ -321,7 +328,7 @@ class _MediaPreviewState extends ConsumerState { }, onDismiss: context.pop, onDragDown: _notifier.updateSwipeDownPercentage, - child: LocalMediaImagePreview(media: media), + child: LocalMediaImagePreview(media: media, heroTag: widget.heroTag), ); } else if (media.type.isImage && (media.isGoogleDriveStored || media.isDropboxStored)) { @@ -332,7 +339,7 @@ class _MediaPreviewState extends ConsumerState { }, onDismiss: context.pop, onDragDown: _notifier.updateSwipeDownPercentage, - child: NetworkImagePreview(media: media), + child: NetworkImagePreview(media: media, heroTag: widget.heroTag), ); } else { return PlaceHolderScreen( @@ -359,6 +366,7 @@ class _MediaPreviewState extends ConsumerState { ), ); return DownloadRequireView( + heroTag: widget.heroTag, dropboxAccessToken: ref.read(AppPreferences.dropboxToken)?.access_token, media: media, diff --git a/app/lib/ui/flow/media_preview/media_preview_view_model.dart b/app/lib/ui/flow/media_preview/media_preview_view_model.dart index 4e10caad..3a2858ba 100644 --- a/app/lib/ui/flow/media_preview/media_preview_view_model.dart +++ b/app/lib/ui/flow/media_preview/media_preview_view_model.dart @@ -16,7 +16,6 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:google_sign_in/google_sign_in.dart'; import 'package:logger/logger.dart'; -import '../home/home_screen_view_model.dart'; part 'media_preview_view_model.freezed.dart'; @@ -36,15 +35,6 @@ final mediaPreviewStateNotifierProvider = ref.read(authServiceProvider), ref.read(connectivityHandlerProvider), ref.read(loggerProvider), - ref.read(homeViewStateNotifier.notifier), - () { - return ref.read( - homeViewStateNotifier.select( - (value) => - value.medias.values.expand((element) => element.values).toList(), - ), - ); - }, state.medias, state.startIndex, ref.read(AppPreferences.dropboxCurrentUserAccount), @@ -63,8 +53,6 @@ class MediaPreviewStateNotifier extends StateNotifier { final ConnectivityHandler _connectivityHandler; final AuthService _authService; final Logger _logger; - final HomeViewStateNotifier _homeNotifier; - final List Function() _getUpdatedMedias; StreamSubscription? _googleAccountSubscription; String? _backUpFolderId; @@ -77,8 +65,6 @@ class MediaPreviewStateNotifier extends StateNotifier { this._authService, this._connectivityHandler, this._logger, - this._homeNotifier, - this._getUpdatedMedias, List medias, int startIndex, DropboxAccount? dropboxAccount, @@ -396,12 +382,14 @@ class MediaPreviewStateNotifier extends StateNotifier { // Preview Actions ----------------------------------------------------------- - Future changeVisibleMediaIndex(int index) async { + Future changeVisibleMediaIndex( + int index, + Future> Function() loadMoreMedia, + ) async { state = state.copyWith(currentIndex: index); if (index == state.medias.length - 1) { - await _homeNotifier.loadMedias(); - state = state.copyWith(medias: _getUpdatedMedias()); + state = state.copyWith(medias: await loadMoreMedia()); } } diff --git a/app/lib/ui/navigation/app_route.dart b/app/lib/ui/navigation/app_route.dart index bbba99b4..4e5baa77 100644 --- a/app/lib/ui/navigation/app_route.dart +++ b/app/lib/ui/navigation/app_route.dart @@ -147,9 +147,16 @@ class AccountRoute extends GoRouteData { class MediaPreviewRouteData { final List medias; + final String heroTag; + final Future> Function() onLoadMore; final String startFrom; - const MediaPreviewRouteData({required this.medias, required this.startFrom}); + const MediaPreviewRouteData({ + required this.medias, + required this.startFrom, + required this.onLoadMore, + required this.heroTag, + }); } @TypedGoRoute(path: AppRoutePath.preview) @@ -163,7 +170,12 @@ class MediaPreviewRoute extends GoRouteData { return CustomTransitionPage( opaque: false, key: state.pageKey, - child: MediaPreview(medias: $extra.medias, startFrom: $extra.startFrom), + child: MediaPreview( + medias: $extra.medias, + startFrom: $extra.startFrom, + onLoadMore: $extra.onLoadMore, + heroTag: $extra.heroTag, + ), transitionsBuilder: (context, animation, secondaryAnimation, child) { return FadeTransition(opacity: animation, child: child); }, From 7d91558aa2e9e5416935d90210a0d030b0fafa14 Mon Sep 17 00:00:00 2001 From: Pratik-canopas Date: Wed, 8 Jan 2025 10:01:27 +0530 Subject: [PATCH 11/24] Album preview --- .idea/libraries/Dart_Packages.xml | 80 +++++++++++++++++++++-------- .idea/libraries/Flutter_Plugins.xml | 49 +++++++++--------- data/.flutter-plugins | 18 +++---- data/.flutter-plugins-dependencies | 2 +- 4 files changed, 94 insertions(+), 55 deletions(-) diff --git a/.idea/libraries/Dart_Packages.xml b/.idea/libraries/Dart_Packages.xml index 18c0c921..6d04606f 100644 --- a/.idea/libraries/Dart_Packages.xml +++ b/.idea/libraries/Dart_Packages.xml @@ -97,6 +97,7 @@ @@ -104,6 +105,7 @@ @@ -111,6 +113,7 @@ @@ -118,6 +121,7 @@ @@ -125,6 +129,7 @@ @@ -132,6 +137,7 @@ @@ -146,6 +152,7 @@ @@ -560,7 +567,6 @@ @@ -575,6 +581,7 @@ @@ -652,6 +659,7 @@ @@ -659,6 +667,7 @@ @@ -680,6 +689,7 @@ @@ -701,7 +711,6 @@ @@ -731,6 +740,7 @@ @@ -794,6 +804,7 @@ @@ -801,6 +812,7 @@ @@ -808,6 +820,7 @@ @@ -836,7 +849,7 @@ @@ -844,7 +857,6 @@ @@ -922,7 +934,6 @@ @@ -951,6 +962,7 @@ @@ -958,6 +970,7 @@ @@ -972,7 +985,6 @@ @@ -980,7 +992,6 @@ @@ -1009,6 +1020,7 @@ @@ -1016,7 +1028,7 @@ @@ -1024,6 +1036,7 @@ @@ -1059,6 +1072,7 @@ @@ -1066,7 +1080,6 @@ @@ -1088,6 +1101,7 @@ @@ -1123,7 +1137,6 @@ @@ -1131,6 +1144,7 @@ @@ -1166,6 +1180,7 @@ @@ -1215,6 +1230,7 @@ @@ -1369,6 +1385,7 @@ @@ -1425,7 +1442,7 @@ @@ -1447,6 +1464,7 @@ @@ -1466,13 +1484,20 @@ + + + + + + + @@ -1528,10 +1553,10 @@ - + @@ -1543,19 +1568,22 @@ + + + - + @@ -1565,14 +1593,16 @@ + + + - - + @@ -1584,46 +1614,49 @@ - + + - - - + + + - + + - + + @@ -1631,6 +1664,7 @@ + @@ -1653,6 +1687,7 @@ + @@ -1660,11 +1695,12 @@ - + + diff --git a/.idea/libraries/Flutter_Plugins.xml b/.idea/libraries/Flutter_Plugins.xml index b294abb3..a5962037 100644 --- a/.idea/libraries/Flutter_Plugins.xml +++ b/.idea/libraries/Flutter_Plugins.xml @@ -1,41 +1,23 @@ + + + + - - - - - - - - - - - - - - - - - - - - - - - + @@ -43,18 +25,39 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/data/.flutter-plugins b/data/.flutter-plugins index 8b0208e4..fde6dc03 100644 --- a/data/.flutter-plugins +++ b/data/.flutter-plugins @@ -2,25 +2,25 @@ flutter_local_notifications=/Users/pratikcanopas/.pub-cache/hosted/pub.dev/flutter_local_notifications-18.0.1/ flutter_local_notifications_linux=/Users/pratikcanopas/.pub-cache/hosted/pub.dev/flutter_local_notifications_linux-5.0.0/ google_sign_in=/Users/pratikcanopas/.pub-cache/hosted/pub.dev/google_sign_in-6.2.2/ -google_sign_in_android=/Users/pratikcanopas/.pub-cache/hosted/pub.dev/google_sign_in_android-6.1.33/ +google_sign_in_android=/Users/pratikcanopas/.pub-cache/hosted/pub.dev/google_sign_in_android-6.1.34/ google_sign_in_ios=/Users/pratikcanopas/.pub-cache/hosted/pub.dev/google_sign_in_ios-5.7.8/ google_sign_in_web=/Users/pratikcanopas/.pub-cache/hosted/pub.dev/google_sign_in_web-0.12.4+3/ -package_info_plus=/Users/pratikcanopas/.pub-cache/hosted/pub.dev/package_info_plus-8.1.1/ +package_info_plus=/Users/pratikcanopas/.pub-cache/hosted/pub.dev/package_info_plus-8.1.2/ path_provider=/Users/pratikcanopas/.pub-cache/hosted/pub.dev/path_provider-2.1.5/ -path_provider_android=/Users/pratikcanopas/.pub-cache/hosted/pub.dev/path_provider_android-2.2.12/ -path_provider_foundation=/Users/pratikcanopas/.pub-cache/hosted/pub.dev/path_provider_foundation-2.4.0/ +path_provider_android=/Users/pratikcanopas/.pub-cache/hosted/pub.dev/path_provider_android-2.2.15/ +path_provider_foundation=/Users/pratikcanopas/.pub-cache/hosted/pub.dev/path_provider_foundation-2.4.1/ path_provider_linux=/Users/pratikcanopas/.pub-cache/hosted/pub.dev/path_provider_linux-2.2.1/ path_provider_windows=/Users/pratikcanopas/.pub-cache/hosted/pub.dev/path_provider_windows-2.3.0/ -photo_manager=/Users/pratikcanopas/.pub-cache/hosted/pub.dev/photo_manager-3.6.2/ -shared_preferences=/Users/pratikcanopas/.pub-cache/hosted/pub.dev/shared_preferences-2.3.3/ -shared_preferences_android=/Users/pratikcanopas/.pub-cache/hosted/pub.dev/shared_preferences_android-2.3.3/ -shared_preferences_foundation=/Users/pratikcanopas/.pub-cache/hosted/pub.dev/shared_preferences_foundation-2.5.3/ +photo_manager=/Users/pratikcanopas/.pub-cache/hosted/pub.dev/photo_manager-3.6.3/ +shared_preferences=/Users/pratikcanopas/.pub-cache/hosted/pub.dev/shared_preferences-2.3.5/ +shared_preferences_android=/Users/pratikcanopas/.pub-cache/hosted/pub.dev/shared_preferences_android-2.4.0/ +shared_preferences_foundation=/Users/pratikcanopas/.pub-cache/hosted/pub.dev/shared_preferences_foundation-2.5.4/ shared_preferences_linux=/Users/pratikcanopas/.pub-cache/hosted/pub.dev/shared_preferences_linux-2.4.1/ shared_preferences_web=/Users/pratikcanopas/.pub-cache/hosted/pub.dev/shared_preferences_web-2.4.2/ shared_preferences_windows=/Users/pratikcanopas/.pub-cache/hosted/pub.dev/shared_preferences_windows-2.4.1/ sqflite=/Users/pratikcanopas/.pub-cache/hosted/pub.dev/sqflite-2.4.1/ sqflite_android=/Users/pratikcanopas/.pub-cache/hosted/pub.dev/sqflite_android-2.4.0/ -sqflite_darwin=/Users/pratikcanopas/.pub-cache/hosted/pub.dev/sqflite_darwin-2.4.1/ +sqflite_darwin=/Users/pratikcanopas/.pub-cache/hosted/pub.dev/sqflite_darwin-2.4.1+1/ url_launcher=/Users/pratikcanopas/.pub-cache/hosted/pub.dev/url_launcher-6.3.1/ url_launcher_android=/Users/pratikcanopas/.pub-cache/hosted/pub.dev/url_launcher_android-6.3.14/ url_launcher_ios=/Users/pratikcanopas/.pub-cache/hosted/pub.dev/url_launcher_ios-6.3.2/ diff --git a/data/.flutter-plugins-dependencies b/data/.flutter-plugins-dependencies index 5f2bffce..9289aa2d 100644 --- a/data/.flutter-plugins-dependencies +++ b/data/.flutter-plugins-dependencies @@ -1 +1 @@ -{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"flutter_local_notifications","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/flutter_local_notifications-18.0.1/","native_build":true,"dependencies":[]},{"name":"google_sign_in_ios","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/google_sign_in_ios-5.7.8/","shared_darwin_source":true,"native_build":true,"dependencies":[]},{"name":"package_info_plus","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/package_info_plus-8.1.1/","native_build":true,"dependencies":[]},{"name":"path_provider_foundation","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/path_provider_foundation-2.4.0/","shared_darwin_source":true,"native_build":true,"dependencies":[]},{"name":"photo_manager","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/photo_manager-3.6.2/","native_build":true,"dependencies":[]},{"name":"shared_preferences_foundation","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/shared_preferences_foundation-2.5.3/","shared_darwin_source":true,"native_build":true,"dependencies":[]},{"name":"sqflite_darwin","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/sqflite_darwin-2.4.1/","shared_darwin_source":true,"native_build":true,"dependencies":[]},{"name":"url_launcher_ios","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/url_launcher_ios-6.3.2/","native_build":true,"dependencies":[]}],"android":[{"name":"flutter_local_notifications","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/flutter_local_notifications-18.0.1/","native_build":true,"dependencies":[]},{"name":"google_sign_in_android","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/google_sign_in_android-6.1.33/","native_build":true,"dependencies":[]},{"name":"package_info_plus","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/package_info_plus-8.1.1/","native_build":true,"dependencies":[]},{"name":"path_provider_android","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/path_provider_android-2.2.12/","native_build":true,"dependencies":[]},{"name":"photo_manager","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/photo_manager-3.6.2/","native_build":true,"dependencies":[]},{"name":"shared_preferences_android","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/shared_preferences_android-2.3.3/","native_build":true,"dependencies":[]},{"name":"sqflite_android","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/sqflite_android-2.4.0/","native_build":true,"dependencies":[]},{"name":"url_launcher_android","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/url_launcher_android-6.3.14/","native_build":true,"dependencies":[]}],"macos":[{"name":"flutter_local_notifications","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/flutter_local_notifications-18.0.1/","native_build":true,"dependencies":[]},{"name":"google_sign_in_ios","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/google_sign_in_ios-5.7.8/","shared_darwin_source":true,"native_build":true,"dependencies":[]},{"name":"package_info_plus","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/package_info_plus-8.1.1/","native_build":true,"dependencies":[]},{"name":"path_provider_foundation","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/path_provider_foundation-2.4.0/","shared_darwin_source":true,"native_build":true,"dependencies":[]},{"name":"photo_manager","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/photo_manager-3.6.2/","native_build":true,"dependencies":[]},{"name":"shared_preferences_foundation","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/shared_preferences_foundation-2.5.3/","shared_darwin_source":true,"native_build":true,"dependencies":[]},{"name":"sqflite_darwin","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/sqflite_darwin-2.4.1/","shared_darwin_source":true,"native_build":true,"dependencies":[]},{"name":"url_launcher_macos","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/url_launcher_macos-3.2.2/","native_build":true,"dependencies":[]}],"linux":[{"name":"flutter_local_notifications_linux","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/flutter_local_notifications_linux-5.0.0/","native_build":false,"dependencies":[]},{"name":"package_info_plus","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/package_info_plus-8.1.1/","native_build":false,"dependencies":[]},{"name":"path_provider_linux","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/path_provider_linux-2.2.1/","native_build":false,"dependencies":[]},{"name":"shared_preferences_linux","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/shared_preferences_linux-2.4.1/","native_build":false,"dependencies":["path_provider_linux"]},{"name":"url_launcher_linux","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/url_launcher_linux-3.2.1/","native_build":true,"dependencies":[]}],"windows":[{"name":"package_info_plus","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/package_info_plus-8.1.1/","native_build":false,"dependencies":[]},{"name":"path_provider_windows","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/path_provider_windows-2.3.0/","native_build":false,"dependencies":[]},{"name":"shared_preferences_windows","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/shared_preferences_windows-2.4.1/","native_build":false,"dependencies":["path_provider_windows"]},{"name":"url_launcher_windows","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/url_launcher_windows-3.1.3/","native_build":true,"dependencies":[]}],"web":[{"name":"google_sign_in_web","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/google_sign_in_web-0.12.4+3/","dependencies":[]},{"name":"package_info_plus","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/package_info_plus-8.1.1/","dependencies":[]},{"name":"shared_preferences_web","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/shared_preferences_web-2.4.2/","dependencies":[]},{"name":"url_launcher_web","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/url_launcher_web-2.3.3/","dependencies":[]}]},"dependencyGraph":[{"name":"flutter_local_notifications","dependencies":["flutter_local_notifications_linux"]},{"name":"flutter_local_notifications_linux","dependencies":[]},{"name":"google_sign_in","dependencies":["google_sign_in_android","google_sign_in_ios","google_sign_in_web"]},{"name":"google_sign_in_android","dependencies":[]},{"name":"google_sign_in_ios","dependencies":[]},{"name":"google_sign_in_web","dependencies":[]},{"name":"package_info_plus","dependencies":[]},{"name":"path_provider","dependencies":["path_provider_android","path_provider_foundation","path_provider_linux","path_provider_windows"]},{"name":"path_provider_android","dependencies":[]},{"name":"path_provider_foundation","dependencies":[]},{"name":"path_provider_linux","dependencies":[]},{"name":"path_provider_windows","dependencies":[]},{"name":"photo_manager","dependencies":[]},{"name":"shared_preferences","dependencies":["shared_preferences_android","shared_preferences_foundation","shared_preferences_linux","shared_preferences_web","shared_preferences_windows"]},{"name":"shared_preferences_android","dependencies":[]},{"name":"shared_preferences_foundation","dependencies":[]},{"name":"shared_preferences_linux","dependencies":["path_provider_linux"]},{"name":"shared_preferences_web","dependencies":[]},{"name":"shared_preferences_windows","dependencies":["path_provider_windows"]},{"name":"sqflite","dependencies":["sqflite_android","sqflite_darwin"]},{"name":"sqflite_android","dependencies":[]},{"name":"sqflite_darwin","dependencies":[]},{"name":"url_launcher","dependencies":["url_launcher_android","url_launcher_ios","url_launcher_linux","url_launcher_macos","url_launcher_web","url_launcher_windows"]},{"name":"url_launcher_android","dependencies":[]},{"name":"url_launcher_ios","dependencies":[]},{"name":"url_launcher_linux","dependencies":[]},{"name":"url_launcher_macos","dependencies":[]},{"name":"url_launcher_web","dependencies":[]},{"name":"url_launcher_windows","dependencies":[]}],"date_created":"2025-01-03 12:26:22.948749","version":"3.27.1","swift_package_manager_enabled":false} \ No newline at end of file +{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"flutter_local_notifications","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/flutter_local_notifications-18.0.1/","native_build":true,"dependencies":[]},{"name":"google_sign_in_ios","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/google_sign_in_ios-5.7.8/","shared_darwin_source":true,"native_build":true,"dependencies":[]},{"name":"package_info_plus","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/package_info_plus-8.1.2/","native_build":true,"dependencies":[]},{"name":"path_provider_foundation","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/path_provider_foundation-2.4.1/","shared_darwin_source":true,"native_build":true,"dependencies":[]},{"name":"photo_manager","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/photo_manager-3.6.3/","native_build":true,"dependencies":[]},{"name":"shared_preferences_foundation","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/shared_preferences_foundation-2.5.4/","shared_darwin_source":true,"native_build":true,"dependencies":[]},{"name":"sqflite_darwin","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/sqflite_darwin-2.4.1+1/","shared_darwin_source":true,"native_build":true,"dependencies":[]},{"name":"url_launcher_ios","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/url_launcher_ios-6.3.2/","native_build":true,"dependencies":[]}],"android":[{"name":"flutter_local_notifications","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/flutter_local_notifications-18.0.1/","native_build":true,"dependencies":[]},{"name":"google_sign_in_android","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/google_sign_in_android-6.1.34/","native_build":true,"dependencies":[]},{"name":"package_info_plus","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/package_info_plus-8.1.2/","native_build":true,"dependencies":[]},{"name":"path_provider_android","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/path_provider_android-2.2.15/","native_build":true,"dependencies":[]},{"name":"photo_manager","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/photo_manager-3.6.3/","native_build":true,"dependencies":[]},{"name":"shared_preferences_android","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/shared_preferences_android-2.4.0/","native_build":true,"dependencies":[]},{"name":"sqflite_android","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/sqflite_android-2.4.0/","native_build":true,"dependencies":[]},{"name":"url_launcher_android","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/url_launcher_android-6.3.14/","native_build":true,"dependencies":[]}],"macos":[{"name":"flutter_local_notifications","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/flutter_local_notifications-18.0.1/","native_build":true,"dependencies":[]},{"name":"google_sign_in_ios","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/google_sign_in_ios-5.7.8/","shared_darwin_source":true,"native_build":true,"dependencies":[]},{"name":"package_info_plus","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/package_info_plus-8.1.2/","native_build":true,"dependencies":[]},{"name":"path_provider_foundation","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/path_provider_foundation-2.4.1/","shared_darwin_source":true,"native_build":true,"dependencies":[]},{"name":"photo_manager","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/photo_manager-3.6.3/","native_build":true,"dependencies":[]},{"name":"shared_preferences_foundation","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/shared_preferences_foundation-2.5.4/","shared_darwin_source":true,"native_build":true,"dependencies":[]},{"name":"sqflite_darwin","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/sqflite_darwin-2.4.1+1/","shared_darwin_source":true,"native_build":true,"dependencies":[]},{"name":"url_launcher_macos","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/url_launcher_macos-3.2.2/","native_build":true,"dependencies":[]}],"linux":[{"name":"flutter_local_notifications_linux","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/flutter_local_notifications_linux-5.0.0/","native_build":false,"dependencies":[]},{"name":"package_info_plus","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/package_info_plus-8.1.2/","native_build":false,"dependencies":[]},{"name":"path_provider_linux","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/path_provider_linux-2.2.1/","native_build":false,"dependencies":[]},{"name":"shared_preferences_linux","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/shared_preferences_linux-2.4.1/","native_build":false,"dependencies":["path_provider_linux"]},{"name":"url_launcher_linux","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/url_launcher_linux-3.2.1/","native_build":true,"dependencies":[]}],"windows":[{"name":"package_info_plus","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/package_info_plus-8.1.2/","native_build":false,"dependencies":[]},{"name":"path_provider_windows","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/path_provider_windows-2.3.0/","native_build":false,"dependencies":[]},{"name":"shared_preferences_windows","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/shared_preferences_windows-2.4.1/","native_build":false,"dependencies":["path_provider_windows"]},{"name":"url_launcher_windows","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/url_launcher_windows-3.1.3/","native_build":true,"dependencies":[]}],"web":[{"name":"google_sign_in_web","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/google_sign_in_web-0.12.4+3/","dependencies":[]},{"name":"package_info_plus","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/package_info_plus-8.1.2/","dependencies":[]},{"name":"shared_preferences_web","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/shared_preferences_web-2.4.2/","dependencies":[]},{"name":"url_launcher_web","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/url_launcher_web-2.3.3/","dependencies":[]}]},"dependencyGraph":[{"name":"flutter_local_notifications","dependencies":["flutter_local_notifications_linux"]},{"name":"flutter_local_notifications_linux","dependencies":[]},{"name":"google_sign_in","dependencies":["google_sign_in_android","google_sign_in_ios","google_sign_in_web"]},{"name":"google_sign_in_android","dependencies":[]},{"name":"google_sign_in_ios","dependencies":[]},{"name":"google_sign_in_web","dependencies":[]},{"name":"package_info_plus","dependencies":[]},{"name":"path_provider","dependencies":["path_provider_android","path_provider_foundation","path_provider_linux","path_provider_windows"]},{"name":"path_provider_android","dependencies":[]},{"name":"path_provider_foundation","dependencies":[]},{"name":"path_provider_linux","dependencies":[]},{"name":"path_provider_windows","dependencies":[]},{"name":"photo_manager","dependencies":[]},{"name":"shared_preferences","dependencies":["shared_preferences_android","shared_preferences_foundation","shared_preferences_linux","shared_preferences_web","shared_preferences_windows"]},{"name":"shared_preferences_android","dependencies":[]},{"name":"shared_preferences_foundation","dependencies":[]},{"name":"shared_preferences_linux","dependencies":["path_provider_linux"]},{"name":"shared_preferences_web","dependencies":[]},{"name":"shared_preferences_windows","dependencies":["path_provider_windows"]},{"name":"sqflite","dependencies":["sqflite_android","sqflite_darwin"]},{"name":"sqflite_android","dependencies":[]},{"name":"sqflite_darwin","dependencies":[]},{"name":"url_launcher","dependencies":["url_launcher_android","url_launcher_ios","url_launcher_linux","url_launcher_macos","url_launcher_web","url_launcher_windows"]},{"name":"url_launcher_android","dependencies":[]},{"name":"url_launcher_ios","dependencies":[]},{"name":"url_launcher_linux","dependencies":[]},{"name":"url_launcher_macos","dependencies":[]},{"name":"url_launcher_web","dependencies":[]},{"name":"url_launcher_windows","dependencies":[]}],"date_created":"2025-01-07 16:15:57.147963","version":"3.27.1","swift_package_manager_enabled":false} \ No newline at end of file From dc8b12ed74de097d7f5d2cdd2d93a9a8073c2d0d Mon Sep 17 00:00:00 2001 From: Pratik-canopas Date: Wed, 8 Jan 2025 10:03:02 +0530 Subject: [PATCH 12/24] test --- app/lib/ui/flow/albums/albums_view_notifier.dart | 2 +- app/lib/ui/flow/home/home_screen.dart | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/app/lib/ui/flow/albums/albums_view_notifier.dart b/app/lib/ui/flow/albums/albums_view_notifier.dart index 3f265edb..e0f1b943 100644 --- a/app/lib/ui/flow/albums/albums_view_notifier.dart +++ b/app/lib/ui/flow/albums/albums_view_notifier.dart @@ -108,7 +108,7 @@ class AlbumStateNotifier extends StateNotifier { state = state.copyWith(loading: true, error: null); try { - if(state.googleAccount != null) { + if (state.googleAccount != null) { _backupFolderId ??= await _googleDriveService.getBackUpFolderId(); } final res = await Future.wait([ diff --git a/app/lib/ui/flow/home/home_screen.dart b/app/lib/ui/flow/home/home_screen.dart index 1488b0d6..17982004 100644 --- a/app/lib/ui/flow/home/home_screen.dart +++ b/app/lib/ui/flow/home/home_screen.dart @@ -223,7 +223,6 @@ class _HomeScreenState extends ConsumerState { uploadMediaProcess: state.uploadMediaProcesses[media.id], downloadMediaProcess: state.downloadMediaProcesses[media.id], - ); }, ), From eb46e174e36c3512f6dd04a694222ee77f2aca2e Mon Sep 17 00:00:00 2001 From: Pratik-canopas Date: Wed, 8 Jan 2025 10:22:03 +0530 Subject: [PATCH 13/24] Fix pop error --- app/lib/ui/flow/albums/albums_screen.dart | 1 + .../media_selection_screen.dart | 2 +- app/lib/ui/navigation/app_route.dart | 21 ++-- app/lib/ui/navigation/app_route.g.dart | 97 ++++++++++--------- 4 files changed, 58 insertions(+), 63 deletions(-) diff --git a/app/lib/ui/flow/albums/albums_screen.dart b/app/lib/ui/flow/albums/albums_screen.dart index fc69db5c..f990a684 100644 --- a/app/lib/ui/flow/albums/albums_screen.dart +++ b/app/lib/ui/flow/albums/albums_screen.dart @@ -104,6 +104,7 @@ class _AlbumsScreenState extends ConsumerState { media: state.medias[album.id], onTap: () async { await AlbumMediaListRoute( + albumId: album.id, $extra: album, ).push(context); _notifier.loadAlbums(); diff --git a/app/lib/ui/flow/media_selection/media_selection_screen.dart b/app/lib/ui/flow/media_selection/media_selection_screen.dart index c9a47eac..cb6cdc54 100644 --- a/app/lib/ui/flow/media_selection/media_selection_screen.dart +++ b/app/lib/ui/flow/media_selection/media_selection_screen.dart @@ -157,7 +157,7 @@ class _MediaSelectionScreenState extends ConsumerState { ), itemCount: gridEntry.value.length, itemBuilder: (context, index) => AppMediaThumbnail( - heroTag: "selection", + heroTag: "selection${gridEntry.value.elementAt(index)}", onTap: () { _notifier.toggleMediaSelection( gridEntry.value.elementAt(index), diff --git a/app/lib/ui/navigation/app_route.dart b/app/lib/ui/navigation/app_route.dart index 4e5baa77..3ea1bf7a 100644 --- a/app/lib/ui/navigation/app_route.dart +++ b/app/lib/ui/navigation/app_route.dart @@ -20,8 +20,8 @@ class AppRoutePath { static const onBoard = '/on-board'; static const home = '/'; static const albums = '/albums'; - static const add = 'add'; - static const mediaList = 'media-list'; + static const addAlbum = '/add-album'; + static const albumMediaList = '/albums/:albumId'; static const transfer = '/transfer'; static const accounts = '/accounts'; static const preview = '/preview'; @@ -49,13 +49,7 @@ class OnBoardRoute extends GoRouteData { ), TypedStatefulShellBranch( routes: [ - TypedGoRoute( - path: AppRoutePath.albums, - routes: [ - TypedGoRoute(path: AppRoutePath.add), - TypedGoRoute(path: AppRoutePath.mediaList), - ], - ), + TypedGoRoute(path: AppRoutePath.albums), ], ), TypedStatefulShellBranch( @@ -105,9 +99,8 @@ class AlbumsRoute extends GoRouteData { const AlbumsScreen(); } +@TypedGoRoute(path: AppRoutePath.addAlbum) class AddAlbumRoute extends GoRouteData { - static final GlobalKey $parentNavigatorKey = rootNavigatorKey; - final Album? $extra; const AddAlbumRoute({this.$extra}); @@ -117,12 +110,12 @@ class AddAlbumRoute extends GoRouteData { AddAlbumScreen(editAlbum: $extra); } +@TypedGoRoute(path: AppRoutePath.albumMediaList) class AlbumMediaListRoute extends GoRouteData { - static final GlobalKey $parentNavigatorKey = rootNavigatorKey; - final Album $extra; + final String albumId; - const AlbumMediaListRoute({required this.$extra}); + const AlbumMediaListRoute({required this.$extra, required this.albumId}); @override Widget build(BuildContext context, GoRouterState state) => diff --git a/app/lib/ui/navigation/app_route.g.dart b/app/lib/ui/navigation/app_route.g.dart index 898cc02d..6c8ff6ac 100644 --- a/app/lib/ui/navigation/app_route.g.dart +++ b/app/lib/ui/navigation/app_route.g.dart @@ -9,6 +9,8 @@ part of 'app_route.dart'; List get $appRoutes => [ $onBoardRoute, $mainShellRoute, + $addAlbumRoute, + $albumMediaListRoute, $mediaPreviewRoute, $mediaMetadataDetailsRoute, $mediaSelectionRoute, @@ -52,18 +54,6 @@ RouteBase get $mainShellRoute => StatefulShellRouteData.$route( GoRouteData.$route( path: '/albums', factory: $AlbumsRouteExtension._fromState, - routes: [ - GoRouteData.$route( - path: 'add', - parentNavigatorKey: AddAlbumRoute.$parentNavigatorKey, - factory: $AddAlbumRouteExtension._fromState, - ), - GoRouteData.$route( - path: 'media-list', - parentNavigatorKey: AlbumMediaListRoute.$parentNavigatorKey, - factory: $AlbumMediaListRouteExtension._fromState, - ), - ], ), ], ), @@ -125,13 +115,52 @@ extension $AlbumsRouteExtension on AlbumsRoute { void replace(BuildContext context) => context.replace(location); } +extension $TransferRouteExtension on TransferRoute { + static TransferRoute _fromState(GoRouterState state) => const TransferRoute(); + + String get location => GoRouteData.$location( + '/transfer', + ); + + void go(BuildContext context) => context.go(location); + + Future push(BuildContext context) => context.push(location); + + void pushReplacement(BuildContext context) => + context.pushReplacement(location); + + void replace(BuildContext context) => context.replace(location); +} + +extension $AccountRouteExtension on AccountRoute { + static AccountRoute _fromState(GoRouterState state) => const AccountRoute(); + + String get location => GoRouteData.$location( + '/accounts', + ); + + void go(BuildContext context) => context.go(location); + + Future push(BuildContext context) => context.push(location); + + void pushReplacement(BuildContext context) => + context.pushReplacement(location); + + void replace(BuildContext context) => context.replace(location); +} + +RouteBase get $addAlbumRoute => GoRouteData.$route( + path: '/add-album', + factory: $AddAlbumRouteExtension._fromState, + ); + extension $AddAlbumRouteExtension on AddAlbumRoute { static AddAlbumRoute _fromState(GoRouterState state) => AddAlbumRoute( $extra: state.extra as Album?, ); String get location => GoRouteData.$location( - '/albums/add', + '/add-album', ); void go(BuildContext context) => context.go(location, extra: $extra); @@ -146,14 +175,20 @@ extension $AddAlbumRouteExtension on AddAlbumRoute { context.replace(location, extra: $extra); } +RouteBase get $albumMediaListRoute => GoRouteData.$route( + path: '/albums/:albumId', + factory: $AlbumMediaListRouteExtension._fromState, + ); + extension $AlbumMediaListRouteExtension on AlbumMediaListRoute { static AlbumMediaListRoute _fromState(GoRouterState state) => AlbumMediaListRoute( + albumId: state.pathParameters['albumId']!, $extra: state.extra as Album, ); String get location => GoRouteData.$location( - '/albums/media-list', + '/albums/${Uri.encodeComponent(albumId)}', ); void go(BuildContext context) => context.go(location, extra: $extra); @@ -168,40 +203,6 @@ extension $AlbumMediaListRouteExtension on AlbumMediaListRoute { context.replace(location, extra: $extra); } -extension $TransferRouteExtension on TransferRoute { - static TransferRoute _fromState(GoRouterState state) => const TransferRoute(); - - String get location => GoRouteData.$location( - '/transfer', - ); - - void go(BuildContext context) => context.go(location); - - Future push(BuildContext context) => context.push(location); - - void pushReplacement(BuildContext context) => - context.pushReplacement(location); - - void replace(BuildContext context) => context.replace(location); -} - -extension $AccountRouteExtension on AccountRoute { - static AccountRoute _fromState(GoRouterState state) => const AccountRoute(); - - String get location => GoRouteData.$location( - '/accounts', - ); - - void go(BuildContext context) => context.go(location); - - Future push(BuildContext context) => context.push(location); - - void pushReplacement(BuildContext context) => - context.pushReplacement(location); - - void replace(BuildContext context) => context.replace(location); -} - RouteBase get $mediaPreviewRoute => GoRouteData.$route( path: '/preview', factory: $MediaPreviewRouteExtension._fromState, From 5f72865a7769edd33fc6d160164ed87da0128e58 Mon Sep 17 00:00:00 2001 From: Sneha Canopas Date: Wed, 8 Jan 2025 11:21:43 +0530 Subject: [PATCH 14/24] Fix hero animation --- app/lib/components/thumbnail_builder.dart | 2 +- .../media_list/album_media_list_screen.dart | 76 ++++++++++--------- .../album_media_list_state_notifier.dart | 41 +++++----- 3 files changed, 57 insertions(+), 62 deletions(-) diff --git a/app/lib/components/thumbnail_builder.dart b/app/lib/components/thumbnail_builder.dart index e8227556..78af9e05 100644 --- a/app/lib/components/thumbnail_builder.dart +++ b/app/lib/components/thumbnail_builder.dart @@ -8,7 +8,7 @@ import 'package:style/indicators/circular_progress_indicator.dart'; import '../domain/image_providers/app_media_image_provider.dart'; class AppMediaImage extends ConsumerWidget { - final Object? heroTag; + final String? heroTag; final AppMedia media; final Size size; final double radius; diff --git a/app/lib/ui/flow/albums/media_list/album_media_list_screen.dart b/app/lib/ui/flow/albums/media_list/album_media_list_screen.dart index 3e1e5489..fcbeced2 100644 --- a/app/lib/ui/flow/albums/media_list/album_media_list_screen.dart +++ b/app/lib/ui/flow/albums/media_list/album_media_list_screen.dart @@ -61,7 +61,6 @@ class _AlbumMediaListScreenState extends ConsumerState { final res = await MediaSelectionRoute($extra: widget.album.source) .push(context); - if (res != null && res is List) { await _notifier.updateAlbumMedias(medias: res); } @@ -119,7 +118,7 @@ class _AlbumMediaListScreenState extends ConsumerState { required BuildContext context, required AlbumMediaListState state, }) { - if (state.loading && state.medias.isEmpty) { + if (state.loading) { return const Center(child: AppCircularProgressIndicator()); } else if (state.error != null) { return ErrorScreen( @@ -145,42 +144,45 @@ class _AlbumMediaListScreenState extends ConsumerState { mainAxisSpacing: 4, ), itemCount: state.medias.length, - itemBuilder: (context, index) => AppMediaThumbnail( - onTap: () async { - await MediaPreviewRoute( - $extra: MediaPreviewRouteData( - onLoadMore: _notifier.loadMedia, - heroTag: "album_media_list", - medias: state.medias, - startFrom: state.medias[index].id, - ), - ).push(context); - }, - onLongTap: () { - showAppSheet( - context: context, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - AppSheetAction( - title: context.l10n.common_remove, - onPressed: () async { - context.pop(); - await _notifier.removeMediaFromAlbum(state.medias[index]); - }, - icon: Icon( - CupertinoIcons.delete, - size: 24, - color: context.colorScheme.textPrimary, + itemBuilder: (context, index) { + final heroTag = "album_media_list-$index-"; + return AppMediaThumbnail( + onTap: () async { + await MediaPreviewRoute( + $extra: MediaPreviewRouteData( + onLoadMore: _notifier.loadMedia, + heroTag: heroTag, + medias: state.medias, + startFrom: state.medias[index].id, + ), + ).push(context); + }, + onLongTap: () { + showAppSheet( + context: context, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + AppSheetAction( + title: context.l10n.common_remove, + onPressed: () async { + context.pop(); + await _notifier.removeMediaFromAlbum(state.medias[index]); + }, + icon: Icon( + CupertinoIcons.delete, + size: 24, + color: context.colorScheme.textPrimary, + ), ), - ), - ], - ), - ); - }, - heroTag: "album_media_list${state.medias[index].toString()}", - media: state.medias[index], - ), + ], + ), + ); + }, + heroTag: "$heroTag${state.medias[index].toString()}", + media: state.medias[index], + ); + }, ); } } diff --git a/app/lib/ui/flow/albums/media_list/album_media_list_state_notifier.dart b/app/lib/ui/flow/albums/media_list/album_media_list_state_notifier.dart index 4d4ca7a2..2bc8694c 100644 --- a/app/lib/ui/flow/albums/media_list/album_media_list_state_notifier.dart +++ b/app/lib/ui/flow/albums/media_list/album_media_list_state_notifier.dart @@ -44,7 +44,7 @@ class AlbumMediaListStateNotifier extends StateNotifier { String? _backupFolderId; Future loadAlbum() async { - if (state.loading) return; + //if (state.loading) return; state = state.copyWith(actionError: null); List albums = []; @@ -61,11 +61,11 @@ class AlbumMediaListStateNotifier extends StateNotifier { } else { albums = await _localMediaService.getAlbums(); } + final album = + albums.firstWhereOrNull((element) => element.id == state.album.id); state = state.copyWith( - album: albums - .firstWhereOrNull((element) => element.id == state.album.id) ?? - state.album, + album: album ?? state.album, ); } catch (e, s) { state = state.copyWith(actionError: e); @@ -117,13 +117,12 @@ class AlbumMediaListStateNotifier extends StateNotifier { bool append = true, }) async { try { - state = state.copyWith(actionError: null); + state = state.copyWith(actionError: null, loading: true); + final album = state.album.copyWith( + medias: append ? [...state.album.medias, ...medias] : medias, + ); if (state.album.source == AppMediaSource.local) { - await _localMediaService.updateAlbum( - state.album.copyWith( - medias: append ? [...state.album.medias, ...medias] : medias, - ), - ); + await _localMediaService.updateAlbum(album); } else if (state.album.source == AppMediaSource.googleDrive) { _backupFolderId ??= await _googleDriveService.getBackUpFolderId(); if (_backupFolderId == null) { @@ -131,20 +130,14 @@ class AlbumMediaListStateNotifier extends StateNotifier { } await _googleDriveService.updateAlbum( folderId: _backupFolderId!, - album: state.album.copyWith( - medias: append ? [...state.album.medias, ...medias] : medias, - ), + album: album, ); } else if (state.album.source == AppMediaSource.dropbox) { - await _dropboxService.updateAlbum( - state.album.copyWith( - medias: append ? [...state.album.medias, ...medias] : medias, - ), - ); + await _dropboxService.updateAlbum(album); } - reloadAlbum(); + await reloadAlbum(); } catch (e, s) { - state = state.copyWith(actionError: e); + state = state.copyWith(actionError: e, loading: false); _logger.e( "AlbumMediaListStateNotifier: Error adding media", error: e, @@ -207,7 +200,7 @@ class AlbumMediaListStateNotifier extends StateNotifier { Future> loadMedia({bool reload = false}) async { ///TODO: remove deleted media try { - if (state.loading) state.medias; + //if (state.loading) state.medias; if (reload) { state = state.copyWith(medias: []); @@ -224,13 +217,12 @@ class AlbumMediaListStateNotifier extends StateNotifier { .take(30) .toList(); - medias = await Future.wait( + final newMedias = await Future.wait( moreMediaIds.map( (id) => _localMediaService.getMedia(id: id), ), - ).then( - (value) => value.nonNulls.toList(), ); + medias = newMedias.nonNulls.toList(); } else if (state.album.source == AppMediaSource.googleDrive) { final loadedMediaIds = state.medias.map((e) => e.driveMediaRefId).nonNulls.toList(); @@ -260,6 +252,7 @@ class AlbumMediaListStateNotifier extends StateNotifier { (value) => value.nonNulls.toList(), ); } + await Future.delayed(Duration(milliseconds: 300)); state = state.copyWith(medias: [...state.medias, ...medias], loading: false); From be70f2ffcaa10e980de47cc30f59c8ad1770663d Mon Sep 17 00:00:00 2001 From: Pratik-canopas Date: Wed, 8 Jan 2025 15:17:21 +0530 Subject: [PATCH 15/24] Add new selection menu --- app/assets/locales/app_en.arb | 5 +- app/lib/components/selection_menu.dart | 108 ++++++++ app/lib/components/thumbnail_builder.dart | 6 +- .../ui/flow/albums/component/album_item.dart | 1 + .../media_list/album_media_list_screen.dart | 140 ++++++---- .../album_media_list_state_notifier.dart | 244 ++++++++---------- ...bum_media_list_state_notifier.freezed.dart | 81 +++++- .../multi_selection_done_button.dart | 175 ++++++------- app/lib/ui/flow/home/home_screen.dart | 12 +- .../ui/flow/home/home_screen_view_model.dart | 4 + .../media_metadata_details.dart | 1 + .../media_selection_screen.dart | 2 +- data/lib/services/dropbox_services.dart | 28 +- data/lib/services/google_drive_service.dart | 26 +- 14 files changed, 514 insertions(+), 319 deletions(-) create mode 100644 app/lib/components/selection_menu.dart diff --git a/app/assets/locales/app_en.arb b/app/assets/locales/app_en.arb index d3453bb4..44fec237 100644 --- a/app/assets/locales/app_en.arb +++ b/app/assets/locales/app_en.arb @@ -130,7 +130,10 @@ "store_in_device_title": "Device", "@_ALBUM_MEDIA_LIST":{}, - "add_media_title": "Add Media", + "add_items_action_title": "Add Items", + "edit_album_action_title": "Edit Album", + "delete_album_action_title": "Delete Album", + "remove_item_action_title": "Remove Items", "@_MEDIA_SELECTION":{}, "select_from_device_title": "Select from Device", diff --git a/app/lib/components/selection_menu.dart b/app/lib/components/selection_menu.dart new file mode 100644 index 00000000..308e4840 --- /dev/null +++ b/app/lib/components/selection_menu.dart @@ -0,0 +1,108 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:style/animations/cross_fade_animation.dart'; +import 'package:style/animations/on_tap_scale.dart'; +import 'package:style/extensions/context_extensions.dart'; +import 'package:style/text/app_text_style.dart'; + +class SelectionMenuAction extends StatelessWidget { + final String title; + final Widget icon; + final void Function() onTap; + + const SelectionMenuAction({ + super.key, + required this.title, + required this.icon, + required this.onTap, + }); + + @override + Widget build(BuildContext context) { + return OnTapScale( + onTap: onTap, + child: SizedBox( + width: 92, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + alignment: Alignment.center, + height: 60, + width: 60, + decoration: BoxDecoration( + color: context.colorScheme.surface, + borderRadius: BorderRadius.circular(8), + border: Border.all(color: context.colorScheme.outline), + ), + child: icon, + ), + const SizedBox(height: 8), + Text( + title, + style: AppTextStyles.body.copyWith( + color: context.colorScheme.textPrimary, + ), + textAlign: TextAlign.center, + overflow: TextOverflow.visible, + ), + ], + ), + ), + ); + } +} + +class SelectionMenu extends StatelessWidget { + final List items; + final bool useSystemPadding; + final bool show; + + const SelectionMenu({ + super.key, + required this.items, + this.useSystemPadding = true, + required this.show, + }); + + @override + Widget build(BuildContext context) { + return CrossFadeAnimation( + showChild: show, + child: AnimatedSwitcher( + duration: const Duration(milliseconds: 300), + child: Align( + alignment: Alignment.bottomCenter, + child: Container( + padding: const EdgeInsets.only(bottom: 16, top: 24), + width: double.infinity, + decoration: BoxDecoration( + color: context.colorScheme.containerLowOnSurface, + border: Border( + top: BorderSide( + width: 1, + color: context.colorScheme.outline, + ), + ), + ), + child: SafeArea( + top: false, + bottom: useSystemPadding, + left: false, + right: false, + child: SingleChildScrollView( + padding: const EdgeInsets.symmetric(horizontal: 16), + scrollDirection: Axis.horizontal, + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + spacing: 4, + children: items, + ), + ), + ), + ), + ), + ), + ); + } +} diff --git a/app/lib/components/thumbnail_builder.dart b/app/lib/components/thumbnail_builder.dart index 78af9e05..ae1db0f4 100644 --- a/app/lib/components/thumbnail_builder.dart +++ b/app/lib/components/thumbnail_builder.dart @@ -8,7 +8,7 @@ import 'package:style/indicators/circular_progress_indicator.dart'; import '../domain/image_providers/app_media_image_provider.dart'; class AppMediaImage extends ConsumerWidget { - final String? heroTag; + final String heroTag; final AppMedia media; final Size size; final double radius; @@ -16,7 +16,7 @@ class AppMediaImage extends ConsumerWidget { const AppMediaImage({ super.key, required this.size, - this.heroTag, + required this.heroTag, this.radius = 4, required this.media, }); @@ -28,7 +28,7 @@ class AppMediaImage extends ConsumerWidget { child: Container( color: context.colorScheme.containerNormalOnSurface, child: Hero( - tag: heroTag ?? '', + tag: heroTag, child: Image( gaplessPlayback: true, image: AppMediaImageProvider( diff --git a/app/lib/ui/flow/albums/component/album_item.dart b/app/lib/ui/flow/albums/component/album_item.dart index ed638d23..78cca3fc 100644 --- a/app/lib/ui/flow/albums/component/album_item.dart +++ b/app/lib/ui/flow/albums/component/album_item.dart @@ -51,6 +51,7 @@ class AlbumItem extends StatelessWidget { ), ) : AppMediaImage( + heroTag: "album${media.toString()}", radius: 8, media: media!, size: Size(double.infinity, double.infinity), diff --git a/app/lib/ui/flow/albums/media_list/album_media_list_screen.dart b/app/lib/ui/flow/albums/media_list/album_media_list_screen.dart index fcbeced2..d8d520fd 100644 --- a/app/lib/ui/flow/albums/media_list/album_media_list_screen.dart +++ b/app/lib/ui/flow/albums/media_list/album_media_list_screen.dart @@ -14,7 +14,9 @@ import '../../../../components/app_page.dart'; import '../../../../components/app_sheet.dart'; import '../../../../components/error_screen.dart'; import '../../../../components/place_holder_screen.dart'; +import '../../../../components/selection_menu.dart'; import '../../../../domain/extensions/context_extensions.dart'; +import '../../../../domain/extensions/widget_extensions.dart'; import '../../../../gen/assets.gen.dart'; import '../../../navigation/app_route.dart'; import 'album_media_list_state_notifier.dart'; @@ -55,7 +57,7 @@ class _AlbumMediaListScreenState extends ConsumerState { mainAxisSize: MainAxisSize.min, children: [ AppSheetAction( - title: context.l10n.add_media_title, + title: context.l10n.add_items_action_title, onPressed: () async { context.pop(); final res = @@ -72,13 +74,13 @@ class _AlbumMediaListScreenState extends ConsumerState { ), ), AppSheetAction( - title: context.l10n.common_edit, + title: context.l10n.edit_album_action_title, onPressed: () async { context.pop(); final res = await AddAlbumRoute($extra: state.album) .push(context); if (res == true) { - await _notifier.reloadAlbum(); + _notifier.loadAlbum(); } }, icon: Icon( @@ -88,10 +90,10 @@ class _AlbumMediaListScreenState extends ConsumerState { ), ), AppSheetAction( - title: context.l10n.common_delete, + title: context.l10n.delete_album_action_title, onPressed: () async { context.pop(); - await _notifier.deleteAlbum(); + _notifier.deleteAlbum(); }, icon: Icon( CupertinoIcons.delete, @@ -135,54 +137,94 @@ class _AlbumMediaListScreenState extends ConsumerState { message: context.l10n.empty_media_message, ); } - return GridView.builder( - gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: context.mediaQuerySize.width > 600 - ? context.mediaQuerySize.width ~/ 180 - : context.mediaQuerySize.width ~/ 100, - crossAxisSpacing: 4, - mainAxisSpacing: 4, - ), - itemCount: state.medias.length, - itemBuilder: (context, index) { - final heroTag = "album_media_list-$index-"; - return AppMediaThumbnail( - onTap: () async { - await MediaPreviewRoute( - $extra: MediaPreviewRouteData( - onLoadMore: _notifier.loadMedia, - heroTag: heroTag, - medias: state.medias, - startFrom: state.medias[index].id, - ), - ).push(context); - }, - onLongTap: () { - showAppSheet( - context: context, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - AppSheetAction( - title: context.l10n.common_remove, - onPressed: () async { - context.pop(); - await _notifier.removeMediaFromAlbum(state.medias[index]); + return Column( + children: [ + Expanded( + child: CustomScrollView( + slivers: [ + SliverGrid.builder( + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: context.mediaQuerySize.width > 600 + ? context.mediaQuerySize.width ~/ 180 + : context.mediaQuerySize.width ~/ 100, + crossAxisSpacing: 4, + mainAxisSpacing: 4, + ), + itemCount: state.medias.length, + itemBuilder: (context, index) { + if (index == state.medias.length - 1) { + runPostFrame(() { + _notifier.loadMedia(); + }); + } + + return AppMediaThumbnail( + selected: state.selectedMedias + .contains(state.medias.keys.elementAt(index)), + onTap: () async { + if (state.selectedMedias.isNotEmpty) { + _notifier.toggleMediaSelection( + state.medias.keys.elementAt(index), + ); + return; + } + await MediaPreviewRoute( + $extra: MediaPreviewRouteData( + onLoadMore: _notifier.loadMedia, + heroTag: "album_media_list", + medias: state.medias.values.toList(), + startFrom: state.medias.values.elementAt(index).id, + ), + ).push(context); + _notifier.loadMedia(reload: true); }, - icon: Icon( - CupertinoIcons.delete, - size: 24, - color: context.colorScheme.textPrimary, + onLongTap: () { + _notifier.toggleMediaSelection( + state.medias.keys.elementAt(index), + ); + }, + heroTag: + "album_media_list${state.medias.values.elementAt(index).toString()}", + media: state.medias.values.elementAt(index), + ); + }, + ), + if (state.loadingMore) + SliverToBoxAdapter( + child: Padding( + padding: EdgeInsets.all(16), + child: AppCircularProgressIndicator( + size: 22, ), ), - ], + ), + ], + ), + ), + SelectionMenu( + items: [ + SelectionMenuAction( + title: context.l10n.common_cancel, + icon: Icon( + Icons.close, + color: context.colorScheme.textPrimary, + size: 24, ), - ); - }, - heroTag: "$heroTag${state.medias[index].toString()}", - media: state.medias[index], - ); - }, + onTap: _notifier.clearSelection, + ), + SelectionMenuAction( + title: context.l10n.remove_item_action_title, + icon: Icon( + CupertinoIcons.delete, + color: context.colorScheme.textPrimary, + size: 24, + ), + onTap: _notifier.removeMediaFromAlbum, + ), + ], + show: state.selectedMedias.isNotEmpty, + ), + ], ); } } diff --git a/app/lib/ui/flow/albums/media_list/album_media_list_state_notifier.dart b/app/lib/ui/flow/albums/media_list/album_media_list_state_notifier.dart index 2bc8694c..7e2547a3 100644 --- a/app/lib/ui/flow/albums/media_list/album_media_list_state_notifier.dart +++ b/app/lib/ui/flow/albums/media_list/album_media_list_state_notifier.dart @@ -29,6 +29,9 @@ class AlbumMediaListStateNotifier extends StateNotifier { final DropboxService _dropboxService; final Logger _logger; + int _loadedMediaCount = 0; + String? _backupFolderId; + AlbumMediaListStateNotifier( Album album, this._localMediaService, @@ -41,11 +44,88 @@ class AlbumMediaListStateNotifier extends StateNotifier { loadMedia(); } - String? _backupFolderId; + Future> loadMedia({bool reload = false}) async { + ///TODO: remove media-ids which is deleted from source + try { + if (state.loading || state.loadingMore) state.medias; - Future loadAlbum() async { - //if (state.loading) return; + state = state.copyWith( + loading: state.medias.isEmpty, + loadingMore: state.medias.isNotEmpty && !reload, + error: null, + actionError: null, + ); + + final moreMediaIds = state.album.medias + .sublist( + (reload ? 0 : _loadedMediaCount), + ) + .take(_loadedMediaCount + (reload ? _loadedMediaCount : 30)) + .toList(); + + Map medias = {}; + + if (state.album.source == AppMediaSource.local) { + final res = await Future.wait( + moreMediaIds.map((id) => _localMediaService.getMedia(id: id)), + ).then((value) => value.nonNulls.toList()); + medias = {for (final item in res) item.id: item}; + } else if (state.album.source == AppMediaSource.googleDrive) { + final res = await Future.wait( + moreMediaIds.map((id) => _googleDriveService.getMedia(id: id)), + ).then((value) => value.nonNulls.toList()); + medias = {for (final item in res) item.driveMediaRefId!: item}; + } else if (state.album.source == AppMediaSource.dropbox) { + final res = await Future.wait( + moreMediaIds.map((id) => _dropboxService.getMedia(id: id)), + ).then((value) => value.nonNulls.toList()); + medias = {for (final item in res) item.dropboxMediaRefId!: item}; + } + + state = state.copyWith( + medias: reload ? medias : {...state.medias, ...medias}, + loading: false, + loadingMore: false, + ); + + _loadedMediaCount = reload + ? moreMediaIds.length + : _loadedMediaCount + moreMediaIds.length; + + // remove media-ids from album which is deleted from source + final manuallyRemovedMedia = moreMediaIds + .where( + (element) => !medias.keys.contains(element), + ) + .toList(); + + if (manuallyRemovedMedia.isNotEmpty) { + updateAlbumMedias( + medias: state.album.medias + .where( + (element) => !manuallyRemovedMedia.contains(element), + ) + .toList(), + append: false, + ); + } + } catch (e, s) { + state = state.copyWith( + loading: false, + loadingMore: false, + error: state.medias.isEmpty ? e : null, + actionError: state.medias.isNotEmpty ? e : null, + ); + _logger.e( + "AlbumMediaListStateNotifier: Error loading medias", + error: e, + stackTrace: s, + ); + } + return state.medias.values.toList(); + } + Future loadAlbum() async { state = state.copyWith(actionError: null); List albums = []; try { @@ -61,27 +141,23 @@ class AlbumMediaListStateNotifier extends StateNotifier { } else { albums = await _localMediaService.getAlbums(); } - final album = - albums.firstWhereOrNull((element) => element.id == state.album.id); state = state.copyWith( - album: album ?? state.album, + album: albums + .firstWhereOrNull((element) => element.id == state.album.id) ?? + state.album, ); + loadMedia(reload: true); } catch (e, s) { state = state.copyWith(actionError: e); _logger.e( - "AlbumMediaListStateNotifier: Error loading albums", + "AlbumMediaListStateNotifier: Error loading album", error: e, stackTrace: s, ); } } - Future reloadAlbum() async { - await loadAlbum(); - await loadMedia(reload: true); - } - Future deleteAlbum() async { try { state = state.copyWith(actionError: null); @@ -116,45 +192,12 @@ class AlbumMediaListStateNotifier extends StateNotifier { required List medias, bool append = true, }) async { - try { - state = state.copyWith(actionError: null, loading: true); - final album = state.album.copyWith( - medias: append ? [...state.album.medias, ...medias] : medias, - ); - if (state.album.source == AppMediaSource.local) { - await _localMediaService.updateAlbum(album); - } else if (state.album.source == AppMediaSource.googleDrive) { - _backupFolderId ??= await _googleDriveService.getBackUpFolderId(); - if (_backupFolderId == null) { - throw BackUpFolderNotFound(); - } - await _googleDriveService.updateAlbum( - folderId: _backupFolderId!, - album: album, - ); - } else if (state.album.source == AppMediaSource.dropbox) { - await _dropboxService.updateAlbum(album); - } - await reloadAlbum(); - } catch (e, s) { - state = state.copyWith(actionError: e, loading: false); - _logger.e( - "AlbumMediaListStateNotifier: Error adding media", - error: e, - stackTrace: s, - ); - } - } - - Future removeMediaFromAlbum(AppMedia media) async { try { state = state.copyWith(actionError: null); if (state.album.source == AppMediaSource.local) { await _localMediaService.updateAlbum( state.album.copyWith( - medias: state.album.medias - .where((element) => element != media.id) - .toList(), + medias: append ? [...state.album.medias, ...medias] : medias, ), ); } else if (state.album.source == AppMediaSource.googleDrive) { @@ -165,120 +208,59 @@ class AlbumMediaListStateNotifier extends StateNotifier { await _googleDriveService.updateAlbum( folderId: _backupFolderId!, album: state.album.copyWith( - medias: state.album.medias - .where((element) => element != media.driveMediaRefId!) - .toList(), + medias: append ? [...state.album.medias, ...medias] : medias, ), ); } else if (state.album.source == AppMediaSource.dropbox) { await _dropboxService.updateAlbum( state.album.copyWith( - medias: state.album.medias - .where((element) => element != media.dropboxMediaRefId!) - .toList(), + medias: append ? [...state.album.medias, ...medias] : medias, ), ); } - state = state.copyWith( - medias: state.medias.where((element) => element != media).toList(), - album: state.album.copyWith( - medias: state.album.medias - .where((element) => element != media.id) - .toList(), - ), - ); + loadAlbum(); } catch (e, s) { state = state.copyWith(actionError: e); _logger.e( - "AlbumMediaListStateNotifier: Error deleting album", + "AlbumMediaListStateNotifier: Error adding media", error: e, stackTrace: s, ); } } - Future> loadMedia({bool reload = false}) async { - ///TODO: remove deleted media - try { - //if (state.loading) state.medias; - - if (reload) { - state = state.copyWith(medias: []); - } - - state = state.copyWith(loading: true, error: null, actionError: null); - - List medias = []; - - if (state.album.source == AppMediaSource.local) { - final loadedMediaIds = state.medias.map((e) => e.id).toList(); - final moreMediaIds = state.album.medias - .where((element) => !loadedMediaIds.contains(element)) - .take(30) - .toList(); - - final newMedias = await Future.wait( - moreMediaIds.map( - (id) => _localMediaService.getMedia(id: id), - ), - ); - medias = newMedias.nonNulls.toList(); - } else if (state.album.source == AppMediaSource.googleDrive) { - final loadedMediaIds = - state.medias.map((e) => e.driveMediaRefId).nonNulls.toList(); - final moreMediaIds = state.album.medias - .where((element) => !loadedMediaIds.contains(element)) - .take(30) - .toList(); - medias = await Future.wait( - moreMediaIds.map( - (id) => _googleDriveService.getMedia(id: id), - ), - ).then( - (value) => value.nonNulls.toList(), - ); - } else if (state.album.source == AppMediaSource.dropbox) { - final loadedMediaIds = - state.medias.map((e) => e.dropboxMediaRefId).nonNulls.toList(); - final moreMediaIds = state.album.medias - .where((element) => !loadedMediaIds.contains(element)) - .take(30) - .toList(); - medias = await Future.wait( - moreMediaIds.map( - (id) => _dropboxService.getMedia(id: id), - ), - ).then( - (value) => value.nonNulls.toList(), - ); - } - await Future.delayed(Duration(milliseconds: 300)); + Future removeMediaFromAlbum() async { + final list = state.album.medias.toList(); + list.removeWhere((element) { + return state.selectedMedias.contains(element); + }); + await updateAlbumMedias(medias: list, append: false); + } - state = - state.copyWith(medias: [...state.medias, ...medias], loading: false); - return [...state.medias, ...medias]; - } catch (e, s) { + void toggleMediaSelection(String id) { + if (state.selectedMedias.contains(id)) { state = state.copyWith( - loading: false, - error: state.medias.isEmpty ? e : null, - actionError: state.medias.isNotEmpty ? e : null, - ); - _logger.e( - "AlbumMediaListStateNotifier: Error loading medias", - error: e, - stackTrace: s, + selectedMedias: + state.selectedMedias.where((element) => element != id).toList(), ); - return state.medias; + } else { + state = state.copyWith(selectedMedias: [...state.selectedMedias, id]); } } + + void clearSelection() { + state = state.copyWith(selectedMedias: []); + } } @freezed class AlbumMediaListState with _$AlbumMediaListState { const factory AlbumMediaListState({ - @Default([]) List medias, + @Default({}) Map medias, + @Default([]) List selectedMedias, required Album album, @Default(false) bool loading, + @Default(false) bool loadingMore, @Default(false) bool deleteAlbumSuccess, Object? error, Object? actionError, diff --git a/app/lib/ui/flow/albums/media_list/album_media_list_state_notifier.freezed.dart b/app/lib/ui/flow/albums/media_list/album_media_list_state_notifier.freezed.dart index 7e4aee26..69f0e383 100644 --- a/app/lib/ui/flow/albums/media_list/album_media_list_state_notifier.freezed.dart +++ b/app/lib/ui/flow/albums/media_list/album_media_list_state_notifier.freezed.dart @@ -16,9 +16,11 @@ final _privateConstructorUsedError = UnsupportedError( /// @nodoc mixin _$AlbumMediaListState { - List get medias => throw _privateConstructorUsedError; + Map get medias => throw _privateConstructorUsedError; + List get selectedMedias => throw _privateConstructorUsedError; Album get album => throw _privateConstructorUsedError; bool get loading => throw _privateConstructorUsedError; + bool get loadingMore => throw _privateConstructorUsedError; bool get deleteAlbumSuccess => throw _privateConstructorUsedError; Object? get error => throw _privateConstructorUsedError; Object? get actionError => throw _privateConstructorUsedError; @@ -37,9 +39,11 @@ abstract class $AlbumMediaListStateCopyWith<$Res> { _$AlbumMediaListStateCopyWithImpl<$Res, AlbumMediaListState>; @useResult $Res call( - {List medias, + {Map medias, + List selectedMedias, Album album, bool loading, + bool loadingMore, bool deleteAlbumSuccess, Object? error, Object? actionError}); @@ -63,8 +67,10 @@ class _$AlbumMediaListStateCopyWithImpl<$Res, $Val extends AlbumMediaListState> @override $Res call({ Object? medias = null, + Object? selectedMedias = null, Object? album = null, Object? loading = null, + Object? loadingMore = null, Object? deleteAlbumSuccess = null, Object? error = freezed, Object? actionError = freezed, @@ -73,7 +79,11 @@ class _$AlbumMediaListStateCopyWithImpl<$Res, $Val extends AlbumMediaListState> medias: null == medias ? _value.medias : medias // ignore: cast_nullable_to_non_nullable - as List, + as Map, + selectedMedias: null == selectedMedias + ? _value.selectedMedias + : selectedMedias // ignore: cast_nullable_to_non_nullable + as List, album: null == album ? _value.album : album // ignore: cast_nullable_to_non_nullable @@ -82,6 +92,10 @@ class _$AlbumMediaListStateCopyWithImpl<$Res, $Val extends AlbumMediaListState> ? _value.loading : loading // ignore: cast_nullable_to_non_nullable as bool, + loadingMore: null == loadingMore + ? _value.loadingMore + : loadingMore // ignore: cast_nullable_to_non_nullable + as bool, deleteAlbumSuccess: null == deleteAlbumSuccess ? _value.deleteAlbumSuccess : deleteAlbumSuccess // ignore: cast_nullable_to_non_nullable @@ -111,9 +125,11 @@ abstract class _$$AlbumMediaListStateImplCopyWith<$Res> @override @useResult $Res call( - {List medias, + {Map medias, + List selectedMedias, Album album, bool loading, + bool loadingMore, bool deleteAlbumSuccess, Object? error, Object? actionError}); @@ -136,8 +152,10 @@ class __$$AlbumMediaListStateImplCopyWithImpl<$Res> @override $Res call({ Object? medias = null, + Object? selectedMedias = null, Object? album = null, Object? loading = null, + Object? loadingMore = null, Object? deleteAlbumSuccess = null, Object? error = freezed, Object? actionError = freezed, @@ -146,7 +164,11 @@ class __$$AlbumMediaListStateImplCopyWithImpl<$Res> medias: null == medias ? _value._medias : medias // ignore: cast_nullable_to_non_nullable - as List, + as Map, + selectedMedias: null == selectedMedias + ? _value._selectedMedias + : selectedMedias // ignore: cast_nullable_to_non_nullable + as List, album: null == album ? _value.album : album // ignore: cast_nullable_to_non_nullable @@ -155,6 +177,10 @@ class __$$AlbumMediaListStateImplCopyWithImpl<$Res> ? _value.loading : loading // ignore: cast_nullable_to_non_nullable as bool, + loadingMore: null == loadingMore + ? _value.loadingMore + : loadingMore // ignore: cast_nullable_to_non_nullable + as bool, deleteAlbumSuccess: null == deleteAlbumSuccess ? _value.deleteAlbumSuccess : deleteAlbumSuccess // ignore: cast_nullable_to_non_nullable @@ -169,21 +195,33 @@ class __$$AlbumMediaListStateImplCopyWithImpl<$Res> class _$AlbumMediaListStateImpl implements _AlbumMediaListState { const _$AlbumMediaListStateImpl( - {final List medias = const [], + {final Map medias = const {}, + final List selectedMedias = const [], required this.album, this.loading = false, + this.loadingMore = false, this.deleteAlbumSuccess = false, this.error, this.actionError}) - : _medias = medias; + : _medias = medias, + _selectedMedias = selectedMedias; + + final Map _medias; + @override + @JsonKey() + Map get medias { + if (_medias is EqualUnmodifiableMapView) return _medias; + // ignore: implicit_dynamic_type + return EqualUnmodifiableMapView(_medias); + } - final List _medias; + final List _selectedMedias; @override @JsonKey() - List get medias { - if (_medias is EqualUnmodifiableListView) return _medias; + List get selectedMedias { + if (_selectedMedias is EqualUnmodifiableListView) return _selectedMedias; // ignore: implicit_dynamic_type - return EqualUnmodifiableListView(_medias); + return EqualUnmodifiableListView(_selectedMedias); } @override @@ -193,6 +231,9 @@ class _$AlbumMediaListStateImpl implements _AlbumMediaListState { final bool loading; @override @JsonKey() + final bool loadingMore; + @override + @JsonKey() final bool deleteAlbumSuccess; @override final Object? error; @@ -201,7 +242,7 @@ class _$AlbumMediaListStateImpl implements _AlbumMediaListState { @override String toString() { - return 'AlbumMediaListState(medias: $medias, album: $album, loading: $loading, deleteAlbumSuccess: $deleteAlbumSuccess, error: $error, actionError: $actionError)'; + return 'AlbumMediaListState(medias: $medias, selectedMedias: $selectedMedias, album: $album, loading: $loading, loadingMore: $loadingMore, deleteAlbumSuccess: $deleteAlbumSuccess, error: $error, actionError: $actionError)'; } @override @@ -210,8 +251,12 @@ class _$AlbumMediaListStateImpl implements _AlbumMediaListState { (other.runtimeType == runtimeType && other is _$AlbumMediaListStateImpl && const DeepCollectionEquality().equals(other._medias, _medias) && + const DeepCollectionEquality() + .equals(other._selectedMedias, _selectedMedias) && (identical(other.album, album) || other.album == album) && (identical(other.loading, loading) || other.loading == loading) && + (identical(other.loadingMore, loadingMore) || + other.loadingMore == loadingMore) && (identical(other.deleteAlbumSuccess, deleteAlbumSuccess) || other.deleteAlbumSuccess == deleteAlbumSuccess) && const DeepCollectionEquality().equals(other.error, error) && @@ -223,8 +268,10 @@ class _$AlbumMediaListStateImpl implements _AlbumMediaListState { int get hashCode => Object.hash( runtimeType, const DeepCollectionEquality().hash(_medias), + const DeepCollectionEquality().hash(_selectedMedias), album, loading, + loadingMore, deleteAlbumSuccess, const DeepCollectionEquality().hash(error), const DeepCollectionEquality().hash(actionError)); @@ -241,20 +288,26 @@ class _$AlbumMediaListStateImpl implements _AlbumMediaListState { abstract class _AlbumMediaListState implements AlbumMediaListState { const factory _AlbumMediaListState( - {final List medias, + {final Map medias, + final List selectedMedias, required final Album album, final bool loading, + final bool loadingMore, final bool deleteAlbumSuccess, final Object? error, final Object? actionError}) = _$AlbumMediaListStateImpl; @override - List get medias; + Map get medias; + @override + List get selectedMedias; @override Album get album; @override bool get loading; @override + bool get loadingMore; + @override bool get deleteAlbumSuccess; @override Object? get error; diff --git a/app/lib/ui/flow/home/components/multi_selection_done_button.dart b/app/lib/ui/flow/home/components/multi_selection_done_button.dart index da582abb..6123a4c8 100644 --- a/app/lib/ui/flow/home/components/multi_selection_done_button.dart +++ b/app/lib/ui/flow/home/components/multi_selection_done_button.dart @@ -1,6 +1,7 @@ import 'dart:io'; import 'package:data/models/media/media_extension.dart'; import '../../../../components/app_dialog.dart'; +import '../../../../components/selection_menu.dart'; import '../../../../domain/extensions/context_extensions.dart'; import '../../../../gen/assets.gen.dart'; import '../home_screen_view_model.dart'; @@ -12,11 +13,9 @@ import 'package:flutter_svg/svg.dart'; import 'package:go_router/go_router.dart'; import 'package:share_plus/share_plus.dart'; import 'package:style/extensions/context_extensions.dart'; -import '../../../../components/action_sheet.dart'; -import '../../../../components/app_sheet.dart'; -class MultiSelectionDoneButton extends ConsumerWidget { - const MultiSelectionDoneButton({super.key}); +class HomeSelectionMenu extends ConsumerWidget { + const HomeSelectionMenu({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { @@ -30,69 +29,67 @@ class MultiSelectionDoneButton extends ConsumerWidget { ), ); - return FloatingActionButton( - elevation: 3, - backgroundColor: context.colorScheme.primary, - onPressed: () { - showAppSheet( - context: context, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - if (state.selectedMedias.values.any( - (element) => - !element.sources.contains(AppMediaSource.googleDrive) && - element.sources.contains(AppMediaSource.local) && - state.googleAccount != null, - )) - _uploadToGoogleDriveAction(context, ref), - if (state.selectedMedias.values - .any((element) => element.isGoogleDriveStored) && - state.googleAccount != null) - _downloadFromGoogleDriveAction(context, ref), - if (state.selectedMedias.values.any( - (element) => - element.sources.contains(AppMediaSource.googleDrive), - ) && - state.googleAccount != null) - _deleteMediaFromGoogleDriveAction(context, ref), - if (state.selectedMedias.values.any( - (element) => - !element.sources.contains(AppMediaSource.dropbox) && - element.sources.contains(AppMediaSource.local) && - state.dropboxAccount != null, - )) - _uploadToDropboxAction(context, ref), - if (state.selectedMedias.values - .any((element) => element.isDropboxStored) && - state.dropboxAccount != null) - _downloadFromDropboxAction(context, ref), - if (state.selectedMedias.values.any( - (element) => - element.sources.contains(AppMediaSource.dropbox), - ) && - state.dropboxAccount != null) - _deleteMediaFromDropboxAction(context, ref), - if (state.selectedMedias.values.any( - (element) => element.sources.contains(AppMediaSource.local), - )) - _deleteFromDevice(context, ref), - if (state.selectedMedias.values - .any((element) => element.isLocalStored)) - _shareAction(context, state.selectedMedias), - ], - ), - ); - }, - child: Icon( - CupertinoIcons.checkmark_alt, - color: context.colorScheme.onPrimary, + return SelectionMenu( + useSystemPadding: false, + items: [ + _clearSelectionAction(context, ref), + if (state.selectedMedias.values.any( + (element) => + !element.sources.contains(AppMediaSource.googleDrive) && + element.sources.contains(AppMediaSource.local) && + state.googleAccount != null, + )) + _uploadToGoogleDriveAction(context, ref), + if (state.selectedMedias.values + .any((element) => element.isGoogleDriveStored) && + state.googleAccount != null) + _downloadFromGoogleDriveAction(context, ref), + if (state.selectedMedias.values.any( + (element) => element.sources.contains(AppMediaSource.googleDrive), + ) && + state.googleAccount != null) + _deleteMediaFromGoogleDriveAction(context, ref), + if (state.selectedMedias.values.any( + (element) => + !element.sources.contains(AppMediaSource.dropbox) && + element.sources.contains(AppMediaSource.local) && + state.dropboxAccount != null, + )) + _uploadToDropboxAction(context, ref), + if (state.selectedMedias.values + .any((element) => element.isDropboxStored) && + state.dropboxAccount != null) + _downloadFromDropboxAction(context, ref), + if (state.selectedMedias.values.any( + (element) => element.sources.contains(AppMediaSource.dropbox), + ) && + state.dropboxAccount != null) + _deleteMediaFromDropboxAction(context, ref), + if (state.selectedMedias.values.any( + (element) => element.sources.contains(AppMediaSource.local), + )) + _deleteFromDevice(context, ref), + if (state.selectedMedias.values.any((element) => element.isLocalStored)) + _shareAction(context, state.selectedMedias, ref), + ], + show: state.selectedMedias.isNotEmpty, + ); + } + + Widget _clearSelectionAction(BuildContext context, WidgetRef ref) { + return SelectionMenuAction( + title: context.l10n.common_cancel, + icon: Icon( + Icons.close, + color: context.colorScheme.textPrimary, + size: 22, ), + onTap: ref.read(homeViewStateNotifier.notifier).clearSelection, ); } Widget _uploadToGoogleDriveAction(BuildContext context, WidgetRef ref) { - return AppSheetAction( + return SelectionMenuAction( icon: Stack( alignment: Alignment.bottomRight, children: [ @@ -112,8 +109,7 @@ class MultiSelectionDoneButton extends ConsumerWidget { ], ), title: context.l10n.upload_to_google_drive_title, - onPressed: () { - context.pop(); + onTap: () { showAppAlertDialog( context: context, title: context.l10n.upload_to_google_drive_title, @@ -128,8 +124,8 @@ class MultiSelectionDoneButton extends ConsumerWidget { AppAlertAction( title: context.l10n.common_upload, onPressed: () { - ref.read(homeViewStateNotifier.notifier).uploadToGoogleDrive(); context.pop(); + ref.read(homeViewStateNotifier.notifier).uploadToGoogleDrive(); }, ), ], @@ -139,7 +135,7 @@ class MultiSelectionDoneButton extends ConsumerWidget { } Widget _downloadFromGoogleDriveAction(BuildContext context, WidgetRef ref) { - return AppSheetAction( + return SelectionMenuAction( icon: Stack( alignment: Alignment.bottomRight, children: [ @@ -159,8 +155,7 @@ class MultiSelectionDoneButton extends ConsumerWidget { ], ), title: context.l10n.download_from_google_drive_title, - onPressed: () async { - context.pop(); + onTap: () async { showAppAlertDialog( context: context, title: context.l10n.download_from_google_drive_title, @@ -175,10 +170,10 @@ class MultiSelectionDoneButton extends ConsumerWidget { AppAlertAction( title: context.l10n.common_download, onPressed: () { + context.pop(); ref .read(homeViewStateNotifier.notifier) .downloadFromGoogleDrive(); - context.pop(); }, ), ], @@ -191,7 +186,7 @@ class MultiSelectionDoneButton extends ConsumerWidget { BuildContext context, WidgetRef ref, ) { - return AppSheetAction( + return SelectionMenuAction( icon: Stack( alignment: Alignment.bottomRight, children: [ @@ -211,8 +206,7 @@ class MultiSelectionDoneButton extends ConsumerWidget { ], ), title: context.l10n.delete_from_google_drive_title, - onPressed: () { - context.pop(); + onTap: () { showAppAlertDialog( context: context, title: context.l10n.delete_from_google_drive_title, @@ -228,10 +222,10 @@ class MultiSelectionDoneButton extends ConsumerWidget { isDestructiveAction: true, title: context.l10n.common_delete, onPressed: () { + context.pop(); ref .read(homeViewStateNotifier.notifier) .deleteGoogleDriveMedias(); - context.pop(); }, ), ], @@ -241,7 +235,7 @@ class MultiSelectionDoneButton extends ConsumerWidget { } Widget _uploadToDropboxAction(BuildContext context, WidgetRef ref) { - return AppSheetAction( + return SelectionMenuAction( icon: Stack( alignment: Alignment.bottomRight, children: [ @@ -261,8 +255,7 @@ class MultiSelectionDoneButton extends ConsumerWidget { ], ), title: context.l10n.upload_to_dropbox_title, - onPressed: () { - context.pop(); + onTap: () { showAppAlertDialog( context: context, title: context.l10n.upload_to_dropbox_title, @@ -277,8 +270,8 @@ class MultiSelectionDoneButton extends ConsumerWidget { AppAlertAction( title: context.l10n.common_upload, onPressed: () { - ref.read(homeViewStateNotifier.notifier).uploadToDropbox(); context.pop(); + ref.read(homeViewStateNotifier.notifier).uploadToDropbox(); }, ), ], @@ -288,7 +281,7 @@ class MultiSelectionDoneButton extends ConsumerWidget { } Widget _downloadFromDropboxAction(BuildContext context, WidgetRef ref) { - return AppSheetAction( + return SelectionMenuAction( icon: Stack( alignment: Alignment.bottomRight, children: [ @@ -308,8 +301,7 @@ class MultiSelectionDoneButton extends ConsumerWidget { ], ), title: context.l10n.download_from_dropbox_title, - onPressed: () async { - context.pop(); + onTap: () async { showAppAlertDialog( context: context, title: context.l10n.download_from_dropbox_title, @@ -324,8 +316,8 @@ class MultiSelectionDoneButton extends ConsumerWidget { AppAlertAction( title: context.l10n.common_download, onPressed: () { - ref.read(homeViewStateNotifier.notifier).downloadFromDropbox(); context.pop(); + ref.read(homeViewStateNotifier.notifier).downloadFromDropbox(); }, ), ], @@ -335,7 +327,7 @@ class MultiSelectionDoneButton extends ConsumerWidget { } Widget _deleteMediaFromDropboxAction(BuildContext context, WidgetRef ref) { - return AppSheetAction( + return SelectionMenuAction( icon: Stack( alignment: Alignment.bottomRight, children: [ @@ -355,8 +347,7 @@ class MultiSelectionDoneButton extends ConsumerWidget { ], ), title: context.l10n.delete_from_dropbox_title, - onPressed: () { - context.pop(); + onTap: () { showAppAlertDialog( context: context, title: context.l10n.delete_from_dropbox_title, @@ -372,8 +363,8 @@ class MultiSelectionDoneButton extends ConsumerWidget { isDestructiveAction: true, title: context.l10n.common_delete, onPressed: () { - ref.read(homeViewStateNotifier.notifier).deleteDropboxMedias(); context.pop(); + ref.read(homeViewStateNotifier.notifier).deleteDropboxMedias(); }, ), ], @@ -383,14 +374,13 @@ class MultiSelectionDoneButton extends ConsumerWidget { } Widget _deleteFromDevice(BuildContext context, WidgetRef ref) { - return AppSheetAction( + return SelectionMenuAction( icon: const Icon( CupertinoIcons.delete, size: 24, ), title: context.l10n.delete_from_device_title, - onPressed: () { - context.pop(); + onTap: () { showAppAlertDialog( context: context, title: context.l10n.delete_from_device_title, @@ -406,8 +396,8 @@ class MultiSelectionDoneButton extends ConsumerWidget { isDestructiveAction: true, title: context.l10n.common_delete, onPressed: () { - ref.read(homeViewStateNotifier.notifier).deleteLocalMedias(); context.pop(); + ref.read(homeViewStateNotifier.notifier).deleteLocalMedias(); }, ), ], @@ -419,22 +409,23 @@ class MultiSelectionDoneButton extends ConsumerWidget { Widget _shareAction( BuildContext context, Map selectedMedias, + WidgetRef ref, ) { - return AppSheetAction( + return SelectionMenuAction( icon: Icon( Platform.isIOS ? CupertinoIcons.share : Icons.share_rounded, color: context.colorScheme.textSecondary, size: 24, ), title: context.l10n.common_share, - onPressed: () { + onTap: () { Share.shareXFiles( selectedMedias.values .where((element) => element.isLocalStored) .map((e) => XFile(e.path)) .toList(), ); - context.pop(); + ref.read(homeViewStateNotifier.notifier).clearSelection(); }, ); } diff --git a/app/lib/ui/flow/home/home_screen.dart b/app/lib/ui/flow/home/home_screen.dart index 17982004..217d7bac 100644 --- a/app/lib/ui/flow/home/home_screen.dart +++ b/app/lib/ui/flow/home/home_screen.dart @@ -73,7 +73,6 @@ class _HomeScreenState extends ConsumerState { homeViewStateNotifier.select( (value) => ( hasMedia: value.medias.isNotEmpty, - hasSelectedMedia: value.selectedMedias.isNotEmpty, isLoading: value.loading, hasLocalMediaAccess: value.hasLocalMediaAccess, error: value.error, @@ -92,15 +91,10 @@ class _HomeScreenState extends ConsumerState { ); } - return Stack( - alignment: Alignment.bottomRight, + return Column( children: [ - _buildMediaList(context: context), - if (state.hasSelectedMedia) - Padding( - padding: context.systemPadding + const EdgeInsets.all(16), - child: const MultiSelectionDoneButton(), - ), + Expanded(child: _buildMediaList(context: context)), + const HomeSelectionMenu(), ], ); } diff --git a/app/lib/ui/flow/home/home_screen_view_model.dart b/app/lib/ui/flow/home/home_screen_view_model.dart index ace5a891..2635de5a 100644 --- a/app/lib/ui/flow/home/home_screen_view_model.dart +++ b/app/lib/ui/flow/home/home_screen_view_model.dart @@ -487,6 +487,10 @@ class HomeViewStateNotifier extends StateNotifier state = state.copyWith(selectedMedias: selectedMedias); } + void clearSelection() { + state = state.copyWith(selectedMedias: {}); + } + Future uploadToGoogleDrive() async { try { if (state.googleAccount == null) return; diff --git a/app/lib/ui/flow/media_metadata_details/media_metadata_details.dart b/app/lib/ui/flow/media_metadata_details/media_metadata_details.dart index 1fbc04fb..4df28946 100644 --- a/app/lib/ui/flow/media_metadata_details/media_metadata_details.dart +++ b/app/lib/ui/flow/media_metadata_details/media_metadata_details.dart @@ -31,6 +31,7 @@ class MediaMetadataDetailsScreen extends StatelessWidget { alignment: Alignment.center, children: [ AppMediaImage( + heroTag: "media_metadata_details${media.toString()}", size: Size(context.mediaQuerySize.width, 200), media: media, radius: 0, diff --git a/app/lib/ui/flow/media_selection/media_selection_screen.dart b/app/lib/ui/flow/media_selection/media_selection_screen.dart index cb6cdc54..0d52b8fa 100644 --- a/app/lib/ui/flow/media_selection/media_selection_screen.dart +++ b/app/lib/ui/flow/media_selection/media_selection_screen.dart @@ -157,7 +157,7 @@ class _MediaSelectionScreenState extends ConsumerState { ), itemCount: gridEntry.value.length, itemBuilder: (context, index) => AppMediaThumbnail( - heroTag: "selection${gridEntry.value.elementAt(index)}", + heroTag: "selection${gridEntry.value.elementAt(index).toString()}", onTap: () { _notifier.toggleMediaSelection( gridEntry.value.elementAt(index), diff --git a/data/lib/services/dropbox_services.dart b/data/lib/services/dropbox_services.dart index 11b83213..21ac4f8c 100644 --- a/data/lib/services/dropbox_services.dart +++ b/data/lib/services/dropbox_services.dart @@ -230,20 +230,28 @@ class DropboxService extends CloudProviderService { } } - Future getMedia({ + Future getMedia({ required String id, }) async { - final res = await _dropboxAuthenticatedDio.req( - DropboxGetFileMetadata(id: id), - ); + try { + final res = await _dropboxAuthenticatedDio.req( + DropboxGetFileMetadata(id: id), + ); - if (res.statusCode == 200) { - return AppMedia.fromDropboxJson(json: res.data, metadataJson: res.data); + if (res.statusCode == 200) { + return AppMedia.fromDropboxJson(json: res.data, metadataJson: res.data); + } + throw SomethingWentWrongError( + statusCode: res.statusCode, + message: res.statusMessage ?? '', + ); + } catch (e) { + if (e is DioException && + (e.response?.statusCode == 409 || e.response?.statusCode == 404)) { + return null; + } + rethrow; } - throw SomethingWentWrongError( - statusCode: res.statusCode, - message: res.statusMessage ?? '', - ); } @override diff --git a/data/lib/services/google_drive_service.dart b/data/lib/services/google_drive_service.dart index 8f4f902e..c4ba390e 100644 --- a/data/lib/services/google_drive_service.dart +++ b/data/lib/services/google_drive_service.dart @@ -154,19 +154,27 @@ class GoogleDriveService extends CloudProviderService { ); } - Future getMedia({ + Future getMedia({ required String id, }) async { - final res = await _client.req(GoogleDriveGetEndpoint(id: id)); + try { + final res = await _client.req(GoogleDriveGetEndpoint(id: id)); - if (res.statusCode == 200) { - return AppMedia.fromGoogleDriveFile(drive.File.fromJson(res.data)); - } + if (res.statusCode == 200) { + return AppMedia.fromGoogleDriveFile(drive.File.fromJson(res.data)); + } - throw SomethingWentWrongError( - statusCode: res.statusCode, - message: res.statusMessage, - ); + throw SomethingWentWrongError( + statusCode: res.statusCode, + message: res.statusMessage, + ); + } catch (e) { + if (e is DioException && + (e.response?.statusCode == 404 || e.response?.statusCode == 409)) { + return null; + } + rethrow; + } } @override From 9da649e90e71c3f2b979d6b9f9b50dce16212c8f Mon Sep 17 00:00:00 2001 From: Pratik-canopas Date: Wed, 8 Jan 2025 15:17:51 +0530 Subject: [PATCH 16/24] Add new selection menu --- app/lib/ui/flow/media_selection/media_selection_screen.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/lib/ui/flow/media_selection/media_selection_screen.dart b/app/lib/ui/flow/media_selection/media_selection_screen.dart index 0d52b8fa..30ae7dfa 100644 --- a/app/lib/ui/flow/media_selection/media_selection_screen.dart +++ b/app/lib/ui/flow/media_selection/media_selection_screen.dart @@ -157,7 +157,8 @@ class _MediaSelectionScreenState extends ConsumerState { ), itemCount: gridEntry.value.length, itemBuilder: (context, index) => AppMediaThumbnail( - heroTag: "selection${gridEntry.value.elementAt(index).toString()}", + heroTag: + "selection${gridEntry.value.elementAt(index).toString()}", onTap: () { _notifier.toggleMediaSelection( gridEntry.value.elementAt(index), From 068f0ad041c7bac46b46bec06ca9f97c5a457cce Mon Sep 17 00:00:00 2001 From: Pratik-canopas Date: Wed, 8 Jan 2025 15:19:59 +0530 Subject: [PATCH 17/24] push build --- .github/workflows/ios_deploy.yml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/.github/workflows/ios_deploy.yml b/.github/workflows/ios_deploy.yml index dcdc5ad3..d4b79584 100644 --- a/.github/workflows/ios_deploy.yml +++ b/.github/workflows/ios_deploy.yml @@ -1,10 +1,5 @@ name: Publish to App Store Connect -on: - push: - branches: - - main - workflow_dispatch: jobs: ios_deploy: From 2f42437305e006c6f8f35833b9687f5771b27d2a Mon Sep 17 00:00:00 2001 From: Pratik-canopas Date: Wed, 8 Jan 2025 15:21:49 +0530 Subject: [PATCH 18/24] publish build --- .github/workflows/ios_deploy.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ios_deploy.yml b/.github/workflows/ios_deploy.yml index d4b79584..1dd15000 100644 --- a/.github/workflows/ios_deploy.yml +++ b/.github/workflows/ios_deploy.yml @@ -1,5 +1,6 @@ name: Publish to App Store Connect +on: push jobs: ios_deploy: From 26104078a74472995b546445a4ff273fea284740 Mon Sep 17 00:00:00 2001 From: Pratik-canopas Date: Wed, 8 Jan 2025 15:36:39 +0530 Subject: [PATCH 19/24] Minor fix --- .../albums/media_list/album_media_list_state_notifier.dart | 4 +++- data/lib/domain/config.dart | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/app/lib/ui/flow/albums/media_list/album_media_list_state_notifier.dart b/app/lib/ui/flow/albums/media_list/album_media_list_state_notifier.dart index 7e2547a3..b70b7791 100644 --- a/app/lib/ui/flow/albums/media_list/album_media_list_state_notifier.dart +++ b/app/lib/ui/flow/albums/media_list/album_media_list_state_notifier.dart @@ -197,7 +197,9 @@ class AlbumMediaListStateNotifier extends StateNotifier { if (state.album.source == AppMediaSource.local) { await _localMediaService.updateAlbum( state.album.copyWith( - medias: append ? [...state.album.medias, ...medias] : medias, + medias: (append ? [...state.album.medias, ...medias] : medias) + .toSet() + .toList(), ), ); } else if (state.album.source == AppMediaSource.googleDrive) { diff --git a/data/lib/domain/config.dart b/data/lib/domain/config.dart index 4a454f6f..6227587f 100644 --- a/data/lib/domain/config.dart +++ b/data/lib/domain/config.dart @@ -16,5 +16,5 @@ class LocalDatabaseConstants { } class FeatureFlag { - static final googleDriveSupport = true; + static final googleDriveSupport = false; } From 8be451309eab383a8b76f6e5ebb93675b3abd6bd Mon Sep 17 00:00:00 2001 From: Pratik-canopas Date: Wed, 8 Jan 2025 17:33:15 +0530 Subject: [PATCH 20/24] Album support --- .github/workflows/ios_deploy.yml | 6 ++++- app/android/app/src/main/AndroidManifest.xml | 4 ++++ app/ios/Runner/Info.plist | 2 ++ .../album_media_list_state_notifier.dart | 24 +++++++++---------- 4 files changed, 23 insertions(+), 13 deletions(-) diff --git a/.github/workflows/ios_deploy.yml b/.github/workflows/ios_deploy.yml index 1dd15000..dcdc5ad3 100644 --- a/.github/workflows/ios_deploy.yml +++ b/.github/workflows/ios_deploy.yml @@ -1,6 +1,10 @@ name: Publish to App Store Connect -on: push +on: + push: + branches: + - main + workflow_dispatch: jobs: ios_deploy: diff --git a/app/android/app/src/main/AndroidManifest.xml b/app/android/app/src/main/AndroidManifest.xml index f10bf9e5..b44fd206 100644 --- a/app/android/app/src/main/AndroidManifest.xml +++ b/app/android/app/src/main/AndroidManifest.xml @@ -51,6 +51,10 @@ + + diff --git a/app/ios/Runner/Info.plist b/app/ios/Runner/Info.plist index 52b1c20e..7ffd752f 100644 --- a/app/ios/Runner/Info.plist +++ b/app/ios/Runner/Info.plist @@ -65,5 +65,7 @@ ITSAppUsesNonExemptEncryption + FlutterDeepLinkingEnabled + diff --git a/app/lib/ui/flow/albums/media_list/album_media_list_state_notifier.dart b/app/lib/ui/flow/albums/media_list/album_media_list_state_notifier.dart index b70b7791..486b916e 100644 --- a/app/lib/ui/flow/albums/media_list/album_media_list_state_notifier.dart +++ b/app/lib/ui/flow/albums/media_list/album_media_list_state_notifier.dart @@ -47,7 +47,11 @@ class AlbumMediaListStateNotifier extends StateNotifier { Future> loadMedia({bool reload = false}) async { ///TODO: remove media-ids which is deleted from source try { - if (state.loading || state.loadingMore) state.medias; + if (state.loading || + state.loadingMore || + (!reload && state.album.medias.length <= _loadedMediaCount)) { + return state.medias.values.toList(); + } state = state.copyWith( loading: state.medias.isEmpty, @@ -194,13 +198,13 @@ class AlbumMediaListStateNotifier extends StateNotifier { }) async { try { state = state.copyWith(actionError: null); + final updatedMedias = + (append ? [...state.album.medias, ...medias] : medias) + .toSet() + .toList(); if (state.album.source == AppMediaSource.local) { await _localMediaService.updateAlbum( - state.album.copyWith( - medias: (append ? [...state.album.medias, ...medias] : medias) - .toSet() - .toList(), - ), + state.album.copyWith(medias: updatedMedias), ); } else if (state.album.source == AppMediaSource.googleDrive) { _backupFolderId ??= await _googleDriveService.getBackUpFolderId(); @@ -209,15 +213,11 @@ class AlbumMediaListStateNotifier extends StateNotifier { } await _googleDriveService.updateAlbum( folderId: _backupFolderId!, - album: state.album.copyWith( - medias: append ? [...state.album.medias, ...medias] : medias, - ), + album: state.album.copyWith(medias: updatedMedias), ); } else if (state.album.source == AppMediaSource.dropbox) { await _dropboxService.updateAlbum( - state.album.copyWith( - medias: append ? [...state.album.medias, ...medias] : medias, - ), + state.album.copyWith(medias: updatedMedias), ); } loadAlbum(); From e14aa75947ee5ba43352404ca3ee2a2ff88f9770 Mon Sep 17 00:00:00 2001 From: Pratik-canopas Date: Wed, 8 Jan 2025 18:31:43 +0530 Subject: [PATCH 21/24] Album improvement --- .../media_list/album_media_list_screen.dart | 85 ++++++----- .../album_media_list_state_notifier.dart | 135 ++++++++++++++---- ...bum_media_list_state_notifier.freezed.dart | 64 ++++++++- 3 files changed, 224 insertions(+), 60 deletions(-) diff --git a/app/lib/ui/flow/albums/media_list/album_media_list_screen.dart b/app/lib/ui/flow/albums/media_list/album_media_list_screen.dart index d8d520fd..fded4ca4 100644 --- a/app/lib/ui/flow/albums/media_list/album_media_list_screen.dart +++ b/app/lib/ui/flow/albums/media_list/album_media_list_screen.dart @@ -64,7 +64,7 @@ class _AlbumMediaListScreenState extends ConsumerState { await MediaSelectionRoute($extra: widget.album.source) .push(context); if (res != null && res is List) { - await _notifier.updateAlbumMedias(medias: res); + await _notifier.addMediaInAlbum(medias: res); } }, icon: Icon( @@ -127,7 +127,7 @@ class _AlbumMediaListScreenState extends ConsumerState { error: state.error!, onRetryTap: () => _notifier.loadMedia(reload: true), ); - } else if (state.medias.isEmpty) { + } else if (state.medias.isEmpty && state.addingMedia.isEmpty) { return PlaceHolderScreen( icon: SvgPicture.asset( Assets.images.ilNoMediaFound, @@ -150,42 +150,63 @@ class _AlbumMediaListScreenState extends ConsumerState { crossAxisSpacing: 4, mainAxisSpacing: 4, ), - itemCount: state.medias.length, + itemCount: (state.medias.length + state.addingMedia.length), itemBuilder: (context, index) { - if (index == state.medias.length - 1) { + if (index >= + (state.medias.length + state.addingMedia.length) - 1) { runPostFrame(() { _notifier.loadMedia(); }); } - - return AppMediaThumbnail( - selected: state.selectedMedias - .contains(state.medias.keys.elementAt(index)), - onTap: () async { - if (state.selectedMedias.isNotEmpty) { - _notifier.toggleMediaSelection( - state.medias.keys.elementAt(index), - ); - return; - } - await MediaPreviewRoute( - $extra: MediaPreviewRouteData( - onLoadMore: _notifier.loadMedia, - heroTag: "album_media_list", - medias: state.medias.values.toList(), - startFrom: state.medias.values.elementAt(index).id, - ), - ).push(context); - _notifier.loadMedia(reload: true); - }, - onLongTap: () { - _notifier.toggleMediaSelection( + if (index < state.medias.length) { + return Opacity( + opacity: state.removingMedia.contains( state.medias.keys.elementAt(index), - ); - }, - heroTag: - "album_media_list${state.medias.values.elementAt(index).toString()}", - media: state.medias.values.elementAt(index), + ) + ? 0.7 + : 1, + child: AppMediaThumbnail( + selected: state.selectedMedias + .contains(state.medias.keys.elementAt(index)), + onTap: () async { + if (state.selectedMedias.isNotEmpty) { + _notifier.toggleMediaSelection( + state.medias.keys.elementAt(index), + ); + return; + } + await MediaPreviewRoute( + $extra: MediaPreviewRouteData( + onLoadMore: _notifier.loadMedia, + heroTag: "album_media_list", + medias: state.medias.values.toList(), + startFrom: + state.medias.values.elementAt(index).id, + ), + ).push(context); + _notifier.loadMedia(reload: true); + }, + onLongTap: () { + _notifier.toggleMediaSelection( + state.medias.keys.elementAt(index), + ); + }, + heroTag: + "album_media_list${state.medias.values.elementAt(index).toString()}", + media: state.medias.values.elementAt(index), + ), + ); + } + + return Container( + width: double.infinity, + height: double.infinity, + color: context.colorScheme.containerNormalOnSurface, + child: const Center( + child: AppCircularProgressIndicator( + size: 22, + ), + ), ); }, ), diff --git a/app/lib/ui/flow/albums/media_list/album_media_list_state_notifier.dart b/app/lib/ui/flow/albums/media_list/album_media_list_state_notifier.dart index 486b916e..e0e77e8e 100644 --- a/app/lib/ui/flow/albums/media_list/album_media_list_state_notifier.dart +++ b/app/lib/ui/flow/albums/media_list/album_media_list_state_notifier.dart @@ -45,7 +45,6 @@ class AlbumMediaListStateNotifier extends StateNotifier { } Future> loadMedia({bool reload = false}) async { - ///TODO: remove media-ids which is deleted from source try { if (state.loading || state.loadingMore || @@ -104,14 +103,7 @@ class AlbumMediaListStateNotifier extends StateNotifier { .toList(); if (manuallyRemovedMedia.isNotEmpty) { - updateAlbumMedias( - medias: state.album.medias - .where( - (element) => !manuallyRemovedMedia.contains(element), - ) - .toList(), - append: false, - ); + removeMediaFromAlbum(removeMediaList: manuallyRemovedMedia); } } catch (e, s) { state = state.copyWith( @@ -151,7 +143,6 @@ class AlbumMediaListStateNotifier extends StateNotifier { .firstWhereOrNull((element) => element.id == state.album.id) ?? state.album, ); - loadMedia(reload: true); } catch (e, s) { state = state.copyWith(actionError: e); _logger.e( @@ -192,20 +183,29 @@ class AlbumMediaListStateNotifier extends StateNotifier { } } - Future updateAlbumMedias({ + Future addMediaInAlbum({ required List medias, - bool append = true, }) async { + state = state.copyWith( + actionError: null, + addingMedia: [...state.addingMedia, ...medias], + ); try { - state = state.copyWith(actionError: null); - final updatedMedias = - (append ? [...state.album.medias, ...medias] : medias) - .toSet() - .toList(); + //Remove duplicate media ids + final updatedMedias = {...state.album.medias, ...medias}.toList(); + + Map moreMedia = {}; + if (state.album.source == AppMediaSource.local) { await _localMediaService.updateAlbum( state.album.copyWith(medias: updatedMedias), ); + + final res = await Future.wait( + medias.map((id) => _localMediaService.getMedia(id: id)), + ).then((value) => value.nonNulls.toList()); + + moreMedia = {for (final item in res) item.id: item}; } else if (state.album.source == AppMediaSource.googleDrive) { _backupFolderId ??= await _googleDriveService.getBackUpFolderId(); if (_backupFolderId == null) { @@ -215,28 +215,109 @@ class AlbumMediaListStateNotifier extends StateNotifier { folderId: _backupFolderId!, album: state.album.copyWith(medias: updatedMedias), ); + final res = await Future.wait( + medias.map((id) => _googleDriveService.getMedia(id: id)), + ).then((value) => value.nonNulls.toList()); + + moreMedia = {for (final item in res) item.driveMediaRefId!: item}; } else if (state.album.source == AppMediaSource.dropbox) { await _dropboxService.updateAlbum( state.album.copyWith(medias: updatedMedias), ); + final res = await Future.wait( + medias.map((id) => _dropboxService.getMedia(id: id)), + ).then((value) => value.nonNulls.toList()); + moreMedia = {for (final item in res) item.dropboxMediaRefId!: item}; } - loadAlbum(); + + state = state.copyWith( + addingMedia: state.addingMedia + .where( + (element) => !medias.contains(element), + ) + .toList(), + medias: {...state.medias, ...moreMedia}, + ); } catch (e, s) { - state = state.copyWith(actionError: e); + state = state.copyWith( + actionError: e, + addingMedia: state.addingMedia + .where( + (element) => !medias.contains(element), + ) + .toList(), + ); _logger.e( - "AlbumMediaListStateNotifier: Error adding media", + "AlbumMediaListStateNotifier: Error while adding media", error: e, stackTrace: s, ); } } - Future removeMediaFromAlbum() async { - final list = state.album.medias.toList(); - list.removeWhere((element) { - return state.selectedMedias.contains(element); - }); - await updateAlbumMedias(medias: list, append: false); + Future removeMediaFromAlbum({List? removeMediaList}) async { + final medias = removeMediaList ?? state.selectedMedias.toList(); + try { + state = state.copyWith( + actionError: null, + selectedMedias: + removeMediaList == null ? [] : state.selectedMedias.toList(), + removingMedia: [...state.removingMedia, ...medias], + ); + + final updatedMedias = state.album.medias + .toSet() + .where( + (element) => !medias.contains(element), + ) + .toList(); + if (state.album.source == AppMediaSource.local) { + await _localMediaService.updateAlbum( + state.album.copyWith(medias: updatedMedias), + ); + } else if (state.album.source == AppMediaSource.googleDrive) { + _backupFolderId ??= await _googleDriveService.getBackUpFolderId(); + if (_backupFolderId == null) { + throw BackUpFolderNotFound(); + } + await _googleDriveService.updateAlbum( + folderId: _backupFolderId!, + album: state.album.copyWith(medias: updatedMedias), + ); + } else if (state.album.source == AppMediaSource.dropbox) { + await _dropboxService.updateAlbum( + state.album.copyWith(medias: updatedMedias), + ); + } + + state = state.copyWith( + removingMedia: state.removingMedia + .where( + (element) => !medias.contains(element), + ) + .toList(), + medias: Map.fromEntries( + state.medias.entries.where( + (element) => !medias.contains(element.key), + ), + ), + album: state.album.copyWith(medias: updatedMedias), + ); + } catch (e, s) { + state = state.copyWith( + actionError: e, + removingMedia: state.removingMedia + .where( + (element) => !medias.contains(element), + ) + .toList(), + ); + _logger.e( + "AlbumMediaListStateNotifier: Error while removing media", + error: e, + stackTrace: s, + ); + } } void toggleMediaSelection(String id) { @@ -263,6 +344,8 @@ class AlbumMediaListState with _$AlbumMediaListState { required Album album, @Default(false) bool loading, @Default(false) bool loadingMore, + @Default([]) List addingMedia, + @Default([]) List removingMedia, @Default(false) bool deleteAlbumSuccess, Object? error, Object? actionError, diff --git a/app/lib/ui/flow/albums/media_list/album_media_list_state_notifier.freezed.dart b/app/lib/ui/flow/albums/media_list/album_media_list_state_notifier.freezed.dart index 69f0e383..18132153 100644 --- a/app/lib/ui/flow/albums/media_list/album_media_list_state_notifier.freezed.dart +++ b/app/lib/ui/flow/albums/media_list/album_media_list_state_notifier.freezed.dart @@ -21,6 +21,8 @@ mixin _$AlbumMediaListState { Album get album => throw _privateConstructorUsedError; bool get loading => throw _privateConstructorUsedError; bool get loadingMore => throw _privateConstructorUsedError; + List get addingMedia => throw _privateConstructorUsedError; + List get removingMedia => throw _privateConstructorUsedError; bool get deleteAlbumSuccess => throw _privateConstructorUsedError; Object? get error => throw _privateConstructorUsedError; Object? get actionError => throw _privateConstructorUsedError; @@ -44,6 +46,8 @@ abstract class $AlbumMediaListStateCopyWith<$Res> { Album album, bool loading, bool loadingMore, + List addingMedia, + List removingMedia, bool deleteAlbumSuccess, Object? error, Object? actionError}); @@ -71,6 +75,8 @@ class _$AlbumMediaListStateCopyWithImpl<$Res, $Val extends AlbumMediaListState> Object? album = null, Object? loading = null, Object? loadingMore = null, + Object? addingMedia = null, + Object? removingMedia = null, Object? deleteAlbumSuccess = null, Object? error = freezed, Object? actionError = freezed, @@ -96,6 +102,14 @@ class _$AlbumMediaListStateCopyWithImpl<$Res, $Val extends AlbumMediaListState> ? _value.loadingMore : loadingMore // ignore: cast_nullable_to_non_nullable as bool, + addingMedia: null == addingMedia + ? _value.addingMedia + : addingMedia // ignore: cast_nullable_to_non_nullable + as List, + removingMedia: null == removingMedia + ? _value.removingMedia + : removingMedia // ignore: cast_nullable_to_non_nullable + as List, deleteAlbumSuccess: null == deleteAlbumSuccess ? _value.deleteAlbumSuccess : deleteAlbumSuccess // ignore: cast_nullable_to_non_nullable @@ -130,6 +144,8 @@ abstract class _$$AlbumMediaListStateImplCopyWith<$Res> Album album, bool loading, bool loadingMore, + List addingMedia, + List removingMedia, bool deleteAlbumSuccess, Object? error, Object? actionError}); @@ -156,6 +172,8 @@ class __$$AlbumMediaListStateImplCopyWithImpl<$Res> Object? album = null, Object? loading = null, Object? loadingMore = null, + Object? addingMedia = null, + Object? removingMedia = null, Object? deleteAlbumSuccess = null, Object? error = freezed, Object? actionError = freezed, @@ -181,6 +199,14 @@ class __$$AlbumMediaListStateImplCopyWithImpl<$Res> ? _value.loadingMore : loadingMore // ignore: cast_nullable_to_non_nullable as bool, + addingMedia: null == addingMedia + ? _value._addingMedia + : addingMedia // ignore: cast_nullable_to_non_nullable + as List, + removingMedia: null == removingMedia + ? _value._removingMedia + : removingMedia // ignore: cast_nullable_to_non_nullable + as List, deleteAlbumSuccess: null == deleteAlbumSuccess ? _value.deleteAlbumSuccess : deleteAlbumSuccess // ignore: cast_nullable_to_non_nullable @@ -200,11 +226,15 @@ class _$AlbumMediaListStateImpl implements _AlbumMediaListState { required this.album, this.loading = false, this.loadingMore = false, + final List addingMedia = const [], + final List removingMedia = const [], this.deleteAlbumSuccess = false, this.error, this.actionError}) : _medias = medias, - _selectedMedias = selectedMedias; + _selectedMedias = selectedMedias, + _addingMedia = addingMedia, + _removingMedia = removingMedia; final Map _medias; @override @@ -232,6 +262,24 @@ class _$AlbumMediaListStateImpl implements _AlbumMediaListState { @override @JsonKey() final bool loadingMore; + final List _addingMedia; + @override + @JsonKey() + List get addingMedia { + if (_addingMedia is EqualUnmodifiableListView) return _addingMedia; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_addingMedia); + } + + final List _removingMedia; + @override + @JsonKey() + List get removingMedia { + if (_removingMedia is EqualUnmodifiableListView) return _removingMedia; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_removingMedia); + } + @override @JsonKey() final bool deleteAlbumSuccess; @@ -242,7 +290,7 @@ class _$AlbumMediaListStateImpl implements _AlbumMediaListState { @override String toString() { - return 'AlbumMediaListState(medias: $medias, selectedMedias: $selectedMedias, album: $album, loading: $loading, loadingMore: $loadingMore, deleteAlbumSuccess: $deleteAlbumSuccess, error: $error, actionError: $actionError)'; + return 'AlbumMediaListState(medias: $medias, selectedMedias: $selectedMedias, album: $album, loading: $loading, loadingMore: $loadingMore, addingMedia: $addingMedia, removingMedia: $removingMedia, deleteAlbumSuccess: $deleteAlbumSuccess, error: $error, actionError: $actionError)'; } @override @@ -257,6 +305,10 @@ class _$AlbumMediaListStateImpl implements _AlbumMediaListState { (identical(other.loading, loading) || other.loading == loading) && (identical(other.loadingMore, loadingMore) || other.loadingMore == loadingMore) && + const DeepCollectionEquality() + .equals(other._addingMedia, _addingMedia) && + const DeepCollectionEquality() + .equals(other._removingMedia, _removingMedia) && (identical(other.deleteAlbumSuccess, deleteAlbumSuccess) || other.deleteAlbumSuccess == deleteAlbumSuccess) && const DeepCollectionEquality().equals(other.error, error) && @@ -272,6 +324,8 @@ class _$AlbumMediaListStateImpl implements _AlbumMediaListState { album, loading, loadingMore, + const DeepCollectionEquality().hash(_addingMedia), + const DeepCollectionEquality().hash(_removingMedia), deleteAlbumSuccess, const DeepCollectionEquality().hash(error), const DeepCollectionEquality().hash(actionError)); @@ -293,6 +347,8 @@ abstract class _AlbumMediaListState implements AlbumMediaListState { required final Album album, final bool loading, final bool loadingMore, + final List addingMedia, + final List removingMedia, final bool deleteAlbumSuccess, final Object? error, final Object? actionError}) = _$AlbumMediaListStateImpl; @@ -308,6 +364,10 @@ abstract class _AlbumMediaListState implements AlbumMediaListState { @override bool get loadingMore; @override + List get addingMedia; + @override + List get removingMedia; + @override bool get deleteAlbumSuccess; @override Object? get error; From 9d81f7f0ece5855df7abd757f664ab0edd35a399 Mon Sep 17 00:00:00 2001 From: Pratik-canopas Date: Wed, 8 Jan 2025 18:36:58 +0530 Subject: [PATCH 22/24] Album support --- app/assets/locales/app_en.arb | 2 ++ .../ui/flow/albums/media_list/album_media_list_screen.dart | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/app/assets/locales/app_en.arb b/app/assets/locales/app_en.arb index 44fec237..c342a5d3 100644 --- a/app/assets/locales/app_en.arb +++ b/app/assets/locales/app_en.arb @@ -134,6 +134,8 @@ "edit_album_action_title": "Edit Album", "delete_album_action_title": "Delete Album", "remove_item_action_title": "Remove Items", + "empty_album_media_list_title": "A Quiet Album", + "empty_album_media_list_message": "It looks like you don't have any media in this album just yet. Go ahead, add some photos or videos, and let's make this place sparkle!", "@_MEDIA_SELECTION":{}, "select_from_device_title": "Select from Device", diff --git a/app/lib/ui/flow/albums/media_list/album_media_list_screen.dart b/app/lib/ui/flow/albums/media_list/album_media_list_screen.dart index fded4ca4..a409b493 100644 --- a/app/lib/ui/flow/albums/media_list/album_media_list_screen.dart +++ b/app/lib/ui/flow/albums/media_list/album_media_list_screen.dart @@ -133,8 +133,8 @@ class _AlbumMediaListScreenState extends ConsumerState { Assets.images.ilNoMediaFound, width: 150, ), - title: context.l10n.empty_media_title, - message: context.l10n.empty_media_message, + title: context.l10n.empty_album_media_list_title, + message: context.l10n.empty_album_media_list_message, ); } return Column( From 33fcdf1fbfe9dbb7191ed736f88a88dd0c374181 Mon Sep 17 00:00:00 2001 From: Pratik-canopas Date: Thu, 9 Jan 2025 10:34:14 +0530 Subject: [PATCH 23/24] Dispose unused streams --- .idea/libraries/Flutter_Plugins.xml | 36 +++++++++---------- app/assets/locales/app_en.arb | 2 +- app/ios/Runner/Info.plist | 4 +-- .../ui/flow/albums/add/add_album_screen.dart | 2 +- .../albums/add/add_album_state_notifier.dart | 10 ++++-- app/lib/ui/flow/albums/albums_screen.dart | 2 +- .../ui/flow/albums/albums_view_notifier.dart | 11 ++++-- data/.flutter-plugins-dependencies | 2 +- data/lib/apis/network/client.dart | 6 +++- .../media_process_repository.dart | 7 ++-- 10 files changed, 51 insertions(+), 31 deletions(-) diff --git a/.idea/libraries/Flutter_Plugins.xml b/.idea/libraries/Flutter_Plugins.xml index a5962037..5d64713e 100644 --- a/.idea/libraries/Flutter_Plugins.xml +++ b/.idea/libraries/Flutter_Plugins.xml @@ -1,23 +1,35 @@ - - - - - + + + + + + + + + + + + + + + + + + - @@ -25,32 +37,20 @@ - - - - - - - - - - - - diff --git a/app/assets/locales/app_en.arb b/app/assets/locales/app_en.arb index c342a5d3..88902bb0 100644 --- a/app/assets/locales/app_en.arb +++ b/app/assets/locales/app_en.arb @@ -125,7 +125,7 @@ "@_ADD_ALBUM":{}, "add_album_screen_title": "Album", - "album_tame_field_title": "Album Name", + "album_name_field_title": "Album Name", "store_in_title": "Store In", "store_in_device_title": "Device", diff --git a/app/ios/Runner/Info.plist b/app/ios/Runner/Info.plist index 7ffd752f..e2c5f2c6 100644 --- a/app/ios/Runner/Info.plist +++ b/app/ios/Runner/Info.plist @@ -65,7 +65,7 @@ ITSAppUsesNonExemptEncryption - FlutterDeepLinkingEnabled - + FlutterDeepLinkingEnabled + diff --git a/app/lib/ui/flow/albums/add/add_album_screen.dart b/app/lib/ui/flow/albums/add/add_album_screen.dart index cd7000ec..baddb67d 100644 --- a/app/lib/ui/flow/albums/add/add_album_screen.dart +++ b/app/lib/ui/flow/albums/add/add_album_screen.dart @@ -96,7 +96,7 @@ class _AddAlbumScreenState extends ConsumerState { child: AppTextField( controller: state.albumNameController, onChanged: _notifier.validateAlbumName, - label: context.l10n.album_tame_field_title, + label: context.l10n.album_name_field_title, ), ), if ((state.googleAccount != null || state.dropboxAccount != null) && diff --git a/app/lib/ui/flow/albums/add/add_album_state_notifier.dart b/app/lib/ui/flow/albums/add/add_album_state_notifier.dart index 5654556d..edce0eef 100644 --- a/app/lib/ui/flow/albums/add/add_album_state_notifier.dart +++ b/app/lib/ui/flow/albums/add/add_album_state_notifier.dart @@ -29,14 +29,20 @@ final addAlbumStateNotifierProvider = StateNotifierProvider.autoDispose ref.read(loggerProvider), state, ); - ref.listen( + final googleDriveAccountSubscription = ref.listen( googleUserAccountProvider, (_, googleAccount) => notifier.onGoogleDriveAccountChange(googleAccount), ); - ref.listen( + final dropboxAccountSubscription = ref.listen( AppPreferences.dropboxCurrentUserAccount, (_, dropboxAccount) => notifier.onDropboxAccountChange(dropboxAccount), ); + + ref.onDispose(() { + googleDriveAccountSubscription.close(); + dropboxAccountSubscription.close(); + }); + return notifier; }); diff --git a/app/lib/ui/flow/albums/albums_screen.dart b/app/lib/ui/flow/albums/albums_screen.dart index f990a684..7493de07 100644 --- a/app/lib/ui/flow/albums/albums_screen.dart +++ b/app/lib/ui/flow/albums/albums_screen.dart @@ -29,8 +29,8 @@ class _AlbumsScreenState extends ConsumerState { @override void initState() { - _notifier = ref.read(albumStateNotifierProvider.notifier); super.initState(); + _notifier = ref.read(albumStateNotifierProvider.notifier); } void _observeError(BuildContext context) { diff --git a/app/lib/ui/flow/albums/albums_view_notifier.dart b/app/lib/ui/flow/albums/albums_view_notifier.dart index e0f1b943..8d1235f8 100644 --- a/app/lib/ui/flow/albums/albums_view_notifier.dart +++ b/app/lib/ui/flow/albums/albums_view_notifier.dart @@ -25,12 +25,19 @@ final albumStateNotifierProvider = ref.read(googleUserAccountProvider), ref.read(AppPreferences.dropboxCurrentUserAccount), ); - ref.listen(googleUserAccountProvider, (p, c) { + final googleDriveAccountSubscription = + ref.listen(googleUserAccountProvider, (p, c) { notifier.onGoogleDriveAccountChange(c); }); - ref.listen(AppPreferences.dropboxCurrentUserAccount, (p, c) { + final dropboxAccountSubscription = + ref.listen(AppPreferences.dropboxCurrentUserAccount, (p, c) { notifier.onDropboxAccountChange(c); }); + + ref.onDispose(() { + googleDriveAccountSubscription.close(); + dropboxAccountSubscription.close(); + }); return notifier; }); diff --git a/data/.flutter-plugins-dependencies b/data/.flutter-plugins-dependencies index 9289aa2d..7ce89141 100644 --- a/data/.flutter-plugins-dependencies +++ b/data/.flutter-plugins-dependencies @@ -1 +1 @@ -{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"flutter_local_notifications","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/flutter_local_notifications-18.0.1/","native_build":true,"dependencies":[]},{"name":"google_sign_in_ios","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/google_sign_in_ios-5.7.8/","shared_darwin_source":true,"native_build":true,"dependencies":[]},{"name":"package_info_plus","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/package_info_plus-8.1.2/","native_build":true,"dependencies":[]},{"name":"path_provider_foundation","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/path_provider_foundation-2.4.1/","shared_darwin_source":true,"native_build":true,"dependencies":[]},{"name":"photo_manager","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/photo_manager-3.6.3/","native_build":true,"dependencies":[]},{"name":"shared_preferences_foundation","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/shared_preferences_foundation-2.5.4/","shared_darwin_source":true,"native_build":true,"dependencies":[]},{"name":"sqflite_darwin","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/sqflite_darwin-2.4.1+1/","shared_darwin_source":true,"native_build":true,"dependencies":[]},{"name":"url_launcher_ios","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/url_launcher_ios-6.3.2/","native_build":true,"dependencies":[]}],"android":[{"name":"flutter_local_notifications","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/flutter_local_notifications-18.0.1/","native_build":true,"dependencies":[]},{"name":"google_sign_in_android","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/google_sign_in_android-6.1.34/","native_build":true,"dependencies":[]},{"name":"package_info_plus","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/package_info_plus-8.1.2/","native_build":true,"dependencies":[]},{"name":"path_provider_android","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/path_provider_android-2.2.15/","native_build":true,"dependencies":[]},{"name":"photo_manager","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/photo_manager-3.6.3/","native_build":true,"dependencies":[]},{"name":"shared_preferences_android","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/shared_preferences_android-2.4.0/","native_build":true,"dependencies":[]},{"name":"sqflite_android","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/sqflite_android-2.4.0/","native_build":true,"dependencies":[]},{"name":"url_launcher_android","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/url_launcher_android-6.3.14/","native_build":true,"dependencies":[]}],"macos":[{"name":"flutter_local_notifications","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/flutter_local_notifications-18.0.1/","native_build":true,"dependencies":[]},{"name":"google_sign_in_ios","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/google_sign_in_ios-5.7.8/","shared_darwin_source":true,"native_build":true,"dependencies":[]},{"name":"package_info_plus","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/package_info_plus-8.1.2/","native_build":true,"dependencies":[]},{"name":"path_provider_foundation","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/path_provider_foundation-2.4.1/","shared_darwin_source":true,"native_build":true,"dependencies":[]},{"name":"photo_manager","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/photo_manager-3.6.3/","native_build":true,"dependencies":[]},{"name":"shared_preferences_foundation","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/shared_preferences_foundation-2.5.4/","shared_darwin_source":true,"native_build":true,"dependencies":[]},{"name":"sqflite_darwin","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/sqflite_darwin-2.4.1+1/","shared_darwin_source":true,"native_build":true,"dependencies":[]},{"name":"url_launcher_macos","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/url_launcher_macos-3.2.2/","native_build":true,"dependencies":[]}],"linux":[{"name":"flutter_local_notifications_linux","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/flutter_local_notifications_linux-5.0.0/","native_build":false,"dependencies":[]},{"name":"package_info_plus","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/package_info_plus-8.1.2/","native_build":false,"dependencies":[]},{"name":"path_provider_linux","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/path_provider_linux-2.2.1/","native_build":false,"dependencies":[]},{"name":"shared_preferences_linux","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/shared_preferences_linux-2.4.1/","native_build":false,"dependencies":["path_provider_linux"]},{"name":"url_launcher_linux","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/url_launcher_linux-3.2.1/","native_build":true,"dependencies":[]}],"windows":[{"name":"package_info_plus","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/package_info_plus-8.1.2/","native_build":false,"dependencies":[]},{"name":"path_provider_windows","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/path_provider_windows-2.3.0/","native_build":false,"dependencies":[]},{"name":"shared_preferences_windows","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/shared_preferences_windows-2.4.1/","native_build":false,"dependencies":["path_provider_windows"]},{"name":"url_launcher_windows","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/url_launcher_windows-3.1.3/","native_build":true,"dependencies":[]}],"web":[{"name":"google_sign_in_web","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/google_sign_in_web-0.12.4+3/","dependencies":[]},{"name":"package_info_plus","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/package_info_plus-8.1.2/","dependencies":[]},{"name":"shared_preferences_web","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/shared_preferences_web-2.4.2/","dependencies":[]},{"name":"url_launcher_web","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/url_launcher_web-2.3.3/","dependencies":[]}]},"dependencyGraph":[{"name":"flutter_local_notifications","dependencies":["flutter_local_notifications_linux"]},{"name":"flutter_local_notifications_linux","dependencies":[]},{"name":"google_sign_in","dependencies":["google_sign_in_android","google_sign_in_ios","google_sign_in_web"]},{"name":"google_sign_in_android","dependencies":[]},{"name":"google_sign_in_ios","dependencies":[]},{"name":"google_sign_in_web","dependencies":[]},{"name":"package_info_plus","dependencies":[]},{"name":"path_provider","dependencies":["path_provider_android","path_provider_foundation","path_provider_linux","path_provider_windows"]},{"name":"path_provider_android","dependencies":[]},{"name":"path_provider_foundation","dependencies":[]},{"name":"path_provider_linux","dependencies":[]},{"name":"path_provider_windows","dependencies":[]},{"name":"photo_manager","dependencies":[]},{"name":"shared_preferences","dependencies":["shared_preferences_android","shared_preferences_foundation","shared_preferences_linux","shared_preferences_web","shared_preferences_windows"]},{"name":"shared_preferences_android","dependencies":[]},{"name":"shared_preferences_foundation","dependencies":[]},{"name":"shared_preferences_linux","dependencies":["path_provider_linux"]},{"name":"shared_preferences_web","dependencies":[]},{"name":"shared_preferences_windows","dependencies":["path_provider_windows"]},{"name":"sqflite","dependencies":["sqflite_android","sqflite_darwin"]},{"name":"sqflite_android","dependencies":[]},{"name":"sqflite_darwin","dependencies":[]},{"name":"url_launcher","dependencies":["url_launcher_android","url_launcher_ios","url_launcher_linux","url_launcher_macos","url_launcher_web","url_launcher_windows"]},{"name":"url_launcher_android","dependencies":[]},{"name":"url_launcher_ios","dependencies":[]},{"name":"url_launcher_linux","dependencies":[]},{"name":"url_launcher_macos","dependencies":[]},{"name":"url_launcher_web","dependencies":[]},{"name":"url_launcher_windows","dependencies":[]}],"date_created":"2025-01-07 16:15:57.147963","version":"3.27.1","swift_package_manager_enabled":false} \ No newline at end of file +{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"flutter_local_notifications","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/flutter_local_notifications-18.0.1/","native_build":true,"dependencies":[]},{"name":"google_sign_in_ios","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/google_sign_in_ios-5.7.8/","shared_darwin_source":true,"native_build":true,"dependencies":[]},{"name":"package_info_plus","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/package_info_plus-8.1.2/","native_build":true,"dependencies":[]},{"name":"path_provider_foundation","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/path_provider_foundation-2.4.1/","shared_darwin_source":true,"native_build":true,"dependencies":[]},{"name":"photo_manager","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/photo_manager-3.6.3/","native_build":true,"dependencies":[]},{"name":"shared_preferences_foundation","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/shared_preferences_foundation-2.5.4/","shared_darwin_source":true,"native_build":true,"dependencies":[]},{"name":"sqflite_darwin","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/sqflite_darwin-2.4.1+1/","shared_darwin_source":true,"native_build":true,"dependencies":[]},{"name":"url_launcher_ios","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/url_launcher_ios-6.3.2/","native_build":true,"dependencies":[]}],"android":[{"name":"flutter_local_notifications","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/flutter_local_notifications-18.0.1/","native_build":true,"dependencies":[]},{"name":"google_sign_in_android","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/google_sign_in_android-6.1.34/","native_build":true,"dependencies":[]},{"name":"package_info_plus","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/package_info_plus-8.1.2/","native_build":true,"dependencies":[]},{"name":"path_provider_android","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/path_provider_android-2.2.15/","native_build":true,"dependencies":[]},{"name":"photo_manager","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/photo_manager-3.6.3/","native_build":true,"dependencies":[]},{"name":"shared_preferences_android","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/shared_preferences_android-2.4.0/","native_build":true,"dependencies":[]},{"name":"sqflite_android","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/sqflite_android-2.4.0/","native_build":true,"dependencies":[]},{"name":"url_launcher_android","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/url_launcher_android-6.3.14/","native_build":true,"dependencies":[]}],"macos":[{"name":"flutter_local_notifications","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/flutter_local_notifications-18.0.1/","native_build":true,"dependencies":[]},{"name":"google_sign_in_ios","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/google_sign_in_ios-5.7.8/","shared_darwin_source":true,"native_build":true,"dependencies":[]},{"name":"package_info_plus","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/package_info_plus-8.1.2/","native_build":true,"dependencies":[]},{"name":"path_provider_foundation","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/path_provider_foundation-2.4.1/","shared_darwin_source":true,"native_build":true,"dependencies":[]},{"name":"photo_manager","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/photo_manager-3.6.3/","native_build":true,"dependencies":[]},{"name":"shared_preferences_foundation","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/shared_preferences_foundation-2.5.4/","shared_darwin_source":true,"native_build":true,"dependencies":[]},{"name":"sqflite_darwin","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/sqflite_darwin-2.4.1+1/","shared_darwin_source":true,"native_build":true,"dependencies":[]},{"name":"url_launcher_macos","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/url_launcher_macos-3.2.2/","native_build":true,"dependencies":[]}],"linux":[{"name":"flutter_local_notifications_linux","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/flutter_local_notifications_linux-5.0.0/","native_build":false,"dependencies":[]},{"name":"package_info_plus","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/package_info_plus-8.1.2/","native_build":false,"dependencies":[]},{"name":"path_provider_linux","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/path_provider_linux-2.2.1/","native_build":false,"dependencies":[]},{"name":"shared_preferences_linux","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/shared_preferences_linux-2.4.1/","native_build":false,"dependencies":["path_provider_linux"]},{"name":"url_launcher_linux","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/url_launcher_linux-3.2.1/","native_build":true,"dependencies":[]}],"windows":[{"name":"package_info_plus","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/package_info_plus-8.1.2/","native_build":false,"dependencies":[]},{"name":"path_provider_windows","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/path_provider_windows-2.3.0/","native_build":false,"dependencies":[]},{"name":"shared_preferences_windows","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/shared_preferences_windows-2.4.1/","native_build":false,"dependencies":["path_provider_windows"]},{"name":"url_launcher_windows","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/url_launcher_windows-3.1.3/","native_build":true,"dependencies":[]}],"web":[{"name":"google_sign_in_web","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/google_sign_in_web-0.12.4+3/","dependencies":[]},{"name":"package_info_plus","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/package_info_plus-8.1.2/","dependencies":[]},{"name":"shared_preferences_web","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/shared_preferences_web-2.4.2/","dependencies":[]},{"name":"url_launcher_web","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/url_launcher_web-2.3.3/","dependencies":[]}]},"dependencyGraph":[{"name":"flutter_local_notifications","dependencies":["flutter_local_notifications_linux"]},{"name":"flutter_local_notifications_linux","dependencies":[]},{"name":"google_sign_in","dependencies":["google_sign_in_android","google_sign_in_ios","google_sign_in_web"]},{"name":"google_sign_in_android","dependencies":[]},{"name":"google_sign_in_ios","dependencies":[]},{"name":"google_sign_in_web","dependencies":[]},{"name":"package_info_plus","dependencies":[]},{"name":"path_provider","dependencies":["path_provider_android","path_provider_foundation","path_provider_linux","path_provider_windows"]},{"name":"path_provider_android","dependencies":[]},{"name":"path_provider_foundation","dependencies":[]},{"name":"path_provider_linux","dependencies":[]},{"name":"path_provider_windows","dependencies":[]},{"name":"photo_manager","dependencies":[]},{"name":"shared_preferences","dependencies":["shared_preferences_android","shared_preferences_foundation","shared_preferences_linux","shared_preferences_web","shared_preferences_windows"]},{"name":"shared_preferences_android","dependencies":[]},{"name":"shared_preferences_foundation","dependencies":[]},{"name":"shared_preferences_linux","dependencies":["path_provider_linux"]},{"name":"shared_preferences_web","dependencies":[]},{"name":"shared_preferences_windows","dependencies":["path_provider_windows"]},{"name":"sqflite","dependencies":["sqflite_android","sqflite_darwin"]},{"name":"sqflite_android","dependencies":[]},{"name":"sqflite_darwin","dependencies":[]},{"name":"url_launcher","dependencies":["url_launcher_android","url_launcher_ios","url_launcher_linux","url_launcher_macos","url_launcher_web","url_launcher_windows"]},{"name":"url_launcher_android","dependencies":[]},{"name":"url_launcher_ios","dependencies":[]},{"name":"url_launcher_linux","dependencies":[]},{"name":"url_launcher_macos","dependencies":[]},{"name":"url_launcher_web","dependencies":[]},{"name":"url_launcher_windows","dependencies":[]}],"date_created":"2025-01-09 09:50:08.848768","version":"3.27.1","swift_package_manager_enabled":false} \ No newline at end of file diff --git a/data/lib/apis/network/client.dart b/data/lib/apis/network/client.dart index 3a282056..ac3b6a78 100644 --- a/data/lib/apis/network/client.dart +++ b/data/lib/apis/network/client.dart @@ -25,9 +25,13 @@ final dropboxAuthenticatedDioProvider = Provider((ref) { rawDio: ref.read(rawDioProvider), dropboxToken: ref.read(AppPreferences.dropboxToken), ); - ref.listen(AppPreferences.dropboxToken, (previous, next) { + final subscription = + ref.listen(AppPreferences.dropboxToken, (previous, next) { dropboxInterceptor.updateToken(next); }); + + ref.onDispose(() => subscription.close()); + return Dio() ..options.connectTimeout = const Duration(seconds: 60) ..options.sendTimeout = const Duration(seconds: 60) diff --git a/data/lib/repositories/media_process_repository.dart b/data/lib/repositories/media_process_repository.dart index b975ab9c..cb63163b 100644 --- a/data/lib/repositories/media_process_repository.dart +++ b/data/lib/repositories/media_process_repository.dart @@ -28,13 +28,16 @@ final mediaProcessRepoProvider = Provider((ref) { ref.read(notificationHandlerProvider), ref.read(AppPreferences.notifications), ); - ref.onDispose(repo.dispose); - ref.listen( + final subscription = ref.listen( AppPreferences.notifications, (previous, next) { repo.updateShowNotification(next); }, ); + ref.onDispose(() { + subscription.close(); + repo.dispose(); + }); return repo; }); From f61793d5c7ec623fcef93666e7cb322bc354d1e4 Mon Sep 17 00:00:00 2001 From: Pratik-canopas Date: Thu, 9 Jan 2025 11:17:40 +0530 Subject: [PATCH 24/24] Update home list issue --- data/lib/models/media/media_extension.dart | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/data/lib/models/media/media_extension.dart b/data/lib/models/media/media_extension.dart index 38a755e8..29e19d02 100644 --- a/data/lib/models/media/media_extension.dart +++ b/data/lib/models/media/media_extension.dart @@ -47,7 +47,9 @@ extension AppMediaExtension on AppMedia { name: name ?? media.name, thumbnailLink: media.thumbnailLink, driveMediaRefId: media.driveMediaRefId, - sources: sources.toList()..add(AppMediaSource.googleDrive), + sources: sources.toList() + ..add(AppMediaSource.googleDrive) + ..toSet().toList(), ); } @@ -55,7 +57,7 @@ extension AppMediaExtension on AppMedia { return copyWith( thumbnailLink: null, driveMediaRefId: null, - sources: sources.toList()..remove(AppMediaSource.googleDrive), + sources: sources.toSet().toList()..remove(AppMediaSource.googleDrive), ); } @@ -73,21 +75,23 @@ extension AppMediaExtension on AppMedia { createdTime: createdTime ?? media.createdTime, name: name ?? media.name, dropboxMediaRefId: media.dropboxMediaRefId, - sources: sources.toList()..add(AppMediaSource.dropbox), + sources: sources.toList() + ..add(AppMediaSource.dropbox) + ..toSet().toList(), ); } AppMedia removeDropboxRef() { return copyWith( dropboxMediaRefId: null, - sources: sources.toList()..remove(AppMediaSource.dropbox), + sources: sources.toSet().toList()..remove(AppMediaSource.dropbox), ); } AppMedia removeLocalRef() { return copyWith( id: driveMediaRefId ?? dropboxMediaRefId ?? '', - sources: sources.toList()..remove(AppMediaSource.local), + sources: sources.toSet().toList()..remove(AppMediaSource.local), ); }