mirror of
https://github.com/immich-app/immich.git
synced 2024-12-29 15:11:58 +00:00
Refactor mobile to use OpenApi generated SDK (#336)
This commit is contained in:
parent
d69470e207
commit
ae7e582ec8
276 changed files with 2473 additions and 3003 deletions
11
README.md
11
README.md
|
@ -161,7 +161,7 @@ To *update* docker-compose with newest image (if you have started the docker-com
|
||||||
docker-compose -f ./docker/docker-compose.yml pull && docker-compose -f ./docker/docker-compose.yml up
|
docker-compose -f ./docker/docker-compose.yml pull && docker-compose -f ./docker/docker-compose.yml up
|
||||||
```
|
```
|
||||||
|
|
||||||
The server will be running at `http://your-ip:2283/api` through `Nginx`
|
The server will be running at `http://your-ip:2283/api`
|
||||||
|
|
||||||
## Step 3: Register User
|
## Step 3: Register User
|
||||||
|
|
||||||
|
@ -225,6 +225,15 @@ make dev # required Makefile installed on the system.
|
||||||
|
|
||||||
All servers and web container are hot reload for quick feedback loop.
|
All servers and web container are hot reload for quick feedback loop.
|
||||||
|
|
||||||
|
## Note for developers
|
||||||
|
### 1 - OpenAPI
|
||||||
|
OpenAPI is used to generate the client (Typescript, Dart) SDK. `openapi-generator-cli` can be installed [here](https://openapi-generator.tech/docs/installation/). When you add a new or modify an existing endpoint, you must run the generate command below to update the client SDK.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run api:generate # Run from server directory
|
||||||
|
```
|
||||||
|
You can find the generated client SDK in the [`web/src/api`](web/src/api) for Typescript SDK and [`mobile/openapi`](mobile/openapi) for Dart SDK.
|
||||||
|
|
||||||
# Support
|
# Support
|
||||||
|
|
||||||
If you like the app, find it helpful, and want to support me to offset the cost of publishing to AppStores, you can sponsor the project with [**Github Sponsor**](https://github.com/sponsors/alextran1502), or a one time donation with the Buy Me a coffee link below.
|
If you like the app, find it helpful, and want to support me to offset the cost of publishing to AppStores, you can sponsor the project with [**Github Sponsor**](https://github.com/sponsors/alextran1502), or a one time donation with the Buy Me a coffee link below.
|
||||||
|
|
|
@ -21,10 +21,18 @@ linter:
|
||||||
# or a specific dart file by using the `// ignore: name_of_lint` and
|
# or a specific dart file by using the `// ignore: name_of_lint` and
|
||||||
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
|
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
|
||||||
# producing the lint.
|
# producing the lint.
|
||||||
|
|
||||||
rules:
|
rules:
|
||||||
# avoid_print: false # Uncomment to disable the `avoid_print` rule
|
# avoid_print: false # Uncomment to disable the `avoid_print` rule
|
||||||
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
|
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
|
||||||
use_build_context_synchronously: false
|
use_build_context_synchronously: false
|
||||||
|
require_trailing_commas: true
|
||||||
|
unrelated_type_equality_checks: true
|
||||||
|
|
||||||
# Additional information about this file can be found at
|
# Additional information about this file can be found at
|
||||||
# https://dart.dev/guides/language/analysis-options
|
# https://dart.dev/guides/language/analysis-options
|
||||||
|
analyzer:
|
||||||
|
exclude:
|
||||||
|
- openapi/
|
||||||
|
- openapi/test/
|
||||||
|
- lib/generated_plugin_registrant.dart
|
||||||
|
|
|
@ -71,6 +71,7 @@
|
||||||
"login_form_label_password": "Password",
|
"login_form_label_password": "Password",
|
||||||
"login_form_password_hint": "password",
|
"login_form_password_hint": "password",
|
||||||
"login_form_save_login": "Stay logged in",
|
"login_form_save_login": "Stay logged in",
|
||||||
|
"login_form_failed_login": "Error logging you in, check server url, email and password",
|
||||||
"monthly_title_text_date_format": "MMMM y",
|
"monthly_title_text_date_format": "MMMM y",
|
||||||
"profile_drawer_client_server_up_to_date": "Client and Server are up-to-date",
|
"profile_drawer_client_server_up_to_date": "Client and Server are up-to-date",
|
||||||
"profile_drawer_sign_out": "Sign Out",
|
"profile_drawer_sign_out": "Sign Out",
|
||||||
|
@ -81,6 +82,7 @@
|
||||||
"search_result_page_new_search_hint": "New Search",
|
"search_result_page_new_search_hint": "New Search",
|
||||||
"select_additional_user_for_sharing_page_suggestions": "Suggestions",
|
"select_additional_user_for_sharing_page_suggestions": "Suggestions",
|
||||||
"select_user_for_sharing_page_err_album": "Failed to create album",
|
"select_user_for_sharing_page_err_album": "Failed to create album",
|
||||||
|
"select_user_for_sharing_page_share_suggestions": "Suggestions",
|
||||||
"share_add": "Add",
|
"share_add": "Add",
|
||||||
"share_add_photos": "Add photos",
|
"share_add_photos": "Add photos",
|
||||||
"share_add_title": "Add a title",
|
"share_add_title": "Add a title",
|
||||||
|
|
|
@ -46,12 +46,15 @@ void main() async {
|
||||||
Locale('de', 'DE')
|
Locale('de', 'DE')
|
||||||
];
|
];
|
||||||
|
|
||||||
runApp(EasyLocalization(
|
runApp(
|
||||||
|
EasyLocalization(
|
||||||
supportedLocales: locales,
|
supportedLocales: locales,
|
||||||
path: 'assets/i18n',
|
path: 'assets/i18n',
|
||||||
useFallbackTranslations: true,
|
useFallbackTranslations: true,
|
||||||
fallbackLocale: locales.first,
|
fallbackLocale: locales.first,
|
||||||
child: const ProviderScope(child: ImmichApp())));
|
child: const ProviderScope(child: ImmichApp()),
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
class ImmichApp extends ConsumerStatefulWidget {
|
class ImmichApp extends ConsumerStatefulWidget {
|
||||||
|
@ -111,6 +114,7 @@ class ImmichAppState extends ConsumerState<ImmichApp>
|
||||||
@override
|
@override
|
||||||
initState() {
|
initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
|
||||||
initApp().then((_) => debugPrint("App Init Completed"));
|
initApp().then((_) => debugPrint("App Init Completed"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -120,10 +124,9 @@ class ImmichAppState extends ConsumerState<ImmichApp>
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
final _immichRouter = AppRouter();
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
var router = ref.watch(appRouterProvider);
|
||||||
ref.watch(releaseInfoProvider.notifier).checkGithubReleaseInfo();
|
ref.watch(releaseInfoProvider.notifier).checkGithubReleaseInfo();
|
||||||
|
|
||||||
return MaterialApp(
|
return MaterialApp(
|
||||||
|
@ -142,7 +145,8 @@ class ImmichAppState extends ConsumerState<ImmichApp>
|
||||||
primarySwatch: Colors.indigo,
|
primarySwatch: Colors.indigo,
|
||||||
fontFamily: 'WorkSans',
|
fontFamily: 'WorkSans',
|
||||||
snackBarTheme: const SnackBarThemeData(
|
snackBarTheme: const SnackBarThemeData(
|
||||||
contentTextStyle: TextStyle(fontFamily: 'WorkSans')),
|
contentTextStyle: TextStyle(fontFamily: 'WorkSans'),
|
||||||
|
),
|
||||||
scaffoldBackgroundColor: immichBackgroundColor,
|
scaffoldBackgroundColor: immichBackgroundColor,
|
||||||
appBarTheme: const AppBarTheme(
|
appBarTheme: const AppBarTheme(
|
||||||
backgroundColor: immichBackgroundColor,
|
backgroundColor: immichBackgroundColor,
|
||||||
|
@ -152,9 +156,10 @@ class ImmichAppState extends ConsumerState<ImmichApp>
|
||||||
systemOverlayStyle: SystemUiOverlayStyle.dark,
|
systemOverlayStyle: SystemUiOverlayStyle.dark,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
routeInformationParser: _immichRouter.defaultRouteParser(),
|
routeInformationParser: router.defaultRouteParser(),
|
||||||
routerDelegate: _immichRouter.delegate(
|
routerDelegate: router.delegate(
|
||||||
navigatorObservers: () => [TabNavigationObserver(ref: ref)]),
|
navigatorObservers: () => [TabNavigationObserver(ref: ref)],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
const ImmichLoadingOverlay(),
|
const ImmichLoadingOverlay(),
|
||||||
const VersionAnnouncementOverlay(),
|
const VersionAnnouncementOverlay(),
|
||||||
|
|
|
@ -3,17 +3,20 @@ import 'package:fluttertoast/fluttertoast.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/modules/asset_viewer/models/image_viewer_page_state.model.dart';
|
import 'package:immich_mobile/modules/asset_viewer/models/image_viewer_page_state.model.dart';
|
||||||
import 'package:immich_mobile/modules/asset_viewer/services/image_viewer.service.dart';
|
import 'package:immich_mobile/modules/asset_viewer/services/image_viewer.service.dart';
|
||||||
import 'package:immich_mobile/shared/models/immich_asset.model.dart';
|
|
||||||
import 'package:immich_mobile/shared/ui/immich_toast.dart';
|
import 'package:immich_mobile/shared/ui/immich_toast.dart';
|
||||||
|
import 'package:openapi/api.dart';
|
||||||
|
|
||||||
class ImageViewerStateNotifier extends StateNotifier<ImageViewerPageState> {
|
class ImageViewerStateNotifier extends StateNotifier<ImageViewerPageState> {
|
||||||
final ImageViewerService _imageViewerService = ImageViewerService();
|
final ImageViewerService _imageViewerService;
|
||||||
|
|
||||||
ImageViewerStateNotifier()
|
ImageViewerStateNotifier(this._imageViewerService)
|
||||||
: super(ImageViewerPageState(
|
: super(
|
||||||
downloadAssetStatus: DownloadAssetStatus.idle));
|
ImageViewerPageState(
|
||||||
|
downloadAssetStatus: DownloadAssetStatus.idle,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
void downloadAsset(ImmichAsset asset, BuildContext context) async {
|
void downloadAsset(AssetResponseDto asset, BuildContext context) async {
|
||||||
state = state.copyWith(downloadAssetStatus: DownloadAssetStatus.loading);
|
state = state.copyWith(downloadAssetStatus: DownloadAssetStatus.loading);
|
||||||
|
|
||||||
bool isSuccess = await _imageViewerService.downloadAssetToDevice(asset);
|
bool isSuccess = await _imageViewerService.downloadAssetToDevice(asset);
|
||||||
|
@ -43,4 +46,5 @@ class ImageViewerStateNotifier extends StateNotifier<ImageViewerPageState> {
|
||||||
|
|
||||||
final imageViewerStateProvider =
|
final imageViewerStateProvider =
|
||||||
StateNotifierProvider<ImageViewerStateNotifier, ImageViewerPageState>(
|
StateNotifierProvider<ImageViewerStateNotifier, ImageViewerPageState>(
|
||||||
((ref) => ImageViewerStateNotifier()));
|
((ref) => ImageViewerStateNotifier(ref.watch(imageViewerServiceProvider))),
|
||||||
|
);
|
||||||
|
|
|
@ -1,33 +1,35 @@
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:hive_flutter/hive_flutter.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/constants/hive_box.dart';
|
import 'package:immich_mobile/shared/services/api.service.dart';
|
||||||
import 'package:immich_mobile/shared/models/immich_asset.model.dart';
|
import 'package:openapi/api.dart';
|
||||||
import 'package:path/path.dart' as p;
|
import 'package:path/path.dart' as p;
|
||||||
import 'package:http/http.dart' as http;
|
|
||||||
|
|
||||||
import 'package:photo_manager/photo_manager.dart';
|
import 'package:photo_manager/photo_manager.dart';
|
||||||
import 'package:path_provider/path_provider.dart';
|
import 'package:path_provider/path_provider.dart';
|
||||||
|
|
||||||
|
final imageViewerServiceProvider =
|
||||||
|
Provider((ref) => ImageViewerService(ref.watch(apiServiceProvider)));
|
||||||
|
|
||||||
class ImageViewerService {
|
class ImageViewerService {
|
||||||
Future<bool> downloadAssetToDevice(ImmichAsset asset) async {
|
final ApiService _apiService;
|
||||||
|
ImageViewerService(this._apiService);
|
||||||
|
|
||||||
|
Future<bool> downloadAssetToDevice(AssetResponseDto asset) async {
|
||||||
try {
|
try {
|
||||||
String fileName = p.basename(asset.originalPath);
|
String fileName = p.basename(asset.originalPath);
|
||||||
var savedEndpoint = Hive.box(userInfoBox).get(serverEndpointKey);
|
|
||||||
Uri filePath = Uri.parse(
|
|
||||||
"$savedEndpoint/asset/download?aid=${asset.deviceAssetId}&did=${asset.deviceId}&isThumb=false");
|
|
||||||
|
|
||||||
var res = await http.get(
|
var res = await _apiService.assetApi.downloadFileWithHttpInfo(
|
||||||
filePath,
|
asset.deviceAssetId,
|
||||||
headers: {
|
asset.deviceId,
|
||||||
"Authorization": "Bearer ${Hive.box(userInfoBox).get(accessTokenKey)}"
|
isThumb: false,
|
||||||
},
|
isWeb: false,
|
||||||
);
|
);
|
||||||
|
|
||||||
final AssetEntity? entity;
|
final AssetEntity? entity;
|
||||||
|
|
||||||
if (asset.type == 'IMAGE') {
|
if (asset.type == AssetTypeEnum.IMAGE) {
|
||||||
entity = await PhotoManager.editor.saveImage(
|
entity = await PhotoManager.editor.saveImage(
|
||||||
res.bodyBytes,
|
res.bodyBytes,
|
||||||
title: p.basename(asset.originalPath),
|
title: p.basename(asset.originalPath),
|
||||||
|
|
|
@ -2,13 +2,12 @@ import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_map/flutter_map.dart';
|
import 'package:flutter_map/flutter_map.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/shared/models/immich_asset_with_exif.model.dart';
|
import 'package:openapi/api.dart';
|
||||||
import 'package:intl/intl.dart';
|
|
||||||
import 'package:path/path.dart' as p;
|
import 'package:path/path.dart' as p;
|
||||||
import 'package:latlong2/latlong.dart';
|
import 'package:latlong2/latlong.dart';
|
||||||
|
|
||||||
class ExifBottomSheet extends ConsumerWidget {
|
class ExifBottomSheet extends ConsumerWidget {
|
||||||
final ImmichAssetWithExif assetDetail;
|
final AssetResponseDto assetDetail;
|
||||||
|
|
||||||
const ExifBottomSheet({Key? key, required this.assetDetail})
|
const ExifBottomSheet({Key? key, required this.assetDetail})
|
||||||
: super(key: key);
|
: super(key: key);
|
||||||
|
@ -26,8 +25,10 @@ class ExifBottomSheet extends ConsumerWidget {
|
||||||
),
|
),
|
||||||
child: FlutterMap(
|
child: FlutterMap(
|
||||||
options: MapOptions(
|
options: MapOptions(
|
||||||
center: LatLng(assetDetail.exifInfo!.latitude!,
|
center: LatLng(
|
||||||
assetDetail.exifInfo!.longitude!),
|
assetDetail.exifInfo?.latitude?.toDouble() ?? 0,
|
||||||
|
assetDetail.exifInfo?.longitude?.toDouble() ?? 0,
|
||||||
|
),
|
||||||
zoom: 16.0,
|
zoom: 16.0,
|
||||||
),
|
),
|
||||||
layers: [
|
layers: [
|
||||||
|
@ -46,10 +47,13 @@ class ExifBottomSheet extends ConsumerWidget {
|
||||||
markers: [
|
markers: [
|
||||||
Marker(
|
Marker(
|
||||||
anchorPos: AnchorPos.align(AnchorAlign.top),
|
anchorPos: AnchorPos.align(AnchorAlign.top),
|
||||||
point: LatLng(assetDetail.exifInfo!.latitude!,
|
point: LatLng(
|
||||||
assetDetail.exifInfo!.longitude!),
|
assetDetail.exifInfo?.latitude?.toDouble() ?? 0,
|
||||||
|
assetDetail.exifInfo?.longitude?.toDouble() ?? 0,
|
||||||
|
),
|
||||||
builder: (ctx) => const Image(
|
builder: (ctx) => const Image(
|
||||||
image: AssetImage('assets/location-pin.png')),
|
image: AssetImage('assets/location-pin.png'),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
@ -63,7 +67,10 @@ class ExifBottomSheet extends ConsumerWidget {
|
||||||
return Text(
|
return Text(
|
||||||
"${assetDetail.exifInfo!.city}, ${assetDetail.exifInfo!.state}",
|
"${assetDetail.exifInfo!.city}, ${assetDetail.exifInfo!.state}",
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 12, color: Colors.grey[200], fontWeight: FontWeight.bold),
|
fontSize: 12,
|
||||||
|
color: Colors.grey[200],
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -74,7 +81,7 @@ class ExifBottomSheet extends ConsumerWidget {
|
||||||
if (assetDetail.exifInfo?.dateTimeOriginal != null)
|
if (assetDetail.exifInfo?.dateTimeOriginal != null)
|
||||||
Text(
|
Text(
|
||||||
DateFormat('date_format'.tr()).format(
|
DateFormat('date_format'.tr()).format(
|
||||||
DateTime.parse(assetDetail.exifInfo!.dateTimeOriginal!),
|
assetDetail.exifInfo!.dateTimeOriginal!,
|
||||||
),
|
),
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: Colors.grey[400],
|
color: Colors.grey[400],
|
||||||
|
@ -151,7 +158,8 @@ class ExifBottomSheet extends ConsumerWidget {
|
||||||
),
|
),
|
||||||
subtitle: assetDetail.exifInfo?.exifImageHeight != null
|
subtitle: assetDetail.exifInfo?.exifImageHeight != null
|
||||||
? Text(
|
? Text(
|
||||||
"${assetDetail.exifInfo?.exifImageHeight} x ${assetDetail.exifInfo?.exifImageWidth} ${assetDetail.exifInfo?.fileSizeInByte!}B ")
|
"${assetDetail.exifInfo?.exifImageHeight} x ${assetDetail.exifInfo?.exifImageWidth} ${assetDetail.exifInfo?.fileSizeInByte!}B ",
|
||||||
|
)
|
||||||
: null,
|
: null,
|
||||||
),
|
),
|
||||||
if (assetDetail.exifInfo?.make != null)
|
if (assetDetail.exifInfo?.make != null)
|
||||||
|
@ -166,7 +174,8 @@ class ExifBottomSheet extends ConsumerWidget {
|
||||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||||
),
|
),
|
||||||
subtitle: Text(
|
subtitle: Text(
|
||||||
"ƒ/${assetDetail.exifInfo?.fNumber} 1/${(1 / (assetDetail.exifInfo?.exposureTime ?? 1)).toStringAsFixed(0)} ${assetDetail.exifInfo?.focalLength}mm ISO${assetDetail.exifInfo?.iso} "),
|
"ƒ/${assetDetail.exifInfo?.fNumber} 1/${(1 / (assetDetail.exifInfo?.exposureTime ?? 1)).toStringAsFixed(0)} ${assetDetail.exifInfo?.focalLength}mm ISO${assetDetail.exifInfo?.iso} ",
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
|
@ -17,16 +17,20 @@ class _RemotePhotoViewState extends State<RemotePhotoView> {
|
||||||
bool allowMoving = _status == _RemoteImageStatus.full;
|
bool allowMoving = _status == _RemoteImageStatus.full;
|
||||||
|
|
||||||
return PhotoView(
|
return PhotoView(
|
||||||
imageProvider: _imageProvider,
|
imageProvider: _imageProvider,
|
||||||
minScale: PhotoViewComputedScale.contained,
|
minScale: PhotoViewComputedScale.contained,
|
||||||
maxScale: allowMoving ? 1.0 : PhotoViewComputedScale.contained,
|
maxScale: allowMoving ? 1.0 : PhotoViewComputedScale.contained,
|
||||||
enablePanAlways: true,
|
enablePanAlways: true,
|
||||||
scaleStateChangedCallback: _scaleStateChanged,
|
scaleStateChangedCallback: _scaleStateChanged,
|
||||||
onScaleEnd: _onScaleListener);
|
onScaleEnd: _onScaleListener,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onScaleListener(BuildContext context, ScaleEndDetails details,
|
void _onScaleListener(
|
||||||
PhotoViewControllerValue controllerValue) {
|
BuildContext context,
|
||||||
|
ScaleEndDetails details,
|
||||||
|
PhotoViewControllerValue controllerValue,
|
||||||
|
) {
|
||||||
// Disable swipe events when zoomed in
|
// Disable swipe events when zoomed in
|
||||||
if (_zoomedIn) return;
|
if (_zoomedIn) return;
|
||||||
|
|
||||||
|
@ -42,12 +46,17 @@ class _RemotePhotoViewState extends State<RemotePhotoView> {
|
||||||
}
|
}
|
||||||
|
|
||||||
CachedNetworkImageProvider _authorizedImageProvider(String url) {
|
CachedNetworkImageProvider _authorizedImageProvider(String url) {
|
||||||
return CachedNetworkImageProvider(url,
|
return CachedNetworkImageProvider(
|
||||||
headers: {"Authorization": widget.authToken}, cacheKey: url);
|
url,
|
||||||
|
headers: {"Authorization": widget.authToken},
|
||||||
|
cacheKey: url,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _performStateTransition(
|
void _performStateTransition(
|
||||||
_RemoteImageStatus newStatus, CachedNetworkImageProvider provider) {
|
_RemoteImageStatus newStatus,
|
||||||
|
CachedNetworkImageProvider provider,
|
||||||
|
) {
|
||||||
// Transition to same status is forbidden
|
// Transition to same status is forbidden
|
||||||
if (_status == newStatus) return;
|
if (_status == newStatus) return;
|
||||||
// Transition full -> thumbnail is forbidden
|
// Transition full -> thumbnail is forbidden
|
||||||
|
@ -67,19 +76,22 @@ class _RemotePhotoViewState extends State<RemotePhotoView> {
|
||||||
_authorizedImageProvider(widget.thumbnailUrl);
|
_authorizedImageProvider(widget.thumbnailUrl);
|
||||||
_imageProvider = thumbnailProvider;
|
_imageProvider = thumbnailProvider;
|
||||||
|
|
||||||
thumbnailProvider
|
thumbnailProvider.resolve(const ImageConfiguration()).addListener(
|
||||||
.resolve(const ImageConfiguration())
|
ImageStreamListener((ImageInfo imageInfo, _) {
|
||||||
.addListener(ImageStreamListener((ImageInfo imageInfo, _) {
|
_performStateTransition(
|
||||||
_performStateTransition(_RemoteImageStatus.thumbnail, thumbnailProvider);
|
_RemoteImageStatus.thumbnail,
|
||||||
}));
|
thumbnailProvider,
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
CachedNetworkImageProvider fullProvider =
|
CachedNetworkImageProvider fullProvider =
|
||||||
_authorizedImageProvider(widget.imageUrl);
|
_authorizedImageProvider(widget.imageUrl);
|
||||||
fullProvider
|
fullProvider.resolve(const ImageConfiguration()).addListener(
|
||||||
.resolve(const ImageConfiguration())
|
ImageStreamListener((ImageInfo imageInfo, _) {
|
||||||
.addListener(ImageStreamListener((ImageInfo imageInfo, _) {
|
_performStateTransition(_RemoteImageStatus.full, fullProvider);
|
||||||
_performStateTransition(_RemoteImageStatus.full, fullProvider);
|
}),
|
||||||
}));
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -90,14 +102,14 @@ class _RemotePhotoViewState extends State<RemotePhotoView> {
|
||||||
}
|
}
|
||||||
|
|
||||||
class RemotePhotoView extends StatefulWidget {
|
class RemotePhotoView extends StatefulWidget {
|
||||||
const RemotePhotoView(
|
const RemotePhotoView({
|
||||||
{Key? key,
|
Key? key,
|
||||||
required this.thumbnailUrl,
|
required this.thumbnailUrl,
|
||||||
required this.imageUrl,
|
required this.imageUrl,
|
||||||
required this.authToken,
|
required this.authToken,
|
||||||
required this.onSwipeDown,
|
required this.onSwipeDown,
|
||||||
required this.onSwipeUp})
|
required this.onSwipeUp,
|
||||||
: super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
final String thumbnailUrl;
|
final String thumbnailUrl;
|
||||||
final String imageUrl;
|
final String imageUrl;
|
||||||
|
|
|
@ -3,17 +3,17 @@ import 'dart:developer';
|
||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/shared/models/immich_asset.model.dart';
|
import 'package:openapi/api.dart';
|
||||||
|
|
||||||
class TopControlAppBar extends ConsumerWidget with PreferredSizeWidget {
|
class TopControlAppBar extends ConsumerWidget with PreferredSizeWidget {
|
||||||
const TopControlAppBar(
|
const TopControlAppBar({
|
||||||
{Key? key,
|
Key? key,
|
||||||
required this.asset,
|
required this.asset,
|
||||||
required this.onMoreInfoPressed,
|
required this.onMoreInfoPressed,
|
||||||
required this.onDownloadPressed})
|
required this.onDownloadPressed,
|
||||||
: super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
final ImmichAsset asset;
|
final AssetResponseDto asset;
|
||||||
final Function onMoreInfoPressed;
|
final Function onMoreInfoPressed;
|
||||||
final Function onDownloadPressed;
|
final Function onDownloadPressed;
|
||||||
|
|
||||||
|
@ -54,12 +54,13 @@ class TopControlAppBar extends ConsumerWidget with PreferredSizeWidget {
|
||||||
: const Icon(Icons.favorite_border_rounded),
|
: const Icon(Icons.favorite_border_rounded),
|
||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
iconSize: iconSize,
|
iconSize: iconSize,
|
||||||
splashRadius: iconSize,
|
splashRadius: iconSize,
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
onMoreInfoPressed();
|
onMoreInfoPressed();
|
||||||
},
|
},
|
||||||
icon: const Icon(Icons.more_horiz_rounded))
|
icon: const Icon(Icons.more_horiz_rounded),
|
||||||
|
)
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,17 +11,16 @@ import 'package:immich_mobile/modules/asset_viewer/ui/exif_bottom_sheet.dart';
|
||||||
import 'package:immich_mobile/modules/asset_viewer/ui/remote_photo_view.dart';
|
import 'package:immich_mobile/modules/asset_viewer/ui/remote_photo_view.dart';
|
||||||
import 'package:immich_mobile/modules/asset_viewer/ui/top_control_app_bar.dart';
|
import 'package:immich_mobile/modules/asset_viewer/ui/top_control_app_bar.dart';
|
||||||
import 'package:immich_mobile/modules/home/services/asset.service.dart';
|
import 'package:immich_mobile/modules/home/services/asset.service.dart';
|
||||||
import 'package:immich_mobile/shared/models/immich_asset.model.dart';
|
import 'package:openapi/api.dart';
|
||||||
import 'package:immich_mobile/shared/models/immich_asset_with_exif.model.dart';
|
|
||||||
|
|
||||||
// ignore: must_be_immutable
|
// ignore: must_be_immutable
|
||||||
class ImageViewerPage extends HookConsumerWidget {
|
class ImageViewerPage extends HookConsumerWidget {
|
||||||
final String imageUrl;
|
final String imageUrl;
|
||||||
final String heroTag;
|
final String heroTag;
|
||||||
final String thumbnailUrl;
|
final String thumbnailUrl;
|
||||||
final ImmichAsset asset;
|
final AssetResponseDto asset;
|
||||||
|
|
||||||
ImmichAssetWithExif? assetDetail;
|
AssetResponseDto? assetDetail;
|
||||||
|
|
||||||
ImageViewerPage({
|
ImageViewerPage({
|
||||||
Key? key,
|
Key? key,
|
||||||
|
@ -54,10 +53,13 @@ class ImageViewerPage extends HookConsumerWidget {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() {
|
useEffect(
|
||||||
getAssetExif();
|
() {
|
||||||
return null;
|
getAssetExif();
|
||||||
}, []);
|
return null;
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: Colors.black,
|
backgroundColor: Colors.black,
|
||||||
|
@ -75,14 +77,15 @@ class ImageViewerPage extends HookConsumerWidget {
|
||||||
children: [
|
children: [
|
||||||
Center(
|
Center(
|
||||||
child: Hero(
|
child: Hero(
|
||||||
tag: heroTag,
|
tag: heroTag,
|
||||||
child: RemotePhotoView(
|
child: RemotePhotoView(
|
||||||
thumbnailUrl: thumbnailUrl,
|
thumbnailUrl: thumbnailUrl,
|
||||||
imageUrl: imageUrl,
|
imageUrl: imageUrl,
|
||||||
authToken: "Bearer ${box.get(accessTokenKey)}",
|
authToken: "Bearer ${box.get(accessTokenKey)}",
|
||||||
onSwipeDown: () => AutoRouter.of(context).pop(),
|
onSwipeDown: () => AutoRouter.of(context).pop(),
|
||||||
onSwipeUp: () => showInfo(),
|
onSwipeUp: () => showInfo(),
|
||||||
)),
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
if (downloadAssetStatus == DownloadAssetStatus.loading)
|
if (downloadAssetStatus == DownloadAssetStatus.loading)
|
||||||
const Center(
|
const Center(
|
||||||
|
|
|
@ -12,15 +12,14 @@ import 'package:immich_mobile/modules/asset_viewer/ui/download_loading_indicator
|
||||||
import 'package:immich_mobile/modules/asset_viewer/ui/exif_bottom_sheet.dart';
|
import 'package:immich_mobile/modules/asset_viewer/ui/exif_bottom_sheet.dart';
|
||||||
import 'package:immich_mobile/modules/asset_viewer/ui/top_control_app_bar.dart';
|
import 'package:immich_mobile/modules/asset_viewer/ui/top_control_app_bar.dart';
|
||||||
import 'package:immich_mobile/modules/home/services/asset.service.dart';
|
import 'package:immich_mobile/modules/home/services/asset.service.dart';
|
||||||
import 'package:immich_mobile/shared/models/immich_asset.model.dart';
|
import 'package:openapi/api.dart';
|
||||||
import 'package:immich_mobile/shared/models/immich_asset_with_exif.model.dart';
|
|
||||||
import 'package:video_player/video_player.dart';
|
import 'package:video_player/video_player.dart';
|
||||||
|
|
||||||
// ignore: must_be_immutable
|
// ignore: must_be_immutable
|
||||||
class VideoViewerPage extends HookConsumerWidget {
|
class VideoViewerPage extends HookConsumerWidget {
|
||||||
final String videoUrl;
|
final String videoUrl;
|
||||||
final ImmichAsset asset;
|
final AssetResponseDto asset;
|
||||||
ImmichAssetWithExif? assetDetail;
|
AssetResponseDto? assetDetail;
|
||||||
|
|
||||||
VideoViewerPage({Key? key, required this.videoUrl, required this.asset})
|
VideoViewerPage({Key? key, required this.videoUrl, required this.asset})
|
||||||
: super(key: key);
|
: super(key: key);
|
||||||
|
@ -49,10 +48,13 @@ class VideoViewerPage extends HookConsumerWidget {
|
||||||
await ref.watch(assetServiceProvider).getAssetById(asset.id);
|
await ref.watch(assetServiceProvider).getAssetById(asset.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() {
|
useEffect(
|
||||||
getAssetExif();
|
() {
|
||||||
return null;
|
getAssetExif();
|
||||||
}, []);
|
return null;
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: Colors.black,
|
backgroundColor: Colors.black,
|
||||||
|
@ -116,8 +118,10 @@ class _VideoThumbnailPlayerState extends State<VideoThumbnailPlayer> {
|
||||||
|
|
||||||
Future<void> initializePlayer() async {
|
Future<void> initializePlayer() async {
|
||||||
try {
|
try {
|
||||||
videoPlayerController = VideoPlayerController.network(widget.url,
|
videoPlayerController = VideoPlayerController.network(
|
||||||
httpHeaders: {"Authorization": "Bearer ${widget.jwtToken}"});
|
widget.url,
|
||||||
|
httpHeaders: {"Authorization": "Bearer ${widget.jwtToken}"},
|
||||||
|
);
|
||||||
|
|
||||||
await videoPlayerController.initialize();
|
await videoPlayerController.initialize();
|
||||||
_createChewieController();
|
_createChewieController();
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import 'package:cancellation_token_http/http.dart';
|
import 'package:cancellation_token_http/http.dart';
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
|
import 'package:openapi/api.dart';
|
||||||
import 'package:photo_manager/photo_manager.dart';
|
import 'package:photo_manager/photo_manager.dart';
|
||||||
|
|
||||||
import 'package:immich_mobile/modules/backup/models/available_album.model.dart';
|
import 'package:immich_mobile/modules/backup/models/available_album.model.dart';
|
||||||
import 'package:immich_mobile/modules/backup/models/current_upload_asset.model.dart';
|
import 'package:immich_mobile/modules/backup/models/current_upload_asset.model.dart';
|
||||||
import 'package:immich_mobile/shared/models/server_info.model.dart';
|
|
||||||
|
|
||||||
enum BackUpProgressEnum { idle, inProgress, done }
|
enum BackUpProgressEnum { idle, inProgress, done }
|
||||||
|
|
||||||
|
@ -14,7 +14,7 @@ class BackUpState {
|
||||||
final List<String> allAssetsInDatabase;
|
final List<String> allAssetsInDatabase;
|
||||||
final double progressInPercentage;
|
final double progressInPercentage;
|
||||||
final CancellationToken cancelToken;
|
final CancellationToken cancelToken;
|
||||||
final ServerInfo serverInfo;
|
final ServerInfoResponseDto serverInfo;
|
||||||
|
|
||||||
/// All available albums on the device
|
/// All available albums on the device
|
||||||
final List<AvailableAlbum> availableAlbums;
|
final List<AvailableAlbum> availableAlbums;
|
||||||
|
@ -49,7 +49,7 @@ class BackUpState {
|
||||||
List<String>? allAssetsInDatabase,
|
List<String>? allAssetsInDatabase,
|
||||||
double? progressInPercentage,
|
double? progressInPercentage,
|
||||||
CancellationToken? cancelToken,
|
CancellationToken? cancelToken,
|
||||||
ServerInfo? serverInfo,
|
ServerInfoResponseDto? serverInfo,
|
||||||
List<AvailableAlbum>? availableAlbums,
|
List<AvailableAlbum>? availableAlbums,
|
||||||
Set<AssetPathEntity>? selectedBackupAlbums,
|
Set<AssetPathEntity>? selectedBackupAlbums,
|
||||||
Set<AssetPathEntity>? excludedBackupAlbums,
|
Set<AssetPathEntity>? excludedBackupAlbums,
|
||||||
|
@ -93,8 +93,10 @@ class BackUpState {
|
||||||
collectionEquals(other.selectedBackupAlbums, selectedBackupAlbums) &&
|
collectionEquals(other.selectedBackupAlbums, selectedBackupAlbums) &&
|
||||||
collectionEquals(other.excludedBackupAlbums, excludedBackupAlbums) &&
|
collectionEquals(other.excludedBackupAlbums, excludedBackupAlbums) &&
|
||||||
collectionEquals(other.allUniqueAssets, allUniqueAssets) &&
|
collectionEquals(other.allUniqueAssets, allUniqueAssets) &&
|
||||||
collectionEquals(other.selectedAlbumsBackupAssetsIds,
|
collectionEquals(
|
||||||
selectedAlbumsBackupAssetsIds) &&
|
other.selectedAlbumsBackupAssetsIds,
|
||||||
|
selectedAlbumsBackupAssetsIds,
|
||||||
|
) &&
|
||||||
other.currentUploadAsset == currentUploadAsset;
|
other.currentUploadAsset == currentUploadAsset;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,48 +0,0 @@
|
||||||
import 'dart:convert';
|
|
||||||
|
|
||||||
class CheckDuplicateAssetResponse {
|
|
||||||
final bool isExist;
|
|
||||||
CheckDuplicateAssetResponse({
|
|
||||||
required this.isExist,
|
|
||||||
});
|
|
||||||
|
|
||||||
CheckDuplicateAssetResponse copyWith({
|
|
||||||
bool? isExist,
|
|
||||||
}) {
|
|
||||||
return CheckDuplicateAssetResponse(
|
|
||||||
isExist: isExist ?? this.isExist,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Map<String, dynamic> toMap() {
|
|
||||||
final result = <String, dynamic>{};
|
|
||||||
|
|
||||||
result.addAll({'isExist': isExist});
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
factory CheckDuplicateAssetResponse.fromMap(Map<String, dynamic> map) {
|
|
||||||
return CheckDuplicateAssetResponse(
|
|
||||||
isExist: map['isExist'] ?? false,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
String toJson() => json.encode(toMap());
|
|
||||||
|
|
||||||
factory CheckDuplicateAssetResponse.fromJson(String source) =>
|
|
||||||
CheckDuplicateAssetResponse.fromMap(json.decode(source));
|
|
||||||
|
|
||||||
@override
|
|
||||||
String toString() => 'CheckDuplicateAssetResponse(isExist: $isExist)';
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool operator ==(Object other) {
|
|
||||||
if (identical(this, other)) return true;
|
|
||||||
|
|
||||||
return other is CheckDuplicateAssetResponse && other.isExist == isExist;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
int get hashCode => isExist.hashCode;
|
|
||||||
}
|
|
|
@ -12,8 +12,8 @@ import 'package:immich_mobile/modules/backup/providers/error_backup_list.provide
|
||||||
import 'package:immich_mobile/modules/backup/services/backup.service.dart';
|
import 'package:immich_mobile/modules/backup/services/backup.service.dart';
|
||||||
import 'package:immich_mobile/modules/login/models/authentication_state.model.dart';
|
import 'package:immich_mobile/modules/login/models/authentication_state.model.dart';
|
||||||
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
|
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
|
||||||
import 'package:immich_mobile/shared/models/server_info.model.dart';
|
|
||||||
import 'package:immich_mobile/shared/services/server_info.service.dart';
|
import 'package:immich_mobile/shared/services/server_info.service.dart';
|
||||||
|
import 'package:openapi/api.dart';
|
||||||
import 'package:photo_manager/photo_manager.dart';
|
import 'package:photo_manager/photo_manager.dart';
|
||||||
|
|
||||||
class BackupNotifier extends StateNotifier<BackUpState> {
|
class BackupNotifier extends StateNotifier<BackUpState> {
|
||||||
|
@ -28,12 +28,12 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
||||||
allAssetsInDatabase: const [],
|
allAssetsInDatabase: const [],
|
||||||
progressInPercentage: 0,
|
progressInPercentage: 0,
|
||||||
cancelToken: CancellationToken(),
|
cancelToken: CancellationToken(),
|
||||||
serverInfo: ServerInfo(
|
serverInfo: ServerInfoResponseDto(
|
||||||
diskAvailable: "0",
|
diskAvailable: "0",
|
||||||
diskAvailableRaw: 0,
|
diskAvailableRaw: 0,
|
||||||
diskSize: "0",
|
diskSize: "0",
|
||||||
diskSizeRaw: 0,
|
diskSizeRaw: 0,
|
||||||
diskUsagePercentage: 0.0,
|
diskUsagePercentage: 0,
|
||||||
diskUse: "0",
|
diskUse: "0",
|
||||||
diskUseRaw: 0,
|
diskUseRaw: 0,
|
||||||
),
|
),
|
||||||
|
@ -113,7 +113,9 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
||||||
// Get all albums on the device
|
// Get all albums on the device
|
||||||
List<AvailableAlbum> availableAlbums = [];
|
List<AvailableAlbum> availableAlbums = [];
|
||||||
List<AssetPathEntity> albums = await PhotoManager.getAssetPathList(
|
List<AssetPathEntity> albums = await PhotoManager.getAssetPathList(
|
||||||
hasAll: true, type: RequestType.common);
|
hasAll: true,
|
||||||
|
type: RequestType.common,
|
||||||
|
);
|
||||||
|
|
||||||
for (AssetPathEntity album in albums) {
|
for (AssetPathEntity album in albums) {
|
||||||
AvailableAlbum availableAlbum = AvailableAlbum(albumEntity: album);
|
AvailableAlbum availableAlbum = AvailableAlbum(albumEntity: album);
|
||||||
|
@ -156,7 +158,10 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
||||||
|
|
||||||
// Get album that contains all assets
|
// Get album that contains all assets
|
||||||
var list = await PhotoManager.getAssetPathList(
|
var list = await PhotoManager.getAssetPathList(
|
||||||
hasAll: true, onlyAll: true, type: RequestType.common);
|
hasAll: true,
|
||||||
|
onlyAll: true,
|
||||||
|
type: RequestType.common,
|
||||||
|
);
|
||||||
AssetPathEntity albumHasAllAssets = list.first;
|
AssetPathEntity albumHasAllAssets = list.first;
|
||||||
|
|
||||||
backupAlbumInfoBox.put(
|
backupAlbumInfoBox.put(
|
||||||
|
@ -175,13 +180,15 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
||||||
for (var selectedAlbumId in backupAlbumInfo!.selectedAlbumIds) {
|
for (var selectedAlbumId in backupAlbumInfo!.selectedAlbumIds) {
|
||||||
var albumAsset = await AssetPathEntity.fromId(selectedAlbumId);
|
var albumAsset = await AssetPathEntity.fromId(selectedAlbumId);
|
||||||
state = state.copyWith(
|
state = state.copyWith(
|
||||||
selectedBackupAlbums: {...state.selectedBackupAlbums, albumAsset});
|
selectedBackupAlbums: {...state.selectedBackupAlbums, albumAsset},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (var excludedAlbumId in backupAlbumInfo.excludedAlbumsIds) {
|
for (var excludedAlbumId in backupAlbumInfo.excludedAlbumsIds) {
|
||||||
var albumAsset = await AssetPathEntity.fromId(excludedAlbumId);
|
var albumAsset = await AssetPathEntity.fromId(excludedAlbumId);
|
||||||
state = state.copyWith(
|
state = state.copyWith(
|
||||||
excludedBackupAlbums: {...state.excludedBackupAlbums, albumAsset});
|
excludedBackupAlbums: {...state.excludedBackupAlbums, albumAsset},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint("[ERROR] Failed to generate album from id $e");
|
debugPrint("[ERROR] Failed to generate album from id $e");
|
||||||
|
@ -211,8 +218,11 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
||||||
|
|
||||||
Set<AssetEntity> allUniqueAssets =
|
Set<AssetEntity> allUniqueAssets =
|
||||||
assetsFromSelectedAlbums.difference(assetsFromExcludedAlbums);
|
assetsFromSelectedAlbums.difference(assetsFromExcludedAlbums);
|
||||||
List<String> allAssetsInDatabase =
|
var allAssetsInDatabase = await _backupService.getDeviceBackupAsset();
|
||||||
await _backupService.getDeviceBackupAsset();
|
|
||||||
|
if (allAssetsInDatabase == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Find asset that were backup from selected albums
|
// Find asset that were backup from selected albums
|
||||||
Set<String> selectedAlbumsBackupAssets =
|
Set<String> selectedAlbumsBackupAssets =
|
||||||
|
@ -328,23 +338,27 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
||||||
void cancelBackup() {
|
void cancelBackup() {
|
||||||
state.cancelToken.cancel();
|
state.cancelToken.cancel();
|
||||||
state = state.copyWith(
|
state = state.copyWith(
|
||||||
backupProgress: BackUpProgressEnum.idle, progressInPercentage: 0.0);
|
backupProgress: BackUpProgressEnum.idle,
|
||||||
|
progressInPercentage: 0.0,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onAssetUploaded(String deviceAssetId, String deviceId) {
|
void _onAssetUploaded(String deviceAssetId, String deviceId) {
|
||||||
state = state.copyWith(selectedAlbumsBackupAssetsIds: {
|
state = state.copyWith(
|
||||||
...state.selectedAlbumsBackupAssetsIds,
|
selectedAlbumsBackupAssetsIds: {
|
||||||
deviceAssetId
|
...state.selectedAlbumsBackupAssetsIds,
|
||||||
}, allAssetsInDatabase: [
|
deviceAssetId
|
||||||
...state.allAssetsInDatabase,
|
},
|
||||||
deviceAssetId
|
allAssetsInDatabase: [...state.allAssetsInDatabase, deviceAssetId],
|
||||||
]);
|
);
|
||||||
|
|
||||||
if (state.allUniqueAssets.length -
|
if (state.allUniqueAssets.length -
|
||||||
state.selectedAlbumsBackupAssetsIds.length ==
|
state.selectedAlbumsBackupAssetsIds.length ==
|
||||||
0) {
|
0) {
|
||||||
state = state.copyWith(
|
state = state.copyWith(
|
||||||
backupProgress: BackUpProgressEnum.done, progressInPercentage: 0.0);
|
backupProgress: BackUpProgressEnum.done,
|
||||||
|
progressInPercentage: 0.0,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
_updateServerInfo();
|
_updateServerInfo();
|
||||||
|
@ -352,24 +366,19 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
||||||
|
|
||||||
void _onUploadProgress(int sent, int total) {
|
void _onUploadProgress(int sent, int total) {
|
||||||
state = state.copyWith(
|
state = state.copyWith(
|
||||||
progressInPercentage: (sent.toDouble() / total.toDouble() * 100));
|
progressInPercentage: (sent.toDouble() / total.toDouble() * 100),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _updateServerInfo() async {
|
Future<void> _updateServerInfo() async {
|
||||||
var serverInfo = await _serverInfoService.getServerInfo();
|
var serverInfo = await _serverInfoService.getServerInfo();
|
||||||
|
|
||||||
// Update server info
|
// Update server info
|
||||||
state = state.copyWith(
|
if (serverInfo != null) {
|
||||||
serverInfo: ServerInfo(
|
state = state.copyWith(
|
||||||
diskSize: serverInfo.diskSize,
|
serverInfo: serverInfo,
|
||||||
diskUse: serverInfo.diskUse,
|
);
|
||||||
diskAvailable: serverInfo.diskAvailable,
|
}
|
||||||
diskSizeRaw: serverInfo.diskSizeRaw,
|
|
||||||
diskUseRaw: serverInfo.diskUseRaw,
|
|
||||||
diskAvailableRaw: serverInfo.diskAvailableRaw,
|
|
||||||
diskUsagePercentage: serverInfo.diskUsagePercentage,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void resumeBackup() {
|
void resumeBackup() {
|
||||||
|
|
|
@ -2,59 +2,38 @@ import 'dart:async';
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:dio/dio.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hive/hive.dart';
|
import 'package:hive/hive.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/constants/hive_box.dart';
|
import 'package:immich_mobile/constants/hive_box.dart';
|
||||||
import 'package:immich_mobile/modules/backup/models/check_duplicate_asset_response.model.dart';
|
|
||||||
import 'package:immich_mobile/modules/backup/models/current_upload_asset.model.dart';
|
import 'package:immich_mobile/modules/backup/models/current_upload_asset.model.dart';
|
||||||
import 'package:immich_mobile/modules/backup/models/error_upload_asset.model.dart';
|
import 'package:immich_mobile/modules/backup/models/error_upload_asset.model.dart';
|
||||||
import 'package:immich_mobile/shared/services/network.service.dart';
|
import 'package:immich_mobile/shared/services/api.service.dart';
|
||||||
import 'package:immich_mobile/shared/models/device_info.model.dart';
|
|
||||||
import 'package:immich_mobile/utils/files_helper.dart';
|
import 'package:immich_mobile/utils/files_helper.dart';
|
||||||
|
import 'package:openapi/api.dart';
|
||||||
import 'package:photo_manager/photo_manager.dart';
|
import 'package:photo_manager/photo_manager.dart';
|
||||||
import 'package:http_parser/http_parser.dart';
|
import 'package:http_parser/http_parser.dart';
|
||||||
import 'package:path/path.dart' as p;
|
import 'package:path/path.dart' as p;
|
||||||
import 'package:cancellation_token_http/http.dart' as http;
|
import 'package:cancellation_token_http/http.dart' as http;
|
||||||
|
|
||||||
final backupServiceProvider =
|
final backupServiceProvider = Provider(
|
||||||
Provider((ref) => BackupService(ref.watch(networkServiceProvider)));
|
(ref) => BackupService(
|
||||||
|
ref.watch(apiServiceProvider),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
class BackupService {
|
class BackupService {
|
||||||
final NetworkService _networkService;
|
final ApiService _apiService;
|
||||||
|
BackupService(this._apiService);
|
||||||
|
|
||||||
BackupService(this._networkService);
|
Future<List<String>?> getDeviceBackupAsset() async {
|
||||||
|
|
||||||
Future<List<String>> getDeviceBackupAsset() async {
|
|
||||||
String deviceId = Hive.box(userInfoBox).get(deviceIdKey);
|
|
||||||
|
|
||||||
Response response =
|
|
||||||
await _networkService.getRequest(url: "asset/$deviceId");
|
|
||||||
List<dynamic> result = jsonDecode(response.toString());
|
|
||||||
|
|
||||||
return result.cast<String>();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<bool> checkDuplicateAsset(String deviceAssetId) async {
|
|
||||||
String deviceId = Hive.box(userInfoBox).get(deviceIdKey);
|
String deviceId = Hive.box(userInfoBox).get(deviceIdKey);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
Response response =
|
return await _apiService.assetApi.getUserAssetsByDeviceId(deviceId);
|
||||||
await _networkService.postRequest(url: "asset/check", data: {
|
|
||||||
"deviceId": deviceId,
|
|
||||||
"deviceAssetId": deviceAssetId,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.statusCode == 200) {
|
|
||||||
var result = CheckDuplicateAssetResponse.fromJson(response.toString());
|
|
||||||
|
|
||||||
return result.isExist;
|
|
||||||
} else {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return false;
|
debugPrint('Error [getDeviceBackupAsset] ${e.toString()}');
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -99,9 +78,11 @@ class BackupService {
|
||||||
var box = Hive.box(userInfoBox);
|
var box = Hive.box(userInfoBox);
|
||||||
|
|
||||||
var req = MultipartRequest(
|
var req = MultipartRequest(
|
||||||
'POST', Uri.parse('$savedEndpoint/asset/upload'),
|
'POST',
|
||||||
onProgress: ((bytes, totalBytes) =>
|
Uri.parse('$savedEndpoint/asset/upload'),
|
||||||
uploadProgressCb(bytes, totalBytes)));
|
onProgress: ((bytes, totalBytes) =>
|
||||||
|
uploadProgressCb(bytes, totalBytes)),
|
||||||
|
);
|
||||||
req.headers["Authorization"] = "Bearer ${box.get(accessTokenKey)}";
|
req.headers["Authorization"] = "Bearer ${box.get(accessTokenKey)}";
|
||||||
|
|
||||||
req.fields['deviceAssetId'] = entity.id;
|
req.fields['deviceAssetId'] = entity.id;
|
||||||
|
@ -133,16 +114,19 @@ class BackupService {
|
||||||
var error = jsonDecode(data);
|
var error = jsonDecode(data);
|
||||||
|
|
||||||
debugPrint(
|
debugPrint(
|
||||||
"Error(${error['statusCode']}) uploading ${entity.id} | $originalFileName | Created on ${entity.createDateTime} | ${error['error']}");
|
"Error(${error['statusCode']}) uploading ${entity.id} | $originalFileName | Created on ${entity.createDateTime} | ${error['error']}",
|
||||||
|
);
|
||||||
|
|
||||||
errorCb(ErrorUploadAsset(
|
errorCb(
|
||||||
asset: entity,
|
ErrorUploadAsset(
|
||||||
id: entity.id,
|
asset: entity,
|
||||||
createdAt: entity.createDateTime,
|
id: entity.id,
|
||||||
fileName: originalFileName,
|
createdAt: entity.createDateTime,
|
||||||
fileType: _getAssetType(entity.type),
|
fileName: originalFileName,
|
||||||
errorMessage: error['error'],
|
fileType: _getAssetType(entity.type),
|
||||||
));
|
errorMessage: error['error'],
|
||||||
|
),
|
||||||
|
);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -160,8 +144,6 @@ class BackupService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void sendBackupRequest(AssetEntity entity) {}
|
|
||||||
|
|
||||||
String _getAssetType(AssetType assetType) {
|
String _getAssetType(AssetType assetType) {
|
||||||
switch (assetType) {
|
switch (assetType) {
|
||||||
case AssetType.audio:
|
case AssetType.audio:
|
||||||
|
@ -175,15 +157,29 @@ class BackupService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<DeviceInfoRemote> setAutoBackup(
|
Future<DeviceInfoResponseDto> setAutoBackup(
|
||||||
bool status, String deviceId, String deviceType) async {
|
bool status,
|
||||||
var res = await _networkService.patchRequest(url: 'device-info', data: {
|
String deviceId,
|
||||||
"isAutoBackup": status,
|
DeviceTypeEnum deviceType,
|
||||||
"deviceId": deviceId,
|
) async {
|
||||||
"deviceType": deviceType,
|
try {
|
||||||
});
|
var updatedDeviceInfo = await _apiService.deviceInfoApi.updateDeviceInfo(
|
||||||
|
UpdateDeviceInfoDto(
|
||||||
|
deviceId: deviceId,
|
||||||
|
deviceType: deviceType,
|
||||||
|
isAutoBackup: status,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
return DeviceInfoRemote.fromJson(res.toString());
|
if (updatedDeviceInfo == null) {
|
||||||
|
throw Exception("Error updating device info");
|
||||||
|
}
|
||||||
|
|
||||||
|
return updatedDeviceInfo;
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint("Error setAutoBackup: ${e.toString()}");
|
||||||
|
throw Error();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -26,7 +26,9 @@ class AlbumInfoCard extends HookConsumerWidget {
|
||||||
ref.watch(backupProvider).excludedBackupAlbums.contains(albumInfo);
|
ref.watch(backupProvider).excludedBackupAlbums.contains(albumInfo);
|
||||||
|
|
||||||
ColorFilter selectedFilter = ColorFilter.mode(
|
ColorFilter selectedFilter = ColorFilter.mode(
|
||||||
Theme.of(context).primaryColor.withAlpha(100), BlendMode.darken);
|
Theme.of(context).primaryColor.withAlpha(100),
|
||||||
|
BlendMode.darken,
|
||||||
|
);
|
||||||
ColorFilter excludedFilter =
|
ColorFilter excludedFilter =
|
||||||
ColorFilter.mode(Colors.red.withAlpha(75), BlendMode.darken);
|
ColorFilter.mode(Colors.red.withAlpha(75), BlendMode.darken);
|
||||||
ColorFilter unselectedFilter =
|
ColorFilter unselectedFilter =
|
||||||
|
@ -40,7 +42,10 @@ class AlbumInfoCard extends HookConsumerWidget {
|
||||||
label: const Text(
|
label: const Text(
|
||||||
"album_info_card_backup_album_included",
|
"album_info_card_backup_album_included",
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 10, color: Colors.white, fontWeight: FontWeight.bold),
|
fontSize: 10,
|
||||||
|
color: Colors.white,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
).tr(),
|
).tr(),
|
||||||
backgroundColor: Theme.of(context).primaryColor,
|
backgroundColor: Theme.of(context).primaryColor,
|
||||||
);
|
);
|
||||||
|
@ -51,7 +56,10 @@ class AlbumInfoCard extends HookConsumerWidget {
|
||||||
label: const Text(
|
label: const Text(
|
||||||
"album_info_card_backup_album_excluded",
|
"album_info_card_backup_album_excluded",
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 10, color: Colors.white, fontWeight: FontWeight.bold),
|
fontSize: 10,
|
||||||
|
color: Colors.white,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
).tr(),
|
).tr(),
|
||||||
backgroundColor: Colors.red[300],
|
backgroundColor: Colors.red[300],
|
||||||
);
|
);
|
||||||
|
@ -138,15 +146,16 @@ class AlbumInfoCard extends HookConsumerWidget {
|
||||||
height: 200,
|
height: 200,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
borderRadius: const BorderRadius.only(
|
borderRadius: const BorderRadius.only(
|
||||||
topLeft: Radius.circular(12),
|
topLeft: Radius.circular(12),
|
||||||
topRight: Radius.circular(12)),
|
topRight: Radius.circular(12),
|
||||||
|
),
|
||||||
image: DecorationImage(
|
image: DecorationImage(
|
||||||
colorFilter: _buildImageFilter(),
|
colorFilter: _buildImageFilter(),
|
||||||
image: imageData != null
|
image: imageData != null
|
||||||
? MemoryImage(imageData!)
|
? MemoryImage(imageData!)
|
||||||
: const AssetImage(
|
: const AssetImage(
|
||||||
'assets/immich-logo-no-outline.png')
|
'assets/immich-logo-no-outline.png',
|
||||||
as ImageProvider,
|
) as ImageProvider,
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -174,9 +183,10 @@ class AlbumInfoCard extends HookConsumerWidget {
|
||||||
Text(
|
Text(
|
||||||
albumInfo.name,
|
albumInfo.name,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
color: Theme.of(context).primaryColor,
|
color: Theme.of(context).primaryColor,
|
||||||
fontWeight: FontWeight.bold),
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(top: 2.0),
|
padding: const EdgeInsets.only(top: 2.0),
|
||||||
|
@ -186,7 +196,9 @@ class AlbumInfoCard extends HookConsumerWidget {
|
||||||
? " (${'backup_all'.tr()})"
|
? " (${'backup_all'.tr()})"
|
||||||
: ""),
|
: ""),
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 12, color: Colors.grey[600]),
|
fontSize: 12,
|
||||||
|
color: Colors.grey[600],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
|
|
|
@ -5,12 +5,12 @@ class BackupInfoCard extends StatelessWidget {
|
||||||
final String title;
|
final String title;
|
||||||
final String subtitle;
|
final String subtitle;
|
||||||
final String info;
|
final String info;
|
||||||
const BackupInfoCard(
|
const BackupInfoCard({
|
||||||
{Key? key,
|
Key? key,
|
||||||
required this.title,
|
required this.title,
|
||||||
required this.subtitle,
|
required this.subtitle,
|
||||||
required this.info})
|
required this.info,
|
||||||
: super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
|
|
@ -20,10 +20,13 @@ class AlbumPreviewPage extends HookConsumerWidget {
|
||||||
await album.getAssetListRange(start: 0, end: album.assetCount);
|
await album.getAssetListRange(start: 0, end: album.assetCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() {
|
useEffect(
|
||||||
_getAssetsInAlbum();
|
() {
|
||||||
return null;
|
_getAssetsInAlbum();
|
||||||
}, []);
|
return null;
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
|
@ -39,9 +42,10 @@ class AlbumPreviewPage extends HookConsumerWidget {
|
||||||
child: Text(
|
child: Text(
|
||||||
"ID ${album.id}",
|
"ID ${album.id}",
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 10,
|
fontSize: 10,
|
||||||
color: Colors.grey[600],
|
color: Colors.grey[600],
|
||||||
fontWeight: FontWeight.bold),
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
@ -59,9 +63,11 @@ class AlbumPreviewPage extends HookConsumerWidget {
|
||||||
),
|
),
|
||||||
itemCount: assets.value.length,
|
itemCount: assets.value.length,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
Future<Uint8List?> thumbData = assets.value[index]
|
Future<Uint8List?> thumbData =
|
||||||
.thumbnailDataWithSize(const ThumbnailSize(200, 200),
|
assets.value[index].thumbnailDataWithSize(
|
||||||
quality: 50);
|
const ThumbnailSize(200, 200),
|
||||||
|
quality: 50,
|
||||||
|
);
|
||||||
|
|
||||||
return FutureBuilder<Uint8List?>(
|
return FutureBuilder<Uint8List?>(
|
||||||
future: thumbData,
|
future: thumbData,
|
||||||
|
|
|
@ -17,10 +17,13 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
|
||||||
final selectedBackupAlbums = ref.watch(backupProvider).selectedBackupAlbums;
|
final selectedBackupAlbums = ref.watch(backupProvider).selectedBackupAlbums;
|
||||||
final excludedBackupAlbums = ref.watch(backupProvider).excludedBackupAlbums;
|
final excludedBackupAlbums = ref.watch(backupProvider).excludedBackupAlbums;
|
||||||
|
|
||||||
useEffect(() {
|
useEffect(
|
||||||
ref.read(backupProvider.notifier).getBackupInfo();
|
() {
|
||||||
return null;
|
ref.read(backupProvider.notifier).getBackupInfo();
|
||||||
}, []);
|
return null;
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
_buildAlbumSelectionList() {
|
_buildAlbumSelectionList() {
|
||||||
if (availableAlbums.isEmpty) {
|
if (availableAlbums.isEmpty) {
|
||||||
|
@ -42,8 +45,9 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
|
||||||
? const EdgeInsets.only(left: 16.00)
|
? const EdgeInsets.only(left: 16.00)
|
||||||
: const EdgeInsets.all(0),
|
: const EdgeInsets.all(0),
|
||||||
child: AlbumInfoCard(
|
child: AlbumInfoCard(
|
||||||
imageData: thumbnailData,
|
imageData: thumbnailData,
|
||||||
albumInfo: availableAlbums[index].albumEntity),
|
albumInfo: availableAlbums[index].albumEntity,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
|
@ -73,13 +77,15 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
|
||||||
child: Chip(
|
child: Chip(
|
||||||
visualDensity: VisualDensity.compact,
|
visualDensity: VisualDensity.compact,
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(5)),
|
borderRadius: BorderRadius.circular(5),
|
||||||
|
),
|
||||||
label: Text(
|
label: Text(
|
||||||
album.name,
|
album.name,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 10,
|
fontSize: 10,
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
fontWeight: FontWeight.bold),
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
backgroundColor: Theme.of(context).primaryColor,
|
backgroundColor: Theme.of(context).primaryColor,
|
||||||
deleteIconColor: Colors.white,
|
deleteIconColor: Colors.white,
|
||||||
|
@ -109,13 +115,15 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
|
||||||
child: Chip(
|
child: Chip(
|
||||||
visualDensity: VisualDensity.compact,
|
visualDensity: VisualDensity.compact,
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(5)),
|
borderRadius: BorderRadius.circular(5),
|
||||||
|
),
|
||||||
label: Text(
|
label: Text(
|
||||||
album.name,
|
album.name,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 10,
|
fontSize: 10,
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
fontWeight: FontWeight.bold),
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
backgroundColor: Colors.red[300],
|
backgroundColor: Colors.red[300],
|
||||||
deleteIconColor: Colors.white,
|
deleteIconColor: Colors.white,
|
||||||
|
@ -185,9 +193,10 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
|
||||||
title: Text(
|
title: Text(
|
||||||
"backup_album_selection_page_total_assets",
|
"backup_album_selection_page_total_assets",
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
color: Colors.grey[700]),
|
color: Colors.grey[700],
|
||||||
|
),
|
||||||
).tr(),
|
).tr(),
|
||||||
trailing: Text(
|
trailing: Text(
|
||||||
ref
|
ref
|
||||||
|
@ -234,7 +243,8 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
|
||||||
builder: (BuildContext context) {
|
builder: (BuildContext context) {
|
||||||
return AlertDialog(
|
return AlertDialog(
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(12)),
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
elevation: 5,
|
elevation: 5,
|
||||||
title: Text(
|
title: Text(
|
||||||
'backup_album_selection_page_selection_info',
|
'backup_album_selection_page_selection_info',
|
||||||
|
@ -250,7 +260,9 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
|
||||||
Text(
|
Text(
|
||||||
'backup_album_selection_page_assets_scatter',
|
'backup_album_selection_page_assets_scatter',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 14, color: Colors.grey[700]),
|
fontSize: 14,
|
||||||
|
color: Colors.grey[700],
|
||||||
|
),
|
||||||
).tr(),
|
).tr(),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
|
@ -11,7 +11,6 @@ import 'package:immich_mobile/modules/backup/providers/backup.provider.dart';
|
||||||
import 'package:immich_mobile/routing/router.dart';
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
import 'package:immich_mobile/shared/providers/websocket.provider.dart';
|
import 'package:immich_mobile/shared/providers/websocket.provider.dart';
|
||||||
import 'package:immich_mobile/modules/backup/ui/backup_info_card.dart';
|
import 'package:immich_mobile/modules/backup/ui/backup_info_card.dart';
|
||||||
import 'package:intl/intl.dart';
|
|
||||||
import 'package:percent_indicator/linear_percent_indicator.dart';
|
import 'package:percent_indicator/linear_percent_indicator.dart';
|
||||||
|
|
||||||
class BackupControllerPage extends HookConsumerWidget {
|
class BackupControllerPage extends HookConsumerWidget {
|
||||||
|
@ -27,16 +26,19 @@ class BackupControllerPage extends HookConsumerWidget {
|
||||||
? false
|
? false
|
||||||
: true;
|
: true;
|
||||||
|
|
||||||
useEffect(() {
|
useEffect(
|
||||||
if (backupState.backupProgress != BackUpProgressEnum.inProgress) {
|
() {
|
||||||
ref.watch(backupProvider.notifier).getBackupInfo();
|
if (backupState.backupProgress != BackUpProgressEnum.inProgress) {
|
||||||
}
|
ref.watch(backupProvider.notifier).getBackupInfo();
|
||||||
|
}
|
||||||
|
|
||||||
ref
|
ref
|
||||||
.watch(websocketProvider.notifier)
|
.watch(websocketProvider.notifier)
|
||||||
.stopListenToEvent('on_upload_success');
|
.stopListenToEvent('on_upload_success');
|
||||||
return null;
|
return null;
|
||||||
}, []);
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
Widget _buildStorageInformation() {
|
Widget _buildStorageInformation() {
|
||||||
return ListTile(
|
return ListTile(
|
||||||
|
@ -68,10 +70,11 @@ class BackupControllerPage extends HookConsumerWidget {
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(top: 12.0),
|
padding: const EdgeInsets.only(top: 12.0),
|
||||||
child: const Text('backup_controller_page_storage_format').tr(
|
child: const Text('backup_controller_page_storage_format').tr(
|
||||||
args: [
|
args: [
|
||||||
backupState.serverInfo.diskUse,
|
backupState.serverInfo.diskUse,
|
||||||
backupState.serverInfo.diskSize
|
backupState.serverInfo.diskSize
|
||||||
]),
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
@ -129,8 +132,10 @@ class BackupControllerPage extends HookConsumerWidget {
|
||||||
.setAutoBackup(true);
|
.setAutoBackup(true);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: Text(backupBtnText,
|
child: Text(
|
||||||
style: const TextStyle(fontWeight: FontWeight.bold)),
|
backupBtnText,
|
||||||
|
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
|
@ -157,9 +162,10 @@ class BackupControllerPage extends HookConsumerWidget {
|
||||||
child: Text(
|
child: Text(
|
||||||
text.trim().substring(0, text.length - 2),
|
text.trim().substring(0, text.length - 2),
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: Theme.of(context).primaryColor,
|
color: Theme.of(context).primaryColor,
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
fontWeight: FontWeight.bold),
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
|
@ -168,9 +174,10 @@ class BackupControllerPage extends HookConsumerWidget {
|
||||||
child: Text(
|
child: Text(
|
||||||
"backup_controller_page_none_selected".tr(),
|
"backup_controller_page_none_selected".tr(),
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: Theme.of(context).primaryColor,
|
color: Theme.of(context).primaryColor,
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
fontWeight: FontWeight.bold),
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -190,9 +197,10 @@ class BackupControllerPage extends HookConsumerWidget {
|
||||||
child: Text(
|
child: Text(
|
||||||
text.trim().substring(0, text.length - 2),
|
text.trim().substring(0, text.length - 2),
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: Colors.red[300],
|
color: Colors.red[300],
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
fontWeight: FontWeight.bold),
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
|
@ -213,9 +221,10 @@ class BackupControllerPage extends HookConsumerWidget {
|
||||||
borderOnForeground: false,
|
borderOnForeground: false,
|
||||||
child: ListTile(
|
child: ListTile(
|
||||||
minVerticalPadding: 15,
|
minVerticalPadding: 15,
|
||||||
title: const Text("backup_controller_page_albums",
|
title: const Text(
|
||||||
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20))
|
"backup_controller_page_albums",
|
||||||
.tr(),
|
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20),
|
||||||
|
).tr(),
|
||||||
subtitle: Padding(
|
subtitle: Padding(
|
||||||
padding: const EdgeInsets.only(top: 8.0),
|
padding: const EdgeInsets.only(top: 8.0),
|
||||||
child: Column(
|
child: Column(
|
||||||
|
@ -284,9 +293,9 @@ class BackupControllerPage extends HookConsumerWidget {
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
fontSize: 11,
|
fontSize: 11,
|
||||||
),
|
),
|
||||||
).tr(args: [
|
).tr(
|
||||||
ref.watch(errorBackupListProvider).length.toString()
|
args: [ref.watch(errorBackupListProvider).length.toString()],
|
||||||
]),
|
),
|
||||||
backgroundColor: Colors.white,
|
backgroundColor: Colors.white,
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
AutoRouter.of(context).push(const FailedBackupStatusRoute());
|
AutoRouter.of(context).push(const FailedBackupStatusRoute());
|
||||||
|
@ -331,12 +340,16 @@ class BackupControllerPage extends HookConsumerWidget {
|
||||||
child: const Text(
|
child: const Text(
|
||||||
'backup_controller_page_filename',
|
'backup_controller_page_filename',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontWeight: FontWeight.bold, fontSize: 10.0),
|
fontWeight: FontWeight.bold,
|
||||||
).tr(args: [
|
fontSize: 10.0,
|
||||||
backupState.currentUploadAsset.fileName,
|
),
|
||||||
backupState.currentUploadAsset.fileType
|
).tr(
|
||||||
.toLowerCase()
|
args: [
|
||||||
]),
|
backupState.currentUploadAsset.fileName,
|
||||||
|
backupState.currentUploadAsset.fileType
|
||||||
|
.toLowerCase()
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
@ -352,16 +365,20 @@ class BackupControllerPage extends HookConsumerWidget {
|
||||||
padding: const EdgeInsets.all(6.0),
|
padding: const EdgeInsets.all(6.0),
|
||||||
child: const Text(
|
child: const Text(
|
||||||
"backup_controller_page_created",
|
"backup_controller_page_created",
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
fontWeight: FontWeight.bold, fontSize: 10.0),
|
fontWeight: FontWeight.bold,
|
||||||
).tr(args: [
|
fontSize: 10.0,
|
||||||
DateFormat.yMMMMd('en_US').format(
|
),
|
||||||
DateTime.parse(
|
).tr(
|
||||||
backupState.currentUploadAsset.createdAt
|
args: [
|
||||||
.toString(),
|
DateFormat.yMMMMd('en_US').format(
|
||||||
),
|
DateTime.parse(
|
||||||
)
|
backupState.currentUploadAsset.createdAt
|
||||||
]),
|
.toString(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
@ -406,14 +423,15 @@ class BackupControllerPage extends HookConsumerWidget {
|
||||||
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
|
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
|
||||||
).tr(),
|
).tr(),
|
||||||
leading: IconButton(
|
leading: IconButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
ref.watch(websocketProvider.notifier).listenUploadEvent();
|
ref.watch(websocketProvider.notifier).listenUploadEvent();
|
||||||
AutoRouter.of(context).pop(true);
|
AutoRouter.of(context).pop(true);
|
||||||
},
|
},
|
||||||
splashRadius: 24,
|
splashRadius: 24,
|
||||||
icon: const Icon(
|
icon: const Icon(
|
||||||
Icons.arrow_back_ios_rounded,
|
Icons.arrow_back_ios_rounded,
|
||||||
)),
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
body: Padding(
|
body: Padding(
|
||||||
padding: const EdgeInsets.only(left: 16.0, right: 16, bottom: 32),
|
padding: const EdgeInsets.only(left: 16.0, right: 16, bottom: 32),
|
||||||
|
|
|
@ -19,13 +19,14 @@ class FailedBackupStatusPage extends HookConsumerWidget {
|
||||||
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
|
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
|
||||||
),
|
),
|
||||||
leading: IconButton(
|
leading: IconButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
AutoRouter.of(context).pop(true);
|
AutoRouter.of(context).pop(true);
|
||||||
},
|
},
|
||||||
splashRadius: 24,
|
splashRadius: 24,
|
||||||
icon: const Icon(
|
icon: const Icon(
|
||||||
Icons.arrow_back_ios_rounded,
|
Icons.arrow_back_ios_rounded,
|
||||||
)),
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
body: ListView.builder(
|
body: ListView.builder(
|
||||||
shrinkWrap: true,
|
shrinkWrap: true,
|
||||||
|
@ -92,9 +93,10 @@ class FailedBackupStatusPage extends HookConsumerWidget {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
color: Colors.grey[700]),
|
color: Colors.grey[700],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
Icon(
|
Icon(
|
||||||
Icons.error,
|
Icons.error,
|
||||||
|
|
|
@ -1,55 +0,0 @@
|
||||||
import 'dart:convert';
|
|
||||||
|
|
||||||
class DeleteAssetResponse {
|
|
||||||
final String id;
|
|
||||||
final String status;
|
|
||||||
|
|
||||||
DeleteAssetResponse({
|
|
||||||
required this.id,
|
|
||||||
required this.status,
|
|
||||||
});
|
|
||||||
|
|
||||||
DeleteAssetResponse copyWith({
|
|
||||||
String? id,
|
|
||||||
String? status,
|
|
||||||
}) {
|
|
||||||
return DeleteAssetResponse(
|
|
||||||
id: id ?? this.id,
|
|
||||||
status: status ?? this.status,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Map<String, dynamic> toMap() {
|
|
||||||
return {
|
|
||||||
'id': id,
|
|
||||||
'status': status,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
factory DeleteAssetResponse.fromMap(Map<String, dynamic> map) {
|
|
||||||
return DeleteAssetResponse(
|
|
||||||
id: map['id'] ?? '',
|
|
||||||
status: map['status'] ?? '',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
String toJson() => json.encode(toMap());
|
|
||||||
|
|
||||||
factory DeleteAssetResponse.fromJson(String source) =>
|
|
||||||
DeleteAssetResponse.fromMap(json.decode(source));
|
|
||||||
|
|
||||||
@override
|
|
||||||
String toString() => 'DeleteAssetResponse(id: $id, status: $status)';
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool operator ==(Object other) {
|
|
||||||
if (identical(this, other)) return true;
|
|
||||||
|
|
||||||
return other is DeleteAssetResponse &&
|
|
||||||
other.id == id &&
|
|
||||||
other.status == status;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
int get hashCode => id.hashCode ^ status.hashCode;
|
|
||||||
}
|
|
|
@ -1,11 +1,9 @@
|
||||||
import 'dart:convert';
|
|
||||||
|
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:immich_mobile/shared/models/immich_asset.model.dart';
|
import 'package:openapi/api.dart';
|
||||||
|
|
||||||
class ImmichAssetGroupByDate {
|
class ImmichAssetGroupByDate {
|
||||||
final String date;
|
final String date;
|
||||||
List<ImmichAsset> assets;
|
List<AssetResponseDto> assets;
|
||||||
ImmichAssetGroupByDate({
|
ImmichAssetGroupByDate({
|
||||||
required this.date,
|
required this.date,
|
||||||
required this.assets,
|
required this.assets,
|
||||||
|
@ -13,7 +11,7 @@ class ImmichAssetGroupByDate {
|
||||||
|
|
||||||
ImmichAssetGroupByDate copyWith({
|
ImmichAssetGroupByDate copyWith({
|
||||||
String? date,
|
String? date,
|
||||||
List<ImmichAsset>? assets,
|
List<AssetResponseDto>? assets,
|
||||||
}) {
|
}) {
|
||||||
return ImmichAssetGroupByDate(
|
return ImmichAssetGroupByDate(
|
||||||
date: date ?? this.date,
|
date: date ?? this.date,
|
||||||
|
@ -21,26 +19,6 @@ class ImmichAssetGroupByDate {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<String, dynamic> toMap() {
|
|
||||||
return {
|
|
||||||
'date': date,
|
|
||||||
'assets': assets.map((x) => x.toMap()).toList(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
factory ImmichAssetGroupByDate.fromMap(Map<String, dynamic> map) {
|
|
||||||
return ImmichAssetGroupByDate(
|
|
||||||
date: map['date'] ?? '',
|
|
||||||
assets: List<ImmichAsset>.from(
|
|
||||||
map['assets']?.map((x) => ImmichAsset.fromMap(x))),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
String toJson() => json.encode(toMap());
|
|
||||||
|
|
||||||
factory ImmichAssetGroupByDate.fromJson(String source) =>
|
|
||||||
ImmichAssetGroupByDate.fromMap(json.decode(source));
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => 'ImmichAssetGroupByDate(date: $date, assets: $assets)';
|
String toString() => 'ImmichAssetGroupByDate(date: $date, assets: $assets)';
|
||||||
|
|
||||||
|
@ -79,28 +57,6 @@ class GetAllAssetResponse {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<String, dynamic> toMap() {
|
|
||||||
return {
|
|
||||||
'count': count,
|
|
||||||
'data': data.map((x) => x.toMap()).toList(),
|
|
||||||
'nextPageKey': nextPageKey,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
factory GetAllAssetResponse.fromMap(Map<String, dynamic> map) {
|
|
||||||
return GetAllAssetResponse(
|
|
||||||
count: map['count']?.toInt() ?? 0,
|
|
||||||
data: List<ImmichAssetGroupByDate>.from(
|
|
||||||
map['data']?.map((x) => ImmichAssetGroupByDate.fromMap(x))),
|
|
||||||
nextPageKey: map['nextPageKey'] ?? '',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
String toJson() => json.encode(toMap());
|
|
||||||
|
|
||||||
factory GetAllAssetResponse.fromJson(String source) =>
|
|
||||||
GetAllAssetResponse.fromMap(json.decode(source));
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() =>
|
String toString() =>
|
||||||
'GetAllAssetResponse(count: $count, data: $data, nextPageKey: $nextPageKey)';
|
'GetAllAssetResponse(count: $count, data: $data, nextPageKey: $nextPageKey)';
|
||||||
|
|
|
@ -1,12 +1,10 @@
|
||||||
import 'dart:convert';
|
|
||||||
|
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
|
|
||||||
import 'package:immich_mobile/shared/models/immich_asset.model.dart';
|
import 'package:openapi/api.dart';
|
||||||
|
|
||||||
class HomePageState {
|
class HomePageState {
|
||||||
final bool isMultiSelectEnable;
|
final bool isMultiSelectEnable;
|
||||||
final Set<ImmichAsset> selectedItems;
|
final Set<AssetResponseDto> selectedItems;
|
||||||
final Set<String> selectedDateGroup;
|
final Set<String> selectedDateGroup;
|
||||||
HomePageState({
|
HomePageState({
|
||||||
required this.isMultiSelectEnable,
|
required this.isMultiSelectEnable,
|
||||||
|
@ -16,7 +14,7 @@ class HomePageState {
|
||||||
|
|
||||||
HomePageState copyWith({
|
HomePageState copyWith({
|
||||||
bool? isMultiSelectEnable,
|
bool? isMultiSelectEnable,
|
||||||
Set<ImmichAsset>? selectedItems,
|
Set<AssetResponseDto>? selectedItems,
|
||||||
Set<String>? selectedDateGroup,
|
Set<String>? selectedDateGroup,
|
||||||
}) {
|
}) {
|
||||||
return HomePageState(
|
return HomePageState(
|
||||||
|
@ -26,28 +24,6 @@ class HomePageState {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<String, dynamic> toMap() {
|
|
||||||
return {
|
|
||||||
'isMultiSelectEnable': isMultiSelectEnable,
|
|
||||||
'selectedItems': selectedItems.map((x) => x.toMap()).toList(),
|
|
||||||
'selectedDateGroup': selectedDateGroup.toList(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
factory HomePageState.fromMap(Map<String, dynamic> map) {
|
|
||||||
return HomePageState(
|
|
||||||
isMultiSelectEnable: map['isMultiSelectEnable'] ?? false,
|
|
||||||
selectedItems: Set<ImmichAsset>.from(
|
|
||||||
map['selectedItems']?.map((x) => ImmichAsset.fromMap(x))),
|
|
||||||
selectedDateGroup: Set<String>.from(map['selectedDateGroup']),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
String toJson() => json.encode(toMap());
|
|
||||||
|
|
||||||
factory HomePageState.fromJson(String source) =>
|
|
||||||
HomePageState.fromMap(json.decode(source));
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() =>
|
String toString() =>
|
||||||
'HomePageState(isMultiSelectEnable: $isMultiSelectEnable, selectedItems: $selectedItems, selectedDateGroup: $selectedDateGroup)';
|
'HomePageState(isMultiSelectEnable: $isMultiSelectEnable, selectedItems: $selectedItems, selectedDateGroup: $selectedDateGroup)';
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/modules/home/models/home_page_state.model.dart';
|
import 'package:immich_mobile/modules/home/models/home_page_state.model.dart';
|
||||||
import 'package:immich_mobile/shared/models/immich_asset.model.dart';
|
import 'package:openapi/api.dart';
|
||||||
|
|
||||||
class HomePageStateNotifier extends StateNotifier<HomePageState> {
|
class HomePageStateNotifier extends StateNotifier<HomePageState> {
|
||||||
HomePageStateNotifier()
|
HomePageStateNotifier()
|
||||||
|
@ -14,7 +14,8 @@ class HomePageStateNotifier extends StateNotifier<HomePageState> {
|
||||||
|
|
||||||
void addSelectedDateGroup(String dateGroupTitle) {
|
void addSelectedDateGroup(String dateGroupTitle) {
|
||||||
state = state.copyWith(
|
state = state.copyWith(
|
||||||
selectedDateGroup: {...state.selectedDateGroup, dateGroupTitle});
|
selectedDateGroup: {...state.selectedDateGroup, dateGroupTitle},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void removeSelectedDateGroup(String dateGroupTitle) {
|
void removeSelectedDateGroup(String dateGroupTitle) {
|
||||||
|
@ -25,36 +26,39 @@ class HomePageStateNotifier extends StateNotifier<HomePageState> {
|
||||||
state = state.copyWith(selectedDateGroup: currentDateGroup);
|
state = state.copyWith(selectedDateGroup: currentDateGroup);
|
||||||
}
|
}
|
||||||
|
|
||||||
void enableMultiSelect(Set<ImmichAsset> selectedItems) {
|
void enableMultiSelect(Set<AssetResponseDto> selectedItems) {
|
||||||
state =
|
state =
|
||||||
state.copyWith(isMultiSelectEnable: true, selectedItems: selectedItems);
|
state.copyWith(isMultiSelectEnable: true, selectedItems: selectedItems);
|
||||||
}
|
}
|
||||||
|
|
||||||
void disableMultiSelect() {
|
void disableMultiSelect() {
|
||||||
state = state.copyWith(
|
state = state.copyWith(
|
||||||
isMultiSelectEnable: false, selectedItems: {}, selectedDateGroup: {});
|
isMultiSelectEnable: false,
|
||||||
|
selectedItems: {},
|
||||||
|
selectedDateGroup: {},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void addSingleSelectedItem(ImmichAsset asset) {
|
void addSingleSelectedItem(AssetResponseDto asset) {
|
||||||
state = state.copyWith(selectedItems: {...state.selectedItems, asset});
|
state = state.copyWith(selectedItems: {...state.selectedItems, asset});
|
||||||
}
|
}
|
||||||
|
|
||||||
void addMultipleSelectedItems(List<ImmichAsset> assets) {
|
void addMultipleSelectedItems(List<AssetResponseDto> assets) {
|
||||||
state = state.copyWith(selectedItems: {...state.selectedItems, ...assets});
|
state = state.copyWith(selectedItems: {...state.selectedItems, ...assets});
|
||||||
}
|
}
|
||||||
|
|
||||||
void removeSingleSelectedItem(ImmichAsset asset) {
|
void removeSingleSelectedItem(AssetResponseDto asset) {
|
||||||
Set<ImmichAsset> currentList = state.selectedItems;
|
Set<AssetResponseDto> currentList = state.selectedItems;
|
||||||
|
|
||||||
currentList.removeWhere((e) => e.id == asset.id);
|
currentList.removeWhere((e) => e.id == asset.id);
|
||||||
|
|
||||||
state = state.copyWith(selectedItems: currentList);
|
state = state.copyWith(selectedItems: currentList);
|
||||||
}
|
}
|
||||||
|
|
||||||
void removeMultipleSelectedItem(List<ImmichAsset> assets) {
|
void removeMultipleSelectedItem(List<AssetResponseDto> assets) {
|
||||||
Set<ImmichAsset> currentList = state.selectedItems;
|
Set<AssetResponseDto> currentList = state.selectedItems;
|
||||||
|
|
||||||
for (ImmichAsset asset in assets) {
|
for (AssetResponseDto asset in assets) {
|
||||||
currentList.removeWhere((e) => e.id == asset.id);
|
currentList.removeWhere((e) => e.id == asset.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -64,4 +68,5 @@ class HomePageStateNotifier extends StateNotifier<HomePageState> {
|
||||||
|
|
||||||
final homePageStateProvider =
|
final homePageStateProvider =
|
||||||
StateNotifierProvider<HomePageStateNotifier, HomePageState>(
|
StateNotifierProvider<HomePageStateNotifier, HomePageState>(
|
||||||
((ref) => HomePageStateNotifier()));
|
((ref) => HomePageStateNotifier()),
|
||||||
|
);
|
||||||
|
|
|
@ -73,10 +73,12 @@ class UploadProfileImageState {
|
||||||
class UploadProfileImageNotifier
|
class UploadProfileImageNotifier
|
||||||
extends StateNotifier<UploadProfileImageState> {
|
extends StateNotifier<UploadProfileImageState> {
|
||||||
UploadProfileImageNotifier(this._userSErvice)
|
UploadProfileImageNotifier(this._userSErvice)
|
||||||
: super(UploadProfileImageState(
|
: super(
|
||||||
profileImagePath: '',
|
UploadProfileImageState(
|
||||||
status: UploadProfileStatus.idle,
|
profileImagePath: '',
|
||||||
));
|
status: UploadProfileStatus.idle,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
final UserService _userSErvice;
|
final UserService _userSErvice;
|
||||||
|
|
||||||
|
@ -88,8 +90,9 @@ class UploadProfileImageNotifier
|
||||||
if (res != null) {
|
if (res != null) {
|
||||||
debugPrint("Succesfully upload profile image");
|
debugPrint("Succesfully upload profile image");
|
||||||
state = state.copyWith(
|
state = state.copyWith(
|
||||||
status: UploadProfileStatus.success,
|
status: UploadProfileStatus.success,
|
||||||
profileImagePath: res.profileImagePath);
|
profileImagePath: res.profileImagePath,
|
||||||
|
);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -100,4 +103,5 @@ class UploadProfileImageNotifier
|
||||||
|
|
||||||
final uploadProfileImageProvider =
|
final uploadProfileImageProvider =
|
||||||
StateNotifierProvider<UploadProfileImageNotifier, UploadProfileImageState>(
|
StateNotifierProvider<UploadProfileImageNotifier, UploadProfileImageState>(
|
||||||
((ref) => UploadProfileImageNotifier(ref.watch(userServiceProvider))));
|
((ref) => UploadProfileImageNotifier(ref.watch(userServiceProvider))),
|
||||||
|
);
|
||||||
|
|
|
@ -1,120 +1,51 @@
|
||||||
import 'dart:convert';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/modules/home/models/delete_asset_response.model.dart';
|
import 'package:immich_mobile/shared/services/api.service.dart';
|
||||||
import 'package:immich_mobile/modules/home/models/get_all_asset_response.model.dart';
|
import 'package:openapi/api.dart';
|
||||||
import 'package:immich_mobile/shared/models/immich_asset.model.dart';
|
|
||||||
import 'package:immich_mobile/shared/models/immich_asset_with_exif.model.dart';
|
|
||||||
import 'package:immich_mobile/shared/services/network.service.dart';
|
|
||||||
|
|
||||||
final assetServiceProvider =
|
final assetServiceProvider = Provider(
|
||||||
Provider((ref) => AssetService(ref.watch(networkServiceProvider)));
|
(ref) => AssetService(
|
||||||
|
ref.watch(apiServiceProvider),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
class AssetService {
|
class AssetService {
|
||||||
final NetworkService _networkService;
|
final ApiService _apiService;
|
||||||
AssetService(this._networkService);
|
|
||||||
|
|
||||||
Future<List<ImmichAsset>?> getAllAsset() async {
|
AssetService(this._apiService);
|
||||||
var res = await _networkService.getRequest(url: "asset/");
|
|
||||||
|
Future<List<AssetResponseDto>?> getAllAsset() async {
|
||||||
try {
|
try {
|
||||||
List<dynamic> decodedData = jsonDecode(res.toString());
|
return await _apiService.assetApi.getAllAssets();
|
||||||
|
|
||||||
List<ImmichAsset> result =
|
|
||||||
List.from(decodedData.map((a) => ImmichAsset.fromMap(a)));
|
|
||||||
return result;
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint("Error getAllAsset ${e.toString()}");
|
debugPrint("Error [getAllAsset] ${e.toString()}");
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<GetAllAssetResponse?> getAllAssetWithPagination() async {
|
|
||||||
var res = await _networkService.getRequest(url: "asset/all");
|
|
||||||
try {
|
|
||||||
Map<String, dynamic> decodedData = jsonDecode(res.toString());
|
|
||||||
|
|
||||||
GetAllAssetResponse result = GetAllAssetResponse.fromMap(decodedData);
|
|
||||||
return result;
|
|
||||||
} catch (e) {
|
|
||||||
debugPrint("Error getAllAsset ${e.toString()}");
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<GetAllAssetResponse?> getOlderAsset(String? nextPageKey) async {
|
|
||||||
try {
|
|
||||||
var res = await _networkService.getRequest(
|
|
||||||
url: "asset/all?nextPageKey=$nextPageKey",
|
|
||||||
);
|
|
||||||
|
|
||||||
Map<String, dynamic> decodedData = jsonDecode(res.toString());
|
|
||||||
|
|
||||||
GetAllAssetResponse result = GetAllAssetResponse.fromMap(decodedData);
|
|
||||||
if (result.count != 0) {
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
debugPrint("Error getAllAsset ${e.toString()}");
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<List<ImmichAsset>> getNewAsset(String latestDate) async {
|
|
||||||
try {
|
|
||||||
var res = await _networkService.getRequest(
|
|
||||||
url: "asset/new?latestDate=$latestDate",
|
|
||||||
);
|
|
||||||
|
|
||||||
List<dynamic> decodedData = jsonDecode(res.toString());
|
|
||||||
|
|
||||||
List<ImmichAsset> result =
|
|
||||||
List.from(decodedData.map((a) => ImmichAsset.fromMap(a)));
|
|
||||||
if (result.isNotEmpty) {
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
return [];
|
|
||||||
} catch (e) {
|
|
||||||
debugPrint("Error getAllAsset ${e.toString()}");
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<ImmichAssetWithExif?> getAssetById(String assetId) async {
|
|
||||||
try {
|
|
||||||
var res = await _networkService.getRequest(
|
|
||||||
url: "asset/assetById/$assetId",
|
|
||||||
);
|
|
||||||
|
|
||||||
Map<String, dynamic> decodedData = jsonDecode(res.toString());
|
|
||||||
|
|
||||||
ImmichAssetWithExif result = ImmichAssetWithExif.fromMap(decodedData);
|
|
||||||
return result;
|
|
||||||
} catch (e) {
|
|
||||||
debugPrint("Error getAllAsset ${e.toString()}");
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<DeleteAssetResponse>?> deleteAssets(
|
Future<AssetResponseDto?> getAssetById(String assetId) async {
|
||||||
Set<ImmichAsset> deleteAssets) async {
|
|
||||||
try {
|
try {
|
||||||
var payload = [];
|
return await _apiService.assetApi.getAssetById(assetId);
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint("Error [getAssetById] ${e.toString()}");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<DeleteAssetResponseDto>?> deleteAssets(
|
||||||
|
Set<AssetResponseDto> deleteAssets,
|
||||||
|
) async {
|
||||||
|
try {
|
||||||
|
List<String> payload = [];
|
||||||
|
|
||||||
for (var asset in deleteAssets) {
|
for (var asset in deleteAssets) {
|
||||||
payload.add(asset.id);
|
payload.add(asset.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
var res = await _networkService
|
return await _apiService.assetApi
|
||||||
.deleteRequest(url: "asset/", data: {"ids": payload});
|
.deleteAsset(DeleteAssetDto(ids: payload));
|
||||||
|
|
||||||
List<dynamic> decodedData = jsonDecode(res.toString());
|
|
||||||
|
|
||||||
List<DeleteAssetResponse> result =
|
|
||||||
List.from(decodedData.map((a) => DeleteAssetResponse.fromMap(a)));
|
|
||||||
|
|
||||||
return result;
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint("Error getAllAsset ${e.toString()}");
|
debugPrint("Error getAllAsset ${e.toString()}");
|
||||||
return null;
|
return null;
|
||||||
|
|
|
@ -15,7 +15,9 @@ class ControlBottomAppBar extends StatelessWidget {
|
||||||
height: MediaQuery.of(context).size.height * 0.15,
|
height: MediaQuery.of(context).size.height * 0.15,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
borderRadius: const BorderRadius.only(
|
borderRadius: const BorderRadius.only(
|
||||||
topLeft: Radius.circular(15), topRight: Radius.circular(15)),
|
topLeft: Radius.circular(15),
|
||||||
|
topRight: Radius.circular(15),
|
||||||
|
),
|
||||||
color: Colors.grey[300]?.withOpacity(0.98),
|
color: Colors.grey[300]?.withOpacity(0.98),
|
||||||
),
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
|
@ -48,12 +50,12 @@ class ControlBottomAppBar extends StatelessWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
class ControlBoxButton extends StatelessWidget {
|
class ControlBoxButton extends StatelessWidget {
|
||||||
const ControlBoxButton(
|
const ControlBoxButton({
|
||||||
{Key? key,
|
Key? key,
|
||||||
required this.label,
|
required this.label,
|
||||||
required this.iconData,
|
required this.iconData,
|
||||||
required this.onPressed})
|
required this.onPressed,
|
||||||
: super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
final String label;
|
final String label;
|
||||||
final IconData iconData;
|
final IconData iconData;
|
||||||
|
|
|
@ -2,8 +2,7 @@ import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/modules/home/providers/home_page_state.provider.dart';
|
import 'package:immich_mobile/modules/home/providers/home_page_state.provider.dart';
|
||||||
import 'package:immich_mobile/shared/models/immich_asset.model.dart';
|
import 'package:openapi/api.dart';
|
||||||
import 'package:intl/intl.dart';
|
|
||||||
|
|
||||||
class DailyTitleText extends ConsumerWidget {
|
class DailyTitleText extends ConsumerWidget {
|
||||||
const DailyTitleText({
|
const DailyTitleText({
|
||||||
|
@ -13,14 +12,15 @@ class DailyTitleText extends ConsumerWidget {
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
final String isoDate;
|
final String isoDate;
|
||||||
final List<ImmichAsset> assetGroup;
|
final List<AssetResponseDto> assetGroup;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
var currentYear = DateTime.now().year;
|
var currentYear = DateTime.now().year;
|
||||||
var groupYear = DateTime.parse(isoDate).year;
|
var groupYear = DateTime.parse(isoDate).year;
|
||||||
var formatDateTemplate =
|
var formatDateTemplate = currentYear == groupYear
|
||||||
currentYear == groupYear ? "daily_title_text_date".tr() : "daily_title_text_date_year".tr();
|
? "daily_title_text_date".tr()
|
||||||
|
: "daily_title_text_date_year".tr();
|
||||||
var dateText =
|
var dateText =
|
||||||
DateFormat(formatDateTemplate).format(DateTime.parse(isoDate));
|
DateFormat(formatDateTemplate).format(DateTime.parse(isoDate));
|
||||||
var isMultiSelectEnable =
|
var isMultiSelectEnable =
|
||||||
|
@ -74,7 +74,11 @@ class DailyTitleText extends ConsumerWidget {
|
||||||
return SliverToBoxAdapter(
|
return SliverToBoxAdapter(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.only(
|
padding: const EdgeInsets.only(
|
||||||
top: 29.0, bottom: 29.0, left: 12.0, right: 12.0),
|
top: 29.0,
|
||||||
|
bottom: 29.0,
|
||||||
|
left: 12.0,
|
||||||
|
right: 12.0,
|
||||||
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
|
|
|
@ -29,15 +29,18 @@ class DisableMultiSelectButton extends ConsumerWidget {
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 4.0),
|
padding: const EdgeInsets.symmetric(horizontal: 4.0),
|
||||||
child: TextButton.icon(
|
child: TextButton.icon(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
onPressed();
|
onPressed();
|
||||||
},
|
},
|
||||||
icon: const Icon(Icons.close_rounded),
|
icon: const Icon(Icons.close_rounded),
|
||||||
label: Text(
|
label: Text(
|
||||||
'$selectedItemCount',
|
'$selectedItemCount',
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontWeight: FontWeight.w600, fontSize: 18),
|
fontWeight: FontWeight.w600,
|
||||||
)),
|
fontSize: 18,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
@ -118,20 +118,24 @@ class DraggableScrollbar extends StatefulWidget {
|
||||||
this.labelConstraints,
|
this.labelConstraints,
|
||||||
}) : assert(child.scrollDirection == Axis.vertical),
|
}) : assert(child.scrollDirection == Axis.vertical),
|
||||||
scrollThumbBuilder = _thumbSemicircleBuilder(
|
scrollThumbBuilder = _thumbSemicircleBuilder(
|
||||||
heightScrollThumb * 0.6, scrollThumbKey, alwaysVisibleScrollThumb),
|
heightScrollThumb * 0.6,
|
||||||
|
scrollThumbKey,
|
||||||
|
alwaysVisibleScrollThumb,
|
||||||
|
),
|
||||||
super(key: key);
|
super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
DraggableScrollbarState createState() => DraggableScrollbarState();
|
DraggableScrollbarState createState() => DraggableScrollbarState();
|
||||||
|
|
||||||
static buildScrollThumbAndLabel(
|
static buildScrollThumbAndLabel({
|
||||||
{required Widget scrollThumb,
|
required Widget scrollThumb,
|
||||||
required Color backgroundColor,
|
required Color backgroundColor,
|
||||||
required Animation<double>? thumbAnimation,
|
required Animation<double>? thumbAnimation,
|
||||||
required Animation<double>? labelAnimation,
|
required Animation<double>? labelAnimation,
|
||||||
required Text? labelText,
|
required Text? labelText,
|
||||||
required BoxConstraints? labelConstraints,
|
required BoxConstraints? labelConstraints,
|
||||||
required bool alwaysVisibleScrollThumb}) {
|
required bool alwaysVisibleScrollThumb,
|
||||||
|
}) {
|
||||||
var scrollThumbAndLabel = labelText == null
|
var scrollThumbAndLabel = labelText == null
|
||||||
? scrollThumb
|
? scrollThumb
|
||||||
: Row(
|
: Row(
|
||||||
|
@ -158,7 +162,10 @@ class DraggableScrollbar extends StatefulWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
static ScrollThumbBuilder _thumbSemicircleBuilder(
|
static ScrollThumbBuilder _thumbSemicircleBuilder(
|
||||||
double width, Key? scrollThumbKey, bool alwaysVisibleScrollThumb) {
|
double width,
|
||||||
|
Key? scrollThumbKey,
|
||||||
|
bool alwaysVisibleScrollThumb,
|
||||||
|
) {
|
||||||
return (
|
return (
|
||||||
Color backgroundColor,
|
Color backgroundColor,
|
||||||
Animation<double> thumbAnimation,
|
Animation<double> thumbAnimation,
|
||||||
|
@ -198,7 +205,9 @@ class DraggableScrollbar extends StatefulWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
static ScrollThumbBuilder _thumbArrowBuilder(
|
static ScrollThumbBuilder _thumbArrowBuilder(
|
||||||
Key? scrollThumbKey, bool alwaysVisibleScrollThumb) {
|
Key? scrollThumbKey,
|
||||||
|
bool alwaysVisibleScrollThumb,
|
||||||
|
) {
|
||||||
return (
|
return (
|
||||||
Color backgroundColor,
|
Color backgroundColor,
|
||||||
Animation<double> thumbAnimation,
|
Animation<double> thumbAnimation,
|
||||||
|
@ -234,7 +243,9 @@ class DraggableScrollbar extends StatefulWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
static ScrollThumbBuilder _thumbRRectBuilder(
|
static ScrollThumbBuilder _thumbRRectBuilder(
|
||||||
Key? scrollThumbKey, bool alwaysVisibleScrollThumb) {
|
Key? scrollThumbKey,
|
||||||
|
bool alwaysVisibleScrollThumb,
|
||||||
|
) {
|
||||||
return (
|
return (
|
||||||
Color backgroundColor,
|
Color backgroundColor,
|
||||||
Animation<double> thumbAnimation,
|
Animation<double> thumbAnimation,
|
||||||
|
@ -372,42 +383,44 @@ class DraggableScrollbarState extends State<DraggableScrollbar>
|
||||||
}
|
}
|
||||||
|
|
||||||
return LayoutBuilder(
|
return LayoutBuilder(
|
||||||
builder: (BuildContext context, BoxConstraints constraints) {
|
builder: (BuildContext context, BoxConstraints constraints) {
|
||||||
//print("LayoutBuilder constraints=$constraints");
|
//print("LayoutBuilder constraints=$constraints");
|
||||||
|
|
||||||
return NotificationListener<ScrollNotification>(
|
return NotificationListener<ScrollNotification>(
|
||||||
onNotification: (ScrollNotification notification) {
|
onNotification: (ScrollNotification notification) {
|
||||||
changePosition(notification);
|
changePosition(notification);
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
child: Stack(
|
child: Stack(
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
RepaintBoundary(
|
RepaintBoundary(
|
||||||
child: widget.child,
|
child: widget.child,
|
||||||
),
|
),
|
||||||
RepaintBoundary(
|
RepaintBoundary(
|
||||||
child: GestureDetector(
|
child: GestureDetector(
|
||||||
onVerticalDragStart: _onVerticalDragStart,
|
onVerticalDragStart: _onVerticalDragStart,
|
||||||
onVerticalDragUpdate: _onVerticalDragUpdate,
|
onVerticalDragUpdate: _onVerticalDragUpdate,
|
||||||
onVerticalDragEnd: _onVerticalDragEnd,
|
onVerticalDragEnd: _onVerticalDragEnd,
|
||||||
child: Container(
|
child: Container(
|
||||||
alignment: Alignment.topRight,
|
alignment: Alignment.topRight,
|
||||||
margin: EdgeInsets.only(top: _barOffset),
|
margin: EdgeInsets.only(top: _barOffset),
|
||||||
padding: widget.padding,
|
padding: widget.padding,
|
||||||
child: widget.scrollThumbBuilder(
|
child: widget.scrollThumbBuilder(
|
||||||
widget.backgroundColor,
|
widget.backgroundColor,
|
||||||
_thumbAnimation,
|
_thumbAnimation,
|
||||||
_labelAnimation,
|
_labelAnimation,
|
||||||
widget.heightScrollThumb,
|
widget.heightScrollThumb,
|
||||||
labelText: labelText,
|
labelText: labelText,
|
||||||
labelConstraints: widget.labelConstraints,
|
labelConstraints: widget.labelConstraints,
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)),
|
],
|
||||||
],
|
),
|
||||||
),
|
);
|
||||||
);
|
},
|
||||||
});
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
//scroll bar has received notification that it's view was scrolled
|
//scroll bar has received notification that it's view was scrolled
|
||||||
|
@ -498,7 +511,10 @@ class DraggableScrollbarState extends State<DraggableScrollbar>
|
||||||
}
|
}
|
||||||
|
|
||||||
double viewDelta = getScrollViewDelta(
|
double viewDelta = getScrollViewDelta(
|
||||||
details.delta.dy, barMaxScrollExtent, viewMaxScrollExtent);
|
details.delta.dy,
|
||||||
|
barMaxScrollExtent,
|
||||||
|
viewMaxScrollExtent,
|
||||||
|
);
|
||||||
|
|
||||||
_viewOffset = widget.controller.position.pixels + viewDelta;
|
_viewOffset = widget.controller.position.pixels + viewDelta;
|
||||||
if (_viewOffset < widget.controller.position.minScrollExtent) {
|
if (_viewOffset < widget.controller.position.minScrollExtent) {
|
||||||
|
@ -579,7 +595,9 @@ class ArrowClipper extends CustomClipper<Path> {
|
||||||
path.lineTo(startPointX + arrowWidth, startPointY);
|
path.lineTo(startPointX + arrowWidth, startPointY);
|
||||||
path.lineTo(startPointX + arrowWidth, startPointY + 1.0);
|
path.lineTo(startPointX + arrowWidth, startPointY + 1.0);
|
||||||
path.lineTo(
|
path.lineTo(
|
||||||
startPointX + arrowWidth / 2, startPointY - arrowWidth / 2 + 1.0);
|
startPointX + arrowWidth / 2,
|
||||||
|
startPointY - arrowWidth / 2 + 1.0,
|
||||||
|
);
|
||||||
path.lineTo(startPointX, startPointY + 1.0);
|
path.lineTo(startPointX, startPointY + 1.0);
|
||||||
path.close();
|
path.close();
|
||||||
|
|
||||||
|
@ -589,7 +607,9 @@ class ArrowClipper extends CustomClipper<Path> {
|
||||||
path.lineTo(startPointX, startPointY);
|
path.lineTo(startPointX, startPointY);
|
||||||
path.lineTo(startPointX, startPointY - 1.0);
|
path.lineTo(startPointX, startPointY - 1.0);
|
||||||
path.lineTo(
|
path.lineTo(
|
||||||
startPointX + arrowWidth / 2, startPointY + arrowWidth / 2 - 1.0);
|
startPointX + arrowWidth / 2,
|
||||||
|
startPointY + arrowWidth / 2 - 1.0,
|
||||||
|
);
|
||||||
path.lineTo(startPointX + arrowWidth, startPointY - 1.0);
|
path.lineTo(startPointX + arrowWidth, startPointY - 1.0);
|
||||||
path.close();
|
path.close();
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/modules/home/ui/thumbnail_image.dart';
|
import 'package:immich_mobile/modules/home/ui/thumbnail_image.dart';
|
||||||
import 'package:immich_mobile/shared/models/immich_asset.model.dart';
|
import 'package:openapi/api.dart';
|
||||||
|
|
||||||
class ImageGrid extends ConsumerWidget {
|
class ImageGrid extends ConsumerWidget {
|
||||||
final List<ImmichAsset> assetGroup;
|
final List<AssetResponseDto> assetGroup;
|
||||||
|
|
||||||
const ImageGrid({Key? key, required this.assetGroup}) : super(key: key);
|
const ImageGrid({Key? key, required this.assetGroup}) : super(key: key);
|
||||||
|
|
||||||
|
@ -25,7 +25,7 @@ class ImageGrid extends ConsumerWidget {
|
||||||
child: Stack(
|
child: Stack(
|
||||||
children: [
|
children: [
|
||||||
ThumbnailImage(asset: assetGroup[index]),
|
ThumbnailImage(asset: assetGroup[index]),
|
||||||
if (assetType != 'IMAGE')
|
if (assetType != AssetTypeEnum.IMAGE)
|
||||||
Positioned(
|
Positioned(
|
||||||
top: 5,
|
top: 5,
|
||||||
right: 5,
|
right: 5,
|
||||||
|
|
|
@ -31,7 +31,8 @@ class ImmichSliverAppBar extends ConsumerWidget {
|
||||||
pinned: false,
|
pinned: false,
|
||||||
snap: false,
|
snap: false,
|
||||||
shape: const RoundedRectangleBorder(
|
shape: const RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.all(Radius.circular(5))),
|
borderRadius: BorderRadius.all(Radius.circular(5)),
|
||||||
|
),
|
||||||
leading: Builder(
|
leading: Builder(
|
||||||
builder: (BuildContext context) {
|
builder: (BuildContext context) {
|
||||||
return Stack(
|
return Stack(
|
||||||
|
@ -99,7 +100,8 @@ class ImmichSliverAppBar extends ConsumerWidget {
|
||||||
child: CircularProgressIndicator(
|
child: CircularProgressIndicator(
|
||||||
strokeWidth: 1,
|
strokeWidth: 1,
|
||||||
valueColor: AlwaysStoppedAnimation<Color>(
|
valueColor: AlwaysStoppedAnimation<Color>(
|
||||||
Theme.of(context).primaryColor),
|
Theme.of(context).primaryColor,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -117,7 +119,8 @@ class ImmichSliverAppBar extends ConsumerWidget {
|
||||||
Icons.cloud_off_rounded,
|
Icons.cloud_off_rounded,
|
||||||
size: 8,
|
size: 8,
|
||||||
),
|
),
|
||||||
child: const Icon(Icons.backup_rounded)),
|
child: const Icon(Icons.backup_rounded),
|
||||||
|
),
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
var onPop = await AutoRouter.of(context)
|
var onPop = await AutoRouter.of(context)
|
||||||
.push(const BackupControllerRoute());
|
.push(const BackupControllerRoute());
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:intl/intl.dart';
|
|
||||||
|
|
||||||
class MonthlyTitleText extends StatelessWidget {
|
class MonthlyTitleText extends StatelessWidget {
|
||||||
const MonthlyTitleText({
|
const MonthlyTitleText({
|
||||||
|
@ -12,7 +11,8 @@ class MonthlyTitleText extends StatelessWidget {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
var monthTitleText = DateFormat("monthly_title_text_date_format".tr()).format(DateTime.parse(isoDate));
|
var monthTitleText = DateFormat("monthly_title_text_date_format".tr())
|
||||||
|
.format(DateTime.parse(isoDate));
|
||||||
|
|
||||||
return SliverToBoxAdapter(
|
return SliverToBoxAdapter(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
|
|
|
@ -55,7 +55,8 @@ class ProfileDrawer extends HookConsumerWidget {
|
||||||
return CircleAvatar(
|
return CircleAvatar(
|
||||||
radius: 35,
|
radius: 35,
|
||||||
backgroundImage: NetworkImage(
|
backgroundImage: NetworkImage(
|
||||||
'$endpoint/user/profile-image/${authState.userId}?d=${dummmy++}'),
|
'$endpoint/user/profile-image/${authState.userId}?d=${dummmy++}',
|
||||||
|
),
|
||||||
backgroundColor: Colors.transparent,
|
backgroundColor: Colors.transparent,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
|
@ -71,7 +72,8 @@ class ProfileDrawer extends HookConsumerWidget {
|
||||||
return CircleAvatar(
|
return CircleAvatar(
|
||||||
radius: 35,
|
radius: 35,
|
||||||
backgroundImage: NetworkImage(
|
backgroundImage: NetworkImage(
|
||||||
'$endpoint/user/profile-image/${authState.userId}?d=${dummmy++}'),
|
'$endpoint/user/profile-image/${authState.userId}?d=${dummmy++}',
|
||||||
|
),
|
||||||
backgroundColor: Colors.transparent,
|
backgroundColor: Colors.transparent,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -93,7 +95,10 @@ class ProfileDrawer extends HookConsumerWidget {
|
||||||
|
|
||||||
_pickUserProfileImage() async {
|
_pickUserProfileImage() async {
|
||||||
final XFile? image = await ImagePicker().pickImage(
|
final XFile? image = await ImagePicker().pickImage(
|
||||||
source: ImageSource.gallery, maxHeight: 1024, maxWidth: 1024);
|
source: ImageSource.gallery,
|
||||||
|
maxHeight: 1024,
|
||||||
|
maxWidth: 1024,
|
||||||
|
);
|
||||||
|
|
||||||
if (image != null) {
|
if (image != null) {
|
||||||
var success =
|
var success =
|
||||||
|
@ -101,16 +106,20 @@ class ProfileDrawer extends HookConsumerWidget {
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
ref.watch(authenticationProvider.notifier).updateUserProfileImagePath(
|
ref.watch(authenticationProvider.notifier).updateUserProfileImagePath(
|
||||||
ref.read(uploadProfileImageProvider).profileImagePath);
|
ref.read(uploadProfileImageProvider).profileImagePath,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() {
|
useEffect(
|
||||||
_getPackageInfo();
|
() {
|
||||||
_buildUserProfileImage();
|
_getPackageInfo();
|
||||||
return null;
|
_buildUserProfileImage();
|
||||||
}, []);
|
return null;
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
return Drawer(
|
return Drawer(
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
@ -186,9 +195,10 @@ class ProfileDrawer extends HookConsumerWidget {
|
||||||
title: const Text(
|
title: const Text(
|
||||||
"profile_drawer_sign_out",
|
"profile_drawer_sign_out",
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: Colors.black54,
|
color: Colors.black54,
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
fontWeight: FontWeight.bold),
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
).tr(),
|
).tr(),
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
bool res =
|
bool res =
|
||||||
|
@ -231,9 +241,10 @@ class ProfileDrawer extends HookConsumerWidget {
|
||||||
: "profile_drawer_client_server_up_to_date".tr(),
|
: "profile_drawer_client_server_up_to_date".tr(),
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 11,
|
fontSize: 11,
|
||||||
color: Theme.of(context).primaryColor,
|
color: Theme.of(context).primaryColor,
|
||||||
fontWeight: FontWeight.w600),
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const Divider(),
|
const Divider(),
|
||||||
|
@ -271,7 +282,7 @@ class ProfileDrawer extends HookConsumerWidget {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
"${serverInfoState.serverVersion.major}.${serverInfoState.serverVersion.minor}.${serverInfoState.serverVersion.patch}",
|
"${serverInfoState.serverVersion.major}.${serverInfoState.serverVersion.minor}.${serverInfoState.serverVersion.patch_}",
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 11,
|
fontSize: 11,
|
||||||
color: Colors.grey[500],
|
color: Colors.grey[500],
|
||||||
|
|
|
@ -8,11 +8,11 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/constants/hive_box.dart';
|
import 'package:immich_mobile/constants/hive_box.dart';
|
||||||
import 'package:immich_mobile/modules/home/providers/home_page_state.provider.dart';
|
import 'package:immich_mobile/modules/home/providers/home_page_state.provider.dart';
|
||||||
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
|
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
|
||||||
import 'package:immich_mobile/shared/models/immich_asset.model.dart';
|
|
||||||
import 'package:immich_mobile/routing/router.dart';
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
|
import 'package:openapi/api.dart';
|
||||||
|
|
||||||
class ThumbnailImage extends HookConsumerWidget {
|
class ThumbnailImage extends HookConsumerWidget {
|
||||||
final ImmichAsset asset;
|
final AssetResponseDto asset;
|
||||||
|
|
||||||
const ThumbnailImage({Key? key, required this.asset}) : super(key: key);
|
const ThumbnailImage({Key? key, required this.asset}) : super(key: key);
|
||||||
|
|
||||||
|
@ -22,14 +22,13 @@ class ThumbnailImage extends HookConsumerWidget {
|
||||||
|
|
||||||
var box = Hive.box(userInfoBox);
|
var box = Hive.box(userInfoBox);
|
||||||
var thumbnailRequestUrl =
|
var thumbnailRequestUrl =
|
||||||
'${box.get(serverEndpointKey)}/asset/file?aid=${asset.deviceAssetId}&did=${asset.deviceId}&isThumb=true';
|
'${box.get(serverEndpointKey)}/asset/thumbnail/${asset.id}';
|
||||||
|
|
||||||
var selectedAsset = ref.watch(homePageStateProvider).selectedItems;
|
var selectedAsset = ref.watch(homePageStateProvider).selectedItems;
|
||||||
var isMultiSelectEnable =
|
var isMultiSelectEnable =
|
||||||
ref.watch(homePageStateProvider).isMultiSelectEnable;
|
ref.watch(homePageStateProvider).isMultiSelectEnable;
|
||||||
var deviceId = ref.watch(authenticationProvider).deviceId;
|
var deviceId = ref.watch(authenticationProvider).deviceId;
|
||||||
|
|
||||||
Widget _buildSelectionIcon(ImmichAsset asset) {
|
Widget _buildSelectionIcon(AssetResponseDto asset) {
|
||||||
if (selectedAsset.contains(asset)) {
|
if (selectedAsset.contains(asset)) {
|
||||||
return Icon(
|
return Icon(
|
||||||
Icons.check_circle,
|
Icons.check_circle,
|
||||||
|
@ -61,7 +60,7 @@ class ThumbnailImage extends HookConsumerWidget {
|
||||||
.watch(homePageStateProvider.notifier)
|
.watch(homePageStateProvider.notifier)
|
||||||
.addSingleSelectedItem(asset);
|
.addSingleSelectedItem(asset);
|
||||||
} else {
|
} else {
|
||||||
if (asset.type == 'IMAGE') {
|
if (asset.type == AssetTypeEnum.IMAGE) {
|
||||||
AutoRouter.of(context).push(
|
AutoRouter.of(context).push(
|
||||||
ImageViewerRoute(
|
ImageViewerRoute(
|
||||||
imageUrl:
|
imageUrl:
|
||||||
|
@ -74,9 +73,10 @@ class ThumbnailImage extends HookConsumerWidget {
|
||||||
} else {
|
} else {
|
||||||
AutoRouter.of(context).push(
|
AutoRouter.of(context).push(
|
||||||
VideoViewerRoute(
|
VideoViewerRoute(
|
||||||
videoUrl:
|
videoUrl:
|
||||||
'${box.get(serverEndpointKey)}/asset/file?aid=${asset.deviceAssetId}&did=${asset.deviceId}',
|
'${box.get(serverEndpointKey)}/asset/file?aid=${asset.deviceAssetId}&did=${asset.deviceId}',
|
||||||
asset: asset),
|
asset: asset,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -94,14 +94,16 @@ class ThumbnailImage extends HookConsumerWidget {
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
border: isMultiSelectEnable && selectedAsset.contains(asset)
|
border: isMultiSelectEnable && selectedAsset.contains(asset)
|
||||||
? Border.all(
|
? Border.all(
|
||||||
color: Theme.of(context).primaryColorLight, width: 10)
|
color: Theme.of(context).primaryColorLight,
|
||||||
|
width: 10,
|
||||||
|
)
|
||||||
: const Border(),
|
: const Border(),
|
||||||
),
|
),
|
||||||
child: CachedNetworkImage(
|
child: CachedNetworkImage(
|
||||||
cacheKey: "${asset.id}-${cacheKey.value}",
|
cacheKey: "${asset.id}-${cacheKey.value}",
|
||||||
width: 300,
|
width: 300,
|
||||||
height: 300,
|
height: 300,
|
||||||
memCacheHeight: asset.type == 'IMAGE' ? 250 : 400,
|
memCacheHeight: asset.type == AssetTypeEnum.IMAGE ? 250 : 400,
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
imageUrl: thumbnailRequestUrl,
|
imageUrl: thumbnailRequestUrl,
|
||||||
httpHeaders: {
|
httpHeaders: {
|
||||||
|
@ -112,9 +114,11 @@ class ThumbnailImage extends HookConsumerWidget {
|
||||||
Transform.scale(
|
Transform.scale(
|
||||||
scale: 0.2,
|
scale: 0.2,
|
||||||
child: CircularProgressIndicator(
|
child: CircularProgressIndicator(
|
||||||
value: downloadProgress.progress),
|
value: downloadProgress.progress,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
errorWidget: (context, url, error) {
|
errorWidget: (context, url, error) {
|
||||||
|
debugPrint("Error getting thumbnail $url = $error");
|
||||||
return Icon(
|
return Icon(
|
||||||
Icons.image_not_supported_outlined,
|
Icons.image_not_supported_outlined,
|
||||||
color: Theme.of(context).primaryColor,
|
color: Theme.of(context).primaryColor,
|
||||||
|
|
|
@ -26,12 +26,15 @@ class HomePage extends HookConsumerWidget {
|
||||||
ref.watch(homePageStateProvider).isMultiSelectEnable;
|
ref.watch(homePageStateProvider).isMultiSelectEnable;
|
||||||
var homePageState = ref.watch(homePageStateProvider);
|
var homePageState = ref.watch(homePageStateProvider);
|
||||||
|
|
||||||
useEffect(() {
|
useEffect(
|
||||||
ref.read(websocketProvider.notifier).connect();
|
() {
|
||||||
ref.read(assetProvider.notifier).getAllAsset();
|
ref.read(websocketProvider.notifier).connect();
|
||||||
ref.watch(serverInfoProvider.notifier).getServerVersion();
|
ref.read(assetProvider.notifier).getAllAsset();
|
||||||
return null;
|
ref.watch(serverInfoProvider.notifier).getServerVersion();
|
||||||
}, []);
|
return null;
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
void reloadAllAsset() {
|
void reloadAllAsset() {
|
||||||
ref.read(assetProvider.notifier).getAllAsset();
|
ref.read(assetProvider.notifier).getAllAsset();
|
||||||
|
|
|
@ -1,10 +1,8 @@
|
||||||
import 'dart:convert';
|
import 'package:openapi/api.dart';
|
||||||
|
|
||||||
import 'package:immich_mobile/shared/models/device_info.model.dart';
|
|
||||||
|
|
||||||
class AuthenticationState {
|
class AuthenticationState {
|
||||||
final String deviceId;
|
final String deviceId;
|
||||||
final String deviceType;
|
final DeviceTypeEnum deviceType;
|
||||||
final String userId;
|
final String userId;
|
||||||
final String userEmail;
|
final String userEmail;
|
||||||
final bool isAuthenticated;
|
final bool isAuthenticated;
|
||||||
|
@ -13,8 +11,7 @@ class AuthenticationState {
|
||||||
final bool isAdmin;
|
final bool isAdmin;
|
||||||
final bool shouldChangePassword;
|
final bool shouldChangePassword;
|
||||||
final String profileImagePath;
|
final String profileImagePath;
|
||||||
final DeviceInfoRemote deviceInfo;
|
final DeviceInfoResponseDto deviceInfo;
|
||||||
|
|
||||||
AuthenticationState({
|
AuthenticationState({
|
||||||
required this.deviceId,
|
required this.deviceId,
|
||||||
required this.deviceType,
|
required this.deviceType,
|
||||||
|
@ -31,7 +28,7 @@ class AuthenticationState {
|
||||||
|
|
||||||
AuthenticationState copyWith({
|
AuthenticationState copyWith({
|
||||||
String? deviceId,
|
String? deviceId,
|
||||||
String? deviceType,
|
DeviceTypeEnum? deviceType,
|
||||||
String? userId,
|
String? userId,
|
||||||
String? userEmail,
|
String? userEmail,
|
||||||
bool? isAuthenticated,
|
bool? isAuthenticated,
|
||||||
|
@ -40,7 +37,7 @@ class AuthenticationState {
|
||||||
bool? isAdmin,
|
bool? isAdmin,
|
||||||
bool? shouldChangePassword,
|
bool? shouldChangePassword,
|
||||||
String? profileImagePath,
|
String? profileImagePath,
|
||||||
DeviceInfoRemote? deviceInfo,
|
DeviceInfoResponseDto? deviceInfo,
|
||||||
}) {
|
}) {
|
||||||
return AuthenticationState(
|
return AuthenticationState(
|
||||||
deviceId: deviceId ?? this.deviceId,
|
deviceId: deviceId ?? this.deviceId,
|
||||||
|
@ -57,45 +54,6 @@ class AuthenticationState {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<String, dynamic> toMap() {
|
|
||||||
final result = <String, dynamic>{};
|
|
||||||
|
|
||||||
result.addAll({'deviceId': deviceId});
|
|
||||||
result.addAll({'deviceType': deviceType});
|
|
||||||
result.addAll({'userId': userId});
|
|
||||||
result.addAll({'userEmail': userEmail});
|
|
||||||
result.addAll({'isAuthenticated': isAuthenticated});
|
|
||||||
result.addAll({'firstName': firstName});
|
|
||||||
result.addAll({'lastName': lastName});
|
|
||||||
result.addAll({'isAdmin': isAdmin});
|
|
||||||
result.addAll({'shouldChangePassword': shouldChangePassword});
|
|
||||||
result.addAll({'profileImagePath': profileImagePath});
|
|
||||||
result.addAll({'deviceInfo': deviceInfo.toMap()});
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
factory AuthenticationState.fromMap(Map<String, dynamic> map) {
|
|
||||||
return AuthenticationState(
|
|
||||||
deviceId: map['deviceId'] ?? '',
|
|
||||||
deviceType: map['deviceType'] ?? '',
|
|
||||||
userId: map['userId'] ?? '',
|
|
||||||
userEmail: map['userEmail'] ?? '',
|
|
||||||
isAuthenticated: map['isAuthenticated'] ?? false,
|
|
||||||
firstName: map['firstName'] ?? '',
|
|
||||||
lastName: map['lastName'] ?? '',
|
|
||||||
isAdmin: map['isAdmin'] ?? false,
|
|
||||||
shouldChangePassword: map['shouldChangePassword'] ?? false,
|
|
||||||
profileImagePath: map['profileImagePath'] ?? '',
|
|
||||||
deviceInfo: DeviceInfoRemote.fromMap(map['deviceInfo']),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
String toJson() => json.encode(toMap());
|
|
||||||
|
|
||||||
factory AuthenticationState.fromJson(String source) =>
|
|
||||||
AuthenticationState.fromMap(json.decode(source));
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return 'AuthenticationState(deviceId: $deviceId, deviceType: $deviceType, userId: $userId, userEmail: $userEmail, isAuthenticated: $isAuthenticated, firstName: $firstName, lastName: $lastName, isAdmin: $isAdmin, shouldChangePassword: $shouldChangePassword, profileImagePath: $profileImagePath, deviceInfo: $deviceInfo)';
|
return 'AuthenticationState(deviceId: $deviceId, deviceType: $deviceType, userId: $userId, userEmail: $userEmail, isAuthenticated: $isAuthenticated, firstName: $firstName, lastName: $lastName, isAdmin: $isAdmin, shouldChangePassword: $shouldChangePassword, profileImagePath: $profileImagePath, deviceInfo: $deviceInfo)';
|
||||||
|
|
|
@ -16,9 +16,10 @@ class HiveSavedLoginInfo {
|
||||||
@HiveField(3)
|
@HiveField(3)
|
||||||
bool isSaveLogin;
|
bool isSaveLogin;
|
||||||
|
|
||||||
HiveSavedLoginInfo(
|
HiveSavedLoginInfo({
|
||||||
{required this.email,
|
required this.email,
|
||||||
required this.password,
|
required this.password,
|
||||||
required this.serverUrl,
|
required this.serverUrl,
|
||||||
required this.isSaveLogin});
|
required this.isSaveLogin,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,23 +1,23 @@
|
||||||
import 'package:dio/dio.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hive/hive.dart';
|
import 'package:hive/hive.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/constants/hive_box.dart';
|
import 'package:immich_mobile/constants/hive_box.dart';
|
||||||
import 'package:immich_mobile/modules/login/models/authentication_state.model.dart';
|
import 'package:immich_mobile/modules/login/models/authentication_state.model.dart';
|
||||||
import 'package:immich_mobile/modules/login/models/hive_saved_login_info.model.dart';
|
import 'package:immich_mobile/modules/login/models/hive_saved_login_info.model.dart';
|
||||||
import 'package:immich_mobile/modules/login/models/login_response.model.dart';
|
|
||||||
import 'package:immich_mobile/modules/backup/services/backup.service.dart';
|
import 'package:immich_mobile/modules/backup/services/backup.service.dart';
|
||||||
|
import 'package:immich_mobile/shared/services/api.service.dart';
|
||||||
import 'package:immich_mobile/shared/services/device_info.service.dart';
|
import 'package:immich_mobile/shared/services/device_info.service.dart';
|
||||||
import 'package:immich_mobile/shared/services/network.service.dart';
|
import 'package:openapi/api.dart';
|
||||||
import 'package:immich_mobile/shared/models/device_info.model.dart';
|
|
||||||
|
|
||||||
class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
|
class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
|
||||||
AuthenticationNotifier(
|
AuthenticationNotifier(
|
||||||
this._deviceInfoService, this._backupService, this._networkService)
|
this._deviceInfoService,
|
||||||
: super(
|
this._backupService,
|
||||||
|
this._apiService,
|
||||||
|
) : super(
|
||||||
AuthenticationState(
|
AuthenticationState(
|
||||||
deviceId: "",
|
deviceId: "",
|
||||||
deviceType: "",
|
deviceType: DeviceTypeEnum.ANDROID,
|
||||||
userId: "",
|
userId: "",
|
||||||
userEmail: "",
|
userEmail: "",
|
||||||
firstName: '',
|
firstName: '',
|
||||||
|
@ -26,12 +26,11 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
|
||||||
isAdmin: false,
|
isAdmin: false,
|
||||||
shouldChangePassword: false,
|
shouldChangePassword: false,
|
||||||
isAuthenticated: false,
|
isAuthenticated: false,
|
||||||
deviceInfo: DeviceInfoRemote(
|
deviceInfo: DeviceInfoResponseDto(
|
||||||
id: 0,
|
id: 0,
|
||||||
userId: "",
|
userId: "",
|
||||||
deviceId: "",
|
deviceId: "",
|
||||||
deviceType: "",
|
deviceType: DeviceTypeEnum.ANDROID,
|
||||||
notificationToken: "",
|
|
||||||
createdAt: "",
|
createdAt: "",
|
||||||
isAutoBackup: false,
|
isAutoBackup: false,
|
||||||
),
|
),
|
||||||
|
@ -40,10 +39,14 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
|
||||||
|
|
||||||
final DeviceInfoService _deviceInfoService;
|
final DeviceInfoService _deviceInfoService;
|
||||||
final BackupService _backupService;
|
final BackupService _backupService;
|
||||||
final NetworkService _networkService;
|
final ApiService _apiService;
|
||||||
|
|
||||||
Future<bool> login(String email, String password, String serverEndpoint,
|
Future<bool> login(
|
||||||
bool isSavedLoginInfo) async {
|
String email,
|
||||||
|
String password,
|
||||||
|
String serverEndpoint,
|
||||||
|
bool isSavedLoginInfo,
|
||||||
|
) async {
|
||||||
// Store server endpoint to Hive and test endpoint
|
// Store server endpoint to Hive and test endpoint
|
||||||
if (serverEndpoint[serverEndpoint.length - 1] == "/") {
|
if (serverEndpoint[serverEndpoint.length - 1] == "/") {
|
||||||
var validUrl = serverEndpoint.substring(0, serverEndpoint.length - 1);
|
var validUrl = serverEndpoint.substring(0, serverEndpoint.length - 1);
|
||||||
|
@ -52,12 +55,12 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
|
||||||
Hive.box(userInfoBox).put(serverEndpointKey, serverEndpoint);
|
Hive.box(userInfoBox).put(serverEndpointKey, serverEndpoint);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check Server URL validity
|
||||||
try {
|
try {
|
||||||
bool isServerEndpointVerified = await _networkService.pingServer();
|
_apiService.setEndpoint(Hive.box(userInfoBox).get(serverEndpointKey));
|
||||||
if (!isServerEndpointVerified) {
|
await _apiService.serverInfoApi.pingServer();
|
||||||
return false;
|
|
||||||
}
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
debugPrint('Invalid Server Endpoint Url $e');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -72,56 +75,73 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
|
||||||
|
|
||||||
// Make sign-in request
|
// Make sign-in request
|
||||||
try {
|
try {
|
||||||
Response res = await _networkService.postRequest(
|
var loginResponse = await _apiService.authenticationApi.login(
|
||||||
url: 'auth/login', data: {'email': email, 'password': password});
|
LoginCredentialDto(
|
||||||
|
email: email,
|
||||||
|
password: password,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
var payload = LogInReponse.fromJson(res.toString());
|
if (loginResponse == null) {
|
||||||
|
debugPrint('Login Response is null');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
Hive.box(userInfoBox).put(accessTokenKey, payload.accessToken);
|
Hive.box(userInfoBox).put(accessTokenKey, loginResponse.accessToken);
|
||||||
|
|
||||||
state = state.copyWith(
|
state = state.copyWith(
|
||||||
isAuthenticated: true,
|
isAuthenticated: true,
|
||||||
userId: payload.userId,
|
userId: loginResponse.userId,
|
||||||
userEmail: payload.userEmail,
|
userEmail: loginResponse.userEmail,
|
||||||
firstName: payload.firstName,
|
firstName: loginResponse.firstName,
|
||||||
lastName: payload.lastName,
|
lastName: loginResponse.lastName,
|
||||||
profileImagePath: payload.profileImagePath,
|
profileImagePath: loginResponse.profileImagePath,
|
||||||
isAdmin: payload.isAdmin,
|
isAdmin: loginResponse.isAdmin,
|
||||||
shouldChangePassword: payload.shouldChangePassword,
|
shouldChangePassword: loginResponse.shouldChangePassword,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Login Success - Set Access Token to API Client
|
||||||
|
_apiService.setAccessToken(loginResponse.accessToken);
|
||||||
|
|
||||||
if (isSavedLoginInfo) {
|
if (isSavedLoginInfo) {
|
||||||
// Save login info to local storage
|
// Save login info to local storage
|
||||||
Hive.box<HiveSavedLoginInfo>(hiveLoginInfoBox).put(
|
Hive.box<HiveSavedLoginInfo>(hiveLoginInfoBox).put(
|
||||||
savedLoginInfoKey,
|
savedLoginInfoKey,
|
||||||
HiveSavedLoginInfo(
|
HiveSavedLoginInfo(
|
||||||
email: email,
|
email: email,
|
||||||
password: password,
|
password: password,
|
||||||
isSaveLogin: true,
|
isSaveLogin: true,
|
||||||
serverUrl: Hive.box(userInfoBox).get(serverEndpointKey)),
|
serverUrl: Hive.box(userInfoBox).get(serverEndpointKey),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
Hive.box<HiveSavedLoginInfo>(hiveLoginInfoBox)
|
Hive.box<HiveSavedLoginInfo>(hiveLoginInfoBox)
|
||||||
.delete(savedLoginInfoKey);
|
.delete(savedLoginInfoKey);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
debugPrint("Error logging in $e");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Register device info
|
// Register device info
|
||||||
try {
|
try {
|
||||||
Response res = await _networkService.postRequest(
|
DeviceInfoResponseDto? deviceInfo =
|
||||||
url: 'device-info',
|
await _apiService.deviceInfoApi.createDeviceInfo(
|
||||||
data: {
|
CreateDeviceInfoDto(
|
||||||
'deviceId': state.deviceId,
|
deviceId: state.deviceId,
|
||||||
'deviceType': state.deviceType,
|
deviceType: state.deviceType,
|
||||||
},
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
DeviceInfoRemote deviceInfo = DeviceInfoRemote.fromJson(res.toString());
|
if (deviceInfo == null) {
|
||||||
|
debugPrint('Device Info Response is null');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
state = state.copyWith(deviceInfo: deviceInfo);
|
state = state.copyWith(deviceInfo: deviceInfo);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint("ERROR Register Device Info: $e");
|
debugPrint("ERROR Register Device Info: $e");
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
@ -129,27 +149,7 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
|
||||||
|
|
||||||
Future<bool> logout() async {
|
Future<bool> logout() async {
|
||||||
Hive.box(userInfoBox).delete(accessTokenKey);
|
Hive.box(userInfoBox).delete(accessTokenKey);
|
||||||
state = AuthenticationState(
|
state = state.copyWith(isAuthenticated: false);
|
||||||
deviceId: "",
|
|
||||||
deviceType: "",
|
|
||||||
userId: "",
|
|
||||||
userEmail: "",
|
|
||||||
firstName: '',
|
|
||||||
lastName: '',
|
|
||||||
profileImagePath: '',
|
|
||||||
shouldChangePassword: false,
|
|
||||||
isAuthenticated: false,
|
|
||||||
isAdmin: false,
|
|
||||||
deviceInfo: DeviceInfoRemote(
|
|
||||||
id: 0,
|
|
||||||
userId: "",
|
|
||||||
deviceId: "",
|
|
||||||
deviceType: "",
|
|
||||||
notificationToken: "",
|
|
||||||
createdAt: "",
|
|
||||||
isAutoBackup: false,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -157,11 +157,13 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
|
||||||
setAutoBackup(bool backupState) async {
|
setAutoBackup(bool backupState) async {
|
||||||
var deviceInfo = await _deviceInfoService.getDeviceInfo();
|
var deviceInfo = await _deviceInfoService.getDeviceInfo();
|
||||||
var deviceId = deviceInfo["deviceId"];
|
var deviceId = deviceInfo["deviceId"];
|
||||||
var deviceType = deviceInfo["deviceType"];
|
|
||||||
|
|
||||||
DeviceInfoRemote deviceInfoRemote =
|
DeviceTypeEnum deviceType = deviceInfo["deviceType"];
|
||||||
|
|
||||||
|
DeviceInfoResponseDto updatedDeviceInfo =
|
||||||
await _backupService.setAutoBackup(backupState, deviceId, deviceType);
|
await _backupService.setAutoBackup(backupState, deviceId, deviceType);
|
||||||
state = state.copyWith(deviceInfo: deviceInfoRemote);
|
|
||||||
|
state = state.copyWith(deviceInfo: updatedDeviceInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
updateUserProfileImagePath(String path) {
|
updateUserProfileImagePath(String path) {
|
||||||
|
@ -169,19 +171,20 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> changePassword(String newPassword) async {
|
Future<bool> changePassword(String newPassword) async {
|
||||||
Response res = await _networkService.putRequest(
|
try {
|
||||||
url: 'user',
|
await _apiService.userApi.updateUser(
|
||||||
data: {
|
UpdateUserDto(
|
||||||
'id': state.userId,
|
id: state.userId,
|
||||||
'password': newPassword,
|
password: newPassword,
|
||||||
'shouldChangePassword': false,
|
shouldChangePassword: false,
|
||||||
},
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (res.statusCode == 200) {
|
|
||||||
state = state.copyWith(shouldChangePassword: false);
|
state = state.copyWith(shouldChangePassword: false);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} catch (e) {
|
||||||
|
debugPrint("Error changing password $e");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -192,6 +195,6 @@ final authenticationProvider =
|
||||||
return AuthenticationNotifier(
|
return AuthenticationNotifier(
|
||||||
ref.watch(deviceInfoServiceProvider),
|
ref.watch(deviceInfoServiceProvider),
|
||||||
ref.watch(backupServiceProvider),
|
ref.watch(backupServiceProvider),
|
||||||
ref.watch(networkServiceProvider),
|
ref.watch(apiServiceProvider),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
@ -140,35 +140,36 @@ class ChangePasswordButton extends ConsumerWidget {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
return ElevatedButton(
|
return ElevatedButton(
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
visualDensity: VisualDensity.standard,
|
visualDensity: VisualDensity.standard,
|
||||||
primary: Theme.of(context).primaryColor,
|
primary: Theme.of(context).primaryColor,
|
||||||
onPrimary: Colors.grey[50],
|
onPrimary: Colors.grey[50],
|
||||||
elevation: 2,
|
elevation: 2,
|
||||||
padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 25),
|
padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 25),
|
||||||
),
|
),
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
if (formKey.currentState!.validate()) {
|
if (formKey.currentState!.validate()) {
|
||||||
var isSuccess = await ref
|
var isSuccess = await ref
|
||||||
.watch(authenticationProvider.notifier)
|
.watch(authenticationProvider.notifier)
|
||||||
.changePassword(passwordController.value.text);
|
.changePassword(passwordController.value.text);
|
||||||
|
|
||||||
if (isSuccess) {
|
if (isSuccess) {
|
||||||
bool res =
|
bool res =
|
||||||
await ref.watch(authenticationProvider.notifier).logout();
|
await ref.watch(authenticationProvider.notifier).logout();
|
||||||
|
|
||||||
if (res) {
|
if (res) {
|
||||||
ref.watch(backupProvider.notifier).cancelBackup();
|
ref.watch(backupProvider.notifier).cancelBackup();
|
||||||
ref.watch(assetProvider.notifier).clearAllAsset();
|
ref.watch(assetProvider.notifier).clearAllAsset();
|
||||||
ref.watch(websocketProvider.notifier).disconnect();
|
ref.watch(websocketProvider.notifier).disconnect();
|
||||||
AutoRouter.of(context).replace(const LoginRoute());
|
AutoRouter.of(context).replace(const LoginRoute());
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
child: const Text(
|
},
|
||||||
"Change Password",
|
child: const Text(
|
||||||
style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold),
|
"Change Password",
|
||||||
));
|
style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,22 +22,25 @@ class LoginForm extends HookConsumerWidget {
|
||||||
final passwordController =
|
final passwordController =
|
||||||
useTextEditingController.fromValue(TextEditingValue.empty);
|
useTextEditingController.fromValue(TextEditingValue.empty);
|
||||||
final serverEndpointController =
|
final serverEndpointController =
|
||||||
useTextEditingController(text: 'login_endpoint_hint'.tr());
|
useTextEditingController(text: 'login_form_endpoint_hint'.tr());
|
||||||
final isSaveLoginInfo = useState<bool>(false);
|
final isSaveLoginInfo = useState<bool>(false);
|
||||||
|
|
||||||
useEffect(() {
|
useEffect(
|
||||||
var loginInfo =
|
() {
|
||||||
Hive.box<HiveSavedLoginInfo>(hiveLoginInfoBox).get(savedLoginInfoKey);
|
var loginInfo = Hive.box<HiveSavedLoginInfo>(hiveLoginInfoBox)
|
||||||
|
.get(savedLoginInfoKey);
|
||||||
|
|
||||||
if (loginInfo != null) {
|
if (loginInfo != null) {
|
||||||
usernameController.text = loginInfo.email;
|
usernameController.text = loginInfo.email;
|
||||||
passwordController.text = loginInfo.password;
|
passwordController.text = loginInfo.password;
|
||||||
serverEndpointController.text = loginInfo.serverUrl;
|
serverEndpointController.text = loginInfo.serverUrl;
|
||||||
isSaveLoginInfo.value = loginInfo.isSaveLogin;
|
isSaveLoginInfo.value = loginInfo.isSaveLogin;
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}, []);
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
return Center(
|
return Center(
|
||||||
child: ConstrainedBox(
|
child: ConstrainedBox(
|
||||||
|
@ -71,14 +74,16 @@ class LoginForm extends HookConsumerWidget {
|
||||||
dense: true,
|
dense: true,
|
||||||
side: const BorderSide(color: Colors.grey, width: 1.5),
|
side: const BorderSide(color: Colors.grey, width: 1.5),
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(5)),
|
borderRadius: BorderRadius.circular(5),
|
||||||
|
),
|
||||||
enableFeedback: true,
|
enableFeedback: true,
|
||||||
title: const Text(
|
title: const Text(
|
||||||
"login_form_save_login",
|
"login_form_save_login",
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
color: Colors.grey),
|
color: Colors.grey,
|
||||||
|
),
|
||||||
).tr(),
|
).tr(),
|
||||||
value: isSaveLoginInfo.value,
|
value: isSaveLoginInfo.value,
|
||||||
onChanged: (switchValue) {
|
onChanged: (switchValue) {
|
||||||
|
@ -108,7 +113,6 @@ class ServerEndpointInput extends StatelessWidget {
|
||||||
: super(key: key);
|
: super(key: key);
|
||||||
|
|
||||||
String? _validateInput(String? url) {
|
String? _validateInput(String? url) {
|
||||||
|
|
||||||
if (url?.startsWith(RegExp(r'https?://')) == true) {
|
if (url?.startsWith(RegExp(r'https?://')) == true) {
|
||||||
return null;
|
return null;
|
||||||
} else {
|
} else {
|
||||||
|
@ -122,7 +126,7 @@ class ServerEndpointInput extends StatelessWidget {
|
||||||
controller: controller,
|
controller: controller,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: 'login_form_endpoint_url'.tr(),
|
labelText: 'login_form_endpoint_url'.tr(),
|
||||||
border: OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
hintText: 'login_form_endpoint_hint'.tr(),
|
hintText: 'login_form_endpoint_hint'.tr(),
|
||||||
),
|
),
|
||||||
validator: _validateInput,
|
validator: _validateInput,
|
||||||
|
@ -140,8 +144,9 @@ class EmailInput extends StatelessWidget {
|
||||||
if (email == null || email == '') return null;
|
if (email == null || email == '') return null;
|
||||||
if (email.endsWith(' ')) return 'login_form_err_trailing_whitespace'.tr();
|
if (email.endsWith(' ')) return 'login_form_err_trailing_whitespace'.tr();
|
||||||
if (email.startsWith(' ')) return 'login_form_err_leading_whitespace'.tr();
|
if (email.startsWith(' ')) return 'login_form_err_leading_whitespace'.tr();
|
||||||
if (email.contains(' ') || !email.contains('@'))
|
if (email.contains(' ') || !email.contains('@')) {
|
||||||
return 'login_form_err_invalid_email'.tr();
|
return 'login_form_err_invalid_email'.tr();
|
||||||
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -151,7 +156,7 @@ class EmailInput extends StatelessWidget {
|
||||||
controller: controller,
|
controller: controller,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: 'login_form_label_email'.tr(),
|
labelText: 'login_form_label_email'.tr(),
|
||||||
border: OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
hintText: 'login_form_email_hint'.tr(),
|
hintText: 'login_form_email_hint'.tr(),
|
||||||
),
|
),
|
||||||
validator: _validateInput,
|
validator: _validateInput,
|
||||||
|
@ -171,9 +176,10 @@ class PasswordInput extends StatelessWidget {
|
||||||
obscureText: true,
|
obscureText: true,
|
||||||
controller: controller,
|
controller: controller,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: 'login_form_label_password'.tr(),
|
labelText: 'login_form_label_password'.tr(),
|
||||||
border: OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
hintText: 'login_form_password_hint'.tr()),
|
hintText: 'login_form_password_hint'.tr(),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -195,43 +201,47 @@ class LoginButton extends ConsumerWidget {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
return ElevatedButton(
|
return ElevatedButton(
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
visualDensity: VisualDensity.standard,
|
visualDensity: VisualDensity.standard,
|
||||||
primary: Theme.of(context).primaryColor,
|
primary: Theme.of(context).primaryColor,
|
||||||
onPrimary: Colors.grey[50],
|
onPrimary: Colors.grey[50],
|
||||||
elevation: 2,
|
elevation: 2,
|
||||||
padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 25),
|
padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 25),
|
||||||
),
|
),
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
// This will remove current cache asset state of previous user login.
|
// This will remove current cache asset state of previous user login.
|
||||||
ref.watch(assetProvider.notifier).clearAllAsset();
|
ref.watch(assetProvider.notifier).clearAllAsset();
|
||||||
|
|
||||||
var isAuthenticated = await ref
|
var isAuthenticated =
|
||||||
.watch(authenticationProvider.notifier)
|
await ref.watch(authenticationProvider.notifier).login(
|
||||||
.login(emailController.text, passwordController.text,
|
emailController.text,
|
||||||
serverEndpointController.text, isSavedLoginInfo);
|
passwordController.text,
|
||||||
|
serverEndpointController.text,
|
||||||
|
isSavedLoginInfo,
|
||||||
|
);
|
||||||
|
|
||||||
if (isAuthenticated) {
|
if (isAuthenticated) {
|
||||||
// Resume backup (if enable) then navigate
|
// Resume backup (if enable) then navigate
|
||||||
|
|
||||||
if (ref.watch(authenticationProvider).shouldChangePassword &&
|
if (ref.watch(authenticationProvider).shouldChangePassword &&
|
||||||
!ref.watch(authenticationProvider).isAdmin) {
|
!ref.watch(authenticationProvider).isAdmin) {
|
||||||
AutoRouter.of(context).push(const ChangePasswordRoute());
|
AutoRouter.of(context).push(const ChangePasswordRoute());
|
||||||
} else {
|
|
||||||
ref.watch(backupProvider.notifier).resumeBackup();
|
|
||||||
AutoRouter.of(context).pushNamed("/tab-controller-page");
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
ImmichToast.show(
|
ref.watch(backupProvider.notifier).resumeBackup();
|
||||||
context: context,
|
AutoRouter.of(context).pushNamed("/tab-controller-page");
|
||||||
msg: "login_failed".tr(),
|
|
||||||
toastType: ToastType.error,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
},
|
} else {
|
||||||
child: const Text(
|
ImmichToast.show(
|
||||||
"login_form_button_text",
|
context: context,
|
||||||
style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold),
|
msg: "login_form_failed_login".tr(),
|
||||||
).tr());
|
toastType: ToastType.error,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: const Text(
|
||||||
|
"login_form_button_text",
|
||||||
|
style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold),
|
||||||
|
).tr(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,84 +0,0 @@
|
||||||
import 'dart:convert';
|
|
||||||
|
|
||||||
class CuratedLocation {
|
|
||||||
final String id;
|
|
||||||
final String city;
|
|
||||||
final String resizePath;
|
|
||||||
final String deviceAssetId;
|
|
||||||
final String deviceId;
|
|
||||||
|
|
||||||
CuratedLocation({
|
|
||||||
required this.id,
|
|
||||||
required this.city,
|
|
||||||
required this.resizePath,
|
|
||||||
required this.deviceAssetId,
|
|
||||||
required this.deviceId,
|
|
||||||
});
|
|
||||||
|
|
||||||
CuratedLocation copyWith({
|
|
||||||
String? id,
|
|
||||||
String? city,
|
|
||||||
String? resizePath,
|
|
||||||
String? deviceAssetId,
|
|
||||||
String? deviceId,
|
|
||||||
}) {
|
|
||||||
return CuratedLocation(
|
|
||||||
id: id ?? this.id,
|
|
||||||
city: city ?? this.city,
|
|
||||||
resizePath: resizePath ?? this.resizePath,
|
|
||||||
deviceAssetId: deviceAssetId ?? this.deviceAssetId,
|
|
||||||
deviceId: deviceId ?? this.deviceId,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Map<String, dynamic> toMap() {
|
|
||||||
return {
|
|
||||||
'id': id,
|
|
||||||
'city': city,
|
|
||||||
'resizePath': resizePath,
|
|
||||||
'deviceAssetId': deviceAssetId,
|
|
||||||
'deviceId': deviceId,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
factory CuratedLocation.fromMap(Map<String, dynamic> map) {
|
|
||||||
return CuratedLocation(
|
|
||||||
id: map['id'] ?? '',
|
|
||||||
city: map['city'] ?? '',
|
|
||||||
resizePath: map['resizePath'] ?? '',
|
|
||||||
deviceAssetId: map['deviceAssetId'] ?? '',
|
|
||||||
deviceId: map['deviceId'] ?? '',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
String toJson() => json.encode(toMap());
|
|
||||||
|
|
||||||
factory CuratedLocation.fromJson(String source) =>
|
|
||||||
CuratedLocation.fromMap(json.decode(source));
|
|
||||||
|
|
||||||
@override
|
|
||||||
String toString() {
|
|
||||||
return 'CuratedLocation(id: $id, city: $city, resizePath: $resizePath, deviceAssetId: $deviceAssetId, deviceId: $deviceId)';
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool operator ==(Object other) {
|
|
||||||
if (identical(this, other)) return true;
|
|
||||||
|
|
||||||
return other is CuratedLocation &&
|
|
||||||
other.id == id &&
|
|
||||||
other.city == city &&
|
|
||||||
other.resizePath == resizePath &&
|
|
||||||
other.deviceAssetId == deviceAssetId &&
|
|
||||||
other.deviceId == deviceId;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
int get hashCode {
|
|
||||||
return id.hashCode ^
|
|
||||||
city.hashCode ^
|
|
||||||
resizePath.hashCode ^
|
|
||||||
deviceAssetId.hashCode ^
|
|
||||||
deviceId.hashCode;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,85 +0,0 @@
|
||||||
import 'dart:convert';
|
|
||||||
|
|
||||||
class CuratedObject {
|
|
||||||
final String id;
|
|
||||||
final String object;
|
|
||||||
final String resizePath;
|
|
||||||
final String deviceAssetId;
|
|
||||||
final String deviceId;
|
|
||||||
CuratedObject({
|
|
||||||
required this.id,
|
|
||||||
required this.object,
|
|
||||||
required this.resizePath,
|
|
||||||
required this.deviceAssetId,
|
|
||||||
required this.deviceId,
|
|
||||||
});
|
|
||||||
|
|
||||||
CuratedObject copyWith({
|
|
||||||
String? id,
|
|
||||||
String? object,
|
|
||||||
String? resizePath,
|
|
||||||
String? deviceAssetId,
|
|
||||||
String? deviceId,
|
|
||||||
}) {
|
|
||||||
return CuratedObject(
|
|
||||||
id: id ?? this.id,
|
|
||||||
object: object ?? this.object,
|
|
||||||
resizePath: resizePath ?? this.resizePath,
|
|
||||||
deviceAssetId: deviceAssetId ?? this.deviceAssetId,
|
|
||||||
deviceId: deviceId ?? this.deviceId,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Map<String, dynamic> toMap() {
|
|
||||||
final result = <String, dynamic>{};
|
|
||||||
|
|
||||||
result.addAll({'id': id});
|
|
||||||
result.addAll({'object': object});
|
|
||||||
result.addAll({'resizePath': resizePath});
|
|
||||||
result.addAll({'deviceAssetId': deviceAssetId});
|
|
||||||
result.addAll({'deviceId': deviceId});
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
factory CuratedObject.fromMap(Map<String, dynamic> map) {
|
|
||||||
return CuratedObject(
|
|
||||||
id: map['id'] ?? '',
|
|
||||||
object: map['object'] ?? '',
|
|
||||||
resizePath: map['resizePath'] ?? '',
|
|
||||||
deviceAssetId: map['deviceAssetId'] ?? '',
|
|
||||||
deviceId: map['deviceId'] ?? '',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
String toJson() => json.encode(toMap());
|
|
||||||
|
|
||||||
factory CuratedObject.fromJson(String source) =>
|
|
||||||
CuratedObject.fromMap(json.decode(source));
|
|
||||||
|
|
||||||
@override
|
|
||||||
String toString() {
|
|
||||||
return 'CuratedObject(id: $id, object: $object, resizePath: $resizePath, deviceAssetId: $deviceAssetId, deviceId: $deviceId)';
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool operator ==(Object other) {
|
|
||||||
if (identical(this, other)) return true;
|
|
||||||
|
|
||||||
return other is CuratedObject &&
|
|
||||||
other.id == id &&
|
|
||||||
other.object == object &&
|
|
||||||
other.resizePath == resizePath &&
|
|
||||||
other.deviceAssetId == deviceAssetId &&
|
|
||||||
other.deviceId == deviceId;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
int get hashCode {
|
|
||||||
return id.hashCode ^
|
|
||||||
object.hashCode ^
|
|
||||||
resizePath.hashCode ^
|
|
||||||
deviceAssetId.hashCode ^
|
|
||||||
deviceId.hashCode;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,13 +1,13 @@
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:immich_mobile/shared/models/immich_asset.model.dart';
|
import 'package:openapi/api.dart';
|
||||||
|
|
||||||
class SearchResultPageState {
|
class SearchResultPageState {
|
||||||
final bool isLoading;
|
final bool isLoading;
|
||||||
final bool isSuccess;
|
final bool isSuccess;
|
||||||
final bool isError;
|
final bool isError;
|
||||||
final List<ImmichAsset> searchResult;
|
final List<AssetResponseDto> searchResult;
|
||||||
|
|
||||||
SearchResultPageState({
|
SearchResultPageState({
|
||||||
required this.isLoading,
|
required this.isLoading,
|
||||||
|
@ -20,7 +20,7 @@ class SearchResultPageState {
|
||||||
bool? isLoading,
|
bool? isLoading,
|
||||||
bool? isSuccess,
|
bool? isSuccess,
|
||||||
bool? isError,
|
bool? isError,
|
||||||
List<ImmichAsset>? searchResult,
|
List<AssetResponseDto>? searchResult,
|
||||||
}) {
|
}) {
|
||||||
return SearchResultPageState(
|
return SearchResultPageState(
|
||||||
isLoading: isLoading ?? this.isLoading,
|
isLoading: isLoading ?? this.isLoading,
|
||||||
|
@ -35,7 +35,7 @@ class SearchResultPageState {
|
||||||
'isLoading': isLoading,
|
'isLoading': isLoading,
|
||||||
'isSuccess': isSuccess,
|
'isSuccess': isSuccess,
|
||||||
'isError': isError,
|
'isError': isError,
|
||||||
'searchResult': searchResult.map((x) => x.toMap()).toList(),
|
'searchResult': searchResult.map((x) => x.toJson()).toList(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -44,8 +44,9 @@ class SearchResultPageState {
|
||||||
isLoading: map['isLoading'] ?? false,
|
isLoading: map['isLoading'] ?? false,
|
||||||
isSuccess: map['isSuccess'] ?? false,
|
isSuccess: map['isSuccess'] ?? false,
|
||||||
isError: map['isError'] ?? false,
|
isError: map['isError'] ?? false,
|
||||||
searchResult: List<ImmichAsset>.from(
|
searchResult: List<AssetResponseDto>.from(
|
||||||
map['searchResult']?.map((x) => ImmichAsset.fromMap(x))),
|
map['searchResult']?.map((x) => AssetResponseDto.mapFromJson(x)),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,9 +1,8 @@
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/modules/search/models/curated_location.model.dart';
|
|
||||||
import 'package:immich_mobile/modules/search/models/curated_object.model.dart';
|
|
||||||
import 'package:immich_mobile/modules/search/models/search_page_state.model.dart';
|
import 'package:immich_mobile/modules/search/models/search_page_state.model.dart';
|
||||||
|
|
||||||
import 'package:immich_mobile/modules/search/services/search.service.dart';
|
import 'package:immich_mobile/modules/search/services/search.service.dart';
|
||||||
|
import 'package:openapi/api.dart';
|
||||||
|
|
||||||
class SearchPageStateNotifier extends StateNotifier<SearchPageState> {
|
class SearchPageStateNotifier extends StateNotifier<SearchPageState> {
|
||||||
SearchPageStateNotifier(this._searchService)
|
SearchPageStateNotifier(this._searchService)
|
||||||
|
@ -58,7 +57,7 @@ final searchPageStateProvider =
|
||||||
});
|
});
|
||||||
|
|
||||||
final getCuratedLocationProvider =
|
final getCuratedLocationProvider =
|
||||||
FutureProvider.autoDispose<List<CuratedLocation>>((ref) async {
|
FutureProvider.autoDispose<List<CuratedLocationsResponseDto>>((ref) async {
|
||||||
final SearchService searchService = ref.watch(searchServiceProvider);
|
final SearchService searchService = ref.watch(searchServiceProvider);
|
||||||
|
|
||||||
var curatedLocation = await searchService.getCuratedLocation();
|
var curatedLocation = await searchService.getCuratedLocation();
|
||||||
|
@ -66,7 +65,7 @@ final getCuratedLocationProvider =
|
||||||
});
|
});
|
||||||
|
|
||||||
final getCuratedObjectProvider =
|
final getCuratedObjectProvider =
|
||||||
FutureProvider.autoDispose<List<CuratedObject>>((ref) async {
|
FutureProvider.autoDispose<List<CuratedObjectsResponseDto>>((ref) async {
|
||||||
final SearchService searchService = ref.watch(searchServiceProvider);
|
final SearchService searchService = ref.watch(searchServiceProvider);
|
||||||
|
|
||||||
var curatedObject = await searchService.getCuratedObjects();
|
var curatedObject = await searchService.getCuratedObjects();
|
||||||
|
|
|
@ -3,8 +3,8 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/modules/search/models/search_result_page_state.model.dart';
|
import 'package:immich_mobile/modules/search/models/search_result_page_state.model.dart';
|
||||||
|
|
||||||
import 'package:immich_mobile/modules/search/services/search.service.dart';
|
import 'package:immich_mobile/modules/search/services/search.service.dart';
|
||||||
import 'package:immich_mobile/shared/models/immich_asset.model.dart';
|
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
|
import 'package:openapi/api.dart';
|
||||||
|
|
||||||
class SearchResultPageNotifier extends StateNotifier<SearchResultPageState> {
|
class SearchResultPageNotifier extends StateNotifier<SearchResultPageState> {
|
||||||
SearchResultPageNotifier(this._searchService)
|
SearchResultPageNotifier(this._searchService)
|
||||||
|
@ -21,19 +21,29 @@ class SearchResultPageNotifier extends StateNotifier<SearchResultPageState> {
|
||||||
|
|
||||||
void search(String searchTerm) async {
|
void search(String searchTerm) async {
|
||||||
state = state.copyWith(
|
state = state.copyWith(
|
||||||
searchResult: [], isError: false, isLoading: true, isSuccess: false);
|
searchResult: [],
|
||||||
|
isError: false,
|
||||||
|
isLoading: true,
|
||||||
|
isSuccess: false,
|
||||||
|
);
|
||||||
|
|
||||||
List<ImmichAsset>? assets = await _searchService.searchAsset(searchTerm);
|
List<AssetResponseDto>? assets =
|
||||||
|
await _searchService.searchAsset(searchTerm);
|
||||||
|
|
||||||
if (assets != null) {
|
if (assets != null) {
|
||||||
state = state.copyWith(
|
state = state.copyWith(
|
||||||
searchResult: assets,
|
searchResult: assets,
|
||||||
isError: false,
|
isError: false,
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
isSuccess: true);
|
isSuccess: true,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
state = state.copyWith(
|
state = state.copyWith(
|
||||||
searchResult: [], isError: true, isLoading: false, isSuccess: false);
|
searchResult: [],
|
||||||
|
isError: true,
|
||||||
|
isLoading: false,
|
||||||
|
isSuccess: false,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -48,7 +58,11 @@ final searchResultGroupByDateTimeProvider = StateProvider((ref) {
|
||||||
var assets = ref.watch(searchResultPageProvider).searchResult;
|
var assets = ref.watch(searchResultPageProvider).searchResult;
|
||||||
|
|
||||||
assets.sortByCompare<DateTime>(
|
assets.sortByCompare<DateTime>(
|
||||||
(e) => DateTime.parse(e.createdAt), (a, b) => b.compareTo(a));
|
(e) => DateTime.parse(e.createdAt),
|
||||||
return assets.groupListsBy((element) =>
|
(a, b) => b.compareTo(a),
|
||||||
DateFormat('y-MM-dd').format(DateTime.parse(element.createdAt)));
|
);
|
||||||
|
return assets.groupListsBy(
|
||||||
|
(element) =>
|
||||||
|
DateFormat('y-MM-dd').format(DateTime.parse(element.createdAt)),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,79 +1,54 @@
|
||||||
import 'dart:convert';
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/modules/search/models/curated_location.model.dart';
|
import 'package:immich_mobile/shared/services/api.service.dart';
|
||||||
import 'package:immich_mobile/modules/search/models/curated_object.model.dart';
|
import 'package:openapi/api.dart';
|
||||||
import 'package:immich_mobile/shared/models/immich_asset.model.dart';
|
|
||||||
import 'package:immich_mobile/shared/services/network.service.dart';
|
|
||||||
|
|
||||||
final searchServiceProvider =
|
final searchServiceProvider = Provider(
|
||||||
Provider((ref) => SearchService(ref.watch(networkServiceProvider)));
|
(ref) => SearchService(
|
||||||
|
ref.watch(apiServiceProvider),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
class SearchService {
|
class SearchService {
|
||||||
final NetworkService _networkService;
|
final ApiService _apiService;
|
||||||
SearchService(this._networkService);
|
SearchService(this._apiService);
|
||||||
|
|
||||||
Future<List<String>?> getUserSuggestedSearchTerms() async {
|
Future<List<String>?> getUserSuggestedSearchTerms() async {
|
||||||
try {
|
try {
|
||||||
var res = await _networkService.getRequest(url: "asset/searchTerm");
|
return await _apiService.assetApi.getAssetSearchTerms();
|
||||||
List<dynamic> decodedData = jsonDecode(res.toString());
|
|
||||||
|
|
||||||
return List.from(decodedData);
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint("[ERROR] [getUserSuggestedSearchTerms] ${e.toString()}");
|
debugPrint("[ERROR] [getUserSuggestedSearchTerms] ${e.toString()}");
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<ImmichAsset>?> searchAsset(String searchTerm) async {
|
Future<List<AssetResponseDto>?> searchAsset(String searchTerm) async {
|
||||||
try {
|
try {
|
||||||
var res = await _networkService.postRequest(
|
return await _apiService.assetApi
|
||||||
url: "asset/search",
|
.searchAsset(SearchAssetDto(searchTerm: searchTerm));
|
||||||
data: {"searchTerm": searchTerm},
|
|
||||||
);
|
|
||||||
|
|
||||||
List<dynamic> decodedData = jsonDecode(res.toString());
|
|
||||||
|
|
||||||
List<ImmichAsset> result =
|
|
||||||
List.from(decodedData.map((a) => ImmichAsset.fromMap(a)));
|
|
||||||
|
|
||||||
return result;
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint("[ERROR] [searchAsset] ${e.toString()}");
|
debugPrint("[ERROR] [searchAsset] ${e.toString()}");
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<CuratedLocation>?> getCuratedLocation() async {
|
Future<List<CuratedLocationsResponseDto>?> getCuratedLocation() async {
|
||||||
try {
|
try {
|
||||||
var res = await _networkService.getRequest(url: "asset/allLocation");
|
var locations = await _apiService.assetApi.getCuratedLocations();
|
||||||
|
|
||||||
List<dynamic> decodedData = jsonDecode(res.toString());
|
return locations;
|
||||||
|
|
||||||
List<CuratedLocation> result =
|
|
||||||
List.from(decodedData.map((a) => CuratedLocation.fromMap(a)));
|
|
||||||
|
|
||||||
return result;
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint("[ERROR] [getCuratedLocation] ${e.toString()}");
|
debugPrint("Error [getCuratedLocation] ${e.toString()}");
|
||||||
throw Error();
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<CuratedObject>?> getCuratedObjects() async {
|
Future<List<CuratedObjectsResponseDto>?> getCuratedObjects() async {
|
||||||
try {
|
try {
|
||||||
var res = await _networkService.getRequest(url: "asset/allObjects");
|
return await _apiService.assetApi.getCuratedObjects();
|
||||||
|
|
||||||
List<dynamic> decodedData = jsonDecode(res.toString());
|
|
||||||
|
|
||||||
List<CuratedObject> result =
|
|
||||||
List.from(decodedData.map((a) => CuratedObject.fromMap(a)));
|
|
||||||
|
|
||||||
return result;
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint("[ERROR] [CuratedObject] ${e.toString()}");
|
debugPrint("Error [getCuratedObjects] ${e.toString()}");
|
||||||
throw Error();
|
throw [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,9 +5,11 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/modules/search/providers/search_page_state.provider.dart';
|
import 'package:immich_mobile/modules/search/providers/search_page_state.provider.dart';
|
||||||
|
|
||||||
class SearchBar extends HookConsumerWidget with PreferredSizeWidget {
|
class SearchBar extends HookConsumerWidget with PreferredSizeWidget {
|
||||||
SearchBar(
|
SearchBar({
|
||||||
{Key? key, required this.searchFocusNode, required this.onSubmitted})
|
Key? key,
|
||||||
: super(key: key);
|
required this.searchFocusNode,
|
||||||
|
required this.onSubmitted,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
final FocusNode searchFocusNode;
|
final FocusNode searchFocusNode;
|
||||||
final Function(String) onSubmitted;
|
final Function(String) onSubmitted;
|
||||||
|
@ -26,7 +28,8 @@ class SearchBar extends HookConsumerWidget with PreferredSizeWidget {
|
||||||
ref.watch(searchPageStateProvider.notifier).disableSearch();
|
ref.watch(searchPageStateProvider.notifier).disableSearch();
|
||||||
searchTermController.clear();
|
searchTermController.clear();
|
||||||
},
|
},
|
||||||
icon: const Icon(Icons.arrow_back_ios_rounded))
|
icon: const Icon(Icons.arrow_back_ios_rounded),
|
||||||
|
)
|
||||||
: const Icon(Icons.search_rounded),
|
: const Icon(Icons.search_rounded),
|
||||||
title: TextField(
|
title: TextField(
|
||||||
controller: searchTermController,
|
controller: searchTermController,
|
||||||
|
@ -50,10 +53,10 @@ class SearchBar extends HookConsumerWidget with PreferredSizeWidget {
|
||||||
},
|
},
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
hintText: 'search_bar_hint'.tr(),
|
hintText: 'search_bar_hint'.tr(),
|
||||||
enabledBorder: UnderlineInputBorder(
|
enabledBorder: const UnderlineInputBorder(
|
||||||
borderSide: BorderSide(color: Colors.transparent),
|
borderSide: BorderSide(color: Colors.transparent),
|
||||||
),
|
),
|
||||||
focusedBorder: UnderlineInputBorder(
|
focusedBorder: const UnderlineInputBorder(
|
||||||
borderSide: BorderSide(color: Colors.transparent),
|
borderSide: BorderSide(color: Colors.transparent),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
@ -2,15 +2,14 @@ import 'package:cached_network_image/cached_network_image.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hive_flutter/hive_flutter.dart';
|
import 'package:hive_flutter/hive_flutter.dart';
|
||||||
import 'package:immich_mobile/constants/hive_box.dart';
|
import 'package:immich_mobile/constants/hive_box.dart';
|
||||||
import 'package:immich_mobile/utils/capitalize_first_letter.dart';
|
|
||||||
|
|
||||||
class ThumbnailWithInfo extends StatelessWidget {
|
class ThumbnailWithInfo extends StatelessWidget {
|
||||||
const ThumbnailWithInfo(
|
const ThumbnailWithInfo({
|
||||||
{Key? key,
|
Key? key,
|
||||||
required this.textInfo,
|
required this.textInfo,
|
||||||
required this.imageUrl,
|
required this.imageUrl,
|
||||||
required this.onTap})
|
required this.onTap,
|
||||||
: super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
final String textInfo;
|
final String textInfo;
|
||||||
final String imageUrl;
|
final String imageUrl;
|
||||||
|
|
|
@ -5,8 +5,6 @@ import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:hive_flutter/hive_flutter.dart';
|
import 'package:hive_flutter/hive_flutter.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/constants/hive_box.dart';
|
import 'package:immich_mobile/constants/hive_box.dart';
|
||||||
import 'package:immich_mobile/modules/search/models/curated_location.model.dart';
|
|
||||||
import 'package:immich_mobile/modules/search/models/curated_object.model.dart';
|
|
||||||
import 'package:immich_mobile/modules/search/providers/search_page_state.provider.dart';
|
import 'package:immich_mobile/modules/search/providers/search_page_state.provider.dart';
|
||||||
import 'package:immich_mobile/modules/search/ui/search_bar.dart';
|
import 'package:immich_mobile/modules/search/ui/search_bar.dart';
|
||||||
import 'package:immich_mobile/modules/search/ui/search_suggestion_list.dart';
|
import 'package:immich_mobile/modules/search/ui/search_suggestion_list.dart';
|
||||||
|
@ -14,6 +12,7 @@ import 'package:immich_mobile/modules/search/ui/thumbnail_with_info.dart';
|
||||||
import 'package:immich_mobile/routing/router.dart';
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
|
import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
|
||||||
import 'package:immich_mobile/utils/capitalize_first_letter.dart';
|
import 'package:immich_mobile/utils/capitalize_first_letter.dart';
|
||||||
|
import 'package:openapi/api.dart';
|
||||||
|
|
||||||
// ignore: must_be_immutable
|
// ignore: must_be_immutable
|
||||||
class SearchPage extends HookConsumerWidget {
|
class SearchPage extends HookConsumerWidget {
|
||||||
|
@ -25,15 +24,18 @@ class SearchPage extends HookConsumerWidget {
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
var box = Hive.box(userInfoBox);
|
var box = Hive.box(userInfoBox);
|
||||||
final isSearchEnabled = ref.watch(searchPageStateProvider).isSearchEnabled;
|
final isSearchEnabled = ref.watch(searchPageStateProvider).isSearchEnabled;
|
||||||
AsyncValue<List<CuratedLocation>> curatedLocation =
|
AsyncValue<List<CuratedLocationsResponseDto>> curatedLocation =
|
||||||
ref.watch(getCuratedLocationProvider);
|
ref.watch(getCuratedLocationProvider);
|
||||||
AsyncValue<List<CuratedObject>> curatedObjects =
|
AsyncValue<List<CuratedObjectsResponseDto>> curatedObjects =
|
||||||
ref.watch(getCuratedObjectProvider);
|
ref.watch(getCuratedObjectProvider);
|
||||||
|
|
||||||
useEffect(() {
|
useEffect(
|
||||||
searchFocusNode = FocusNode();
|
() {
|
||||||
return () => searchFocusNode.dispose();
|
searchFocusNode = FocusNode();
|
||||||
}, []);
|
return () => searchFocusNode.dispose();
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
_onSearchSubmitted(String searchTerm) async {
|
_onSearchSubmitted(String searchTerm) async {
|
||||||
searchFocusNode.unfocus();
|
searchFocusNode.unfocus();
|
||||||
|
@ -58,16 +60,16 @@ class SearchPage extends HookConsumerWidget {
|
||||||
scrollDirection: Axis.horizontal,
|
scrollDirection: Axis.horizontal,
|
||||||
itemCount: curatedLocation.value?.length,
|
itemCount: curatedLocation.value?.length,
|
||||||
itemBuilder: ((context, index) {
|
itemBuilder: ((context, index) {
|
||||||
CuratedLocation locationInfo = curatedLocations[index];
|
var locationInfo = curatedLocations[index];
|
||||||
var thumbnailRequestUrl =
|
var thumbnailRequestUrl =
|
||||||
'${box.get(serverEndpointKey)}/asset/file?aid=${locationInfo.deviceAssetId}&did=${locationInfo.deviceId}&isThumb=true';
|
'${box.get(serverEndpointKey)}/asset/thumbnail/${locationInfo.id}';
|
||||||
|
|
||||||
return ThumbnailWithInfo(
|
return ThumbnailWithInfo(
|
||||||
imageUrl: thumbnailRequestUrl,
|
imageUrl: thumbnailRequestUrl,
|
||||||
textInfo: locationInfo.city,
|
textInfo: locationInfo.city,
|
||||||
onTap: () {
|
onTap: () {
|
||||||
AutoRouter.of(context).push(
|
AutoRouter.of(context).push(
|
||||||
SearchResultRoute(searchTerm: locationInfo.city));
|
SearchResultRoute(searchTerm: locationInfo.city),
|
||||||
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
|
@ -109,7 +111,7 @@ class SearchPage extends HookConsumerWidget {
|
||||||
scrollDirection: Axis.horizontal,
|
scrollDirection: Axis.horizontal,
|
||||||
itemCount: curatedObjects.value?.length,
|
itemCount: curatedObjects.value?.length,
|
||||||
itemBuilder: ((context, index) {
|
itemBuilder: ((context, index) {
|
||||||
CuratedObject curatedObjectInfo = objects[index];
|
var curatedObjectInfo = objects[index];
|
||||||
var thumbnailRequestUrl =
|
var thumbnailRequestUrl =
|
||||||
'${box.get(serverEndpointKey)}/asset/file?aid=${curatedObjectInfo.deviceAssetId}&did=${curatedObjectInfo.deviceId}&isThumb=true';
|
'${box.get(serverEndpointKey)}/asset/file?aid=${curatedObjectInfo.deviceAssetId}&did=${curatedObjectInfo.deviceId}&isThumb=true';
|
||||||
|
|
||||||
|
@ -117,9 +119,12 @@ class SearchPage extends HookConsumerWidget {
|
||||||
imageUrl: thumbnailRequestUrl,
|
imageUrl: thumbnailRequestUrl,
|
||||||
textInfo: curatedObjectInfo.object,
|
textInfo: curatedObjectInfo.object,
|
||||||
onTap: () {
|
onTap: () {
|
||||||
AutoRouter.of(context).push(SearchResultRoute(
|
AutoRouter.of(context).push(
|
||||||
|
SearchResultRoute(
|
||||||
searchTerm: curatedObjectInfo.object
|
searchTerm: curatedObjectInfo.object
|
||||||
.capitalizeFirstLetter()));
|
.capitalizeFirstLetter(),
|
||||||
|
),
|
||||||
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
|
@ -160,7 +165,7 @@ class SearchPage extends HookConsumerWidget {
|
||||||
ListView(
|
ListView(
|
||||||
children: [
|
children: [
|
||||||
Padding(
|
Padding(
|
||||||
padding: EdgeInsets.all(16.0),
|
padding: const EdgeInsets.all(16.0),
|
||||||
child: const Text(
|
child: const Text(
|
||||||
"search_page_places",
|
"search_page_places",
|
||||||
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
|
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
|
||||||
|
@ -168,8 +173,8 @@ class SearchPage extends HookConsumerWidget {
|
||||||
),
|
),
|
||||||
_buildPlaces(),
|
_buildPlaces(),
|
||||||
Padding(
|
Padding(
|
||||||
padding: EdgeInsets.all(16.0),
|
padding: const EdgeInsets.all(16.0),
|
||||||
child: const Text(
|
child: const Text(
|
||||||
"search_page_things",
|
"search_page_things",
|
||||||
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
|
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
|
||||||
).tr(),
|
).tr(),
|
||||||
|
|
|
@ -29,13 +29,18 @@ class SearchResultPage extends HookConsumerWidget {
|
||||||
|
|
||||||
late FocusNode searchFocusNode;
|
late FocusNode searchFocusNode;
|
||||||
|
|
||||||
useEffect(() {
|
useEffect(
|
||||||
searchFocusNode = FocusNode();
|
() {
|
||||||
|
searchFocusNode = FocusNode();
|
||||||
|
|
||||||
Future.delayed(Duration.zero,
|
Future.delayed(
|
||||||
() => ref.read(searchResultPageProvider.notifier).search(searchTerm));
|
Duration.zero,
|
||||||
return () => searchFocusNode.dispose();
|
() => ref.read(searchResultPageProvider.notifier).search(searchTerm),
|
||||||
}, []);
|
);
|
||||||
|
return () => searchFocusNode.dispose();
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
_onSearchSubmitted(String newSearchTerm) {
|
_onSearchSubmitted(String newSearchTerm) {
|
||||||
debugPrint("Re-Search with $newSearchTerm");
|
debugPrint("Re-Search with $newSearchTerm");
|
||||||
|
@ -69,10 +74,10 @@ class SearchResultPage extends HookConsumerWidget {
|
||||||
},
|
},
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
hintText: 'search_result_page_new_search_hint'.tr(),
|
hintText: 'search_result_page_new_search_hint'.tr(),
|
||||||
enabledBorder: UnderlineInputBorder(
|
enabledBorder: const UnderlineInputBorder(
|
||||||
borderSide: BorderSide(color: Colors.transparent),
|
borderSide: BorderSide(color: Colors.transparent),
|
||||||
),
|
),
|
||||||
focusedBorder: UnderlineInputBorder(
|
focusedBorder: const UnderlineInputBorder(
|
||||||
borderSide: BorderSide(color: Colors.transparent),
|
borderSide: BorderSide(color: Colors.transparent),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -90,9 +95,10 @@ class SearchResultPage extends HookConsumerWidget {
|
||||||
Text(
|
Text(
|
||||||
currentSearchTerm.value,
|
currentSearchTerm.value,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: Theme.of(context).primaryColor,
|
color: Theme.of(context).primaryColor,
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
fontWeight: FontWeight.bold),
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
),
|
),
|
||||||
Icon(
|
Icon(
|
||||||
|
@ -116,9 +122,10 @@ class SearchResultPage extends HookConsumerWidget {
|
||||||
|
|
||||||
if (searchResultPageState.isLoading) {
|
if (searchResultPageState.isLoading) {
|
||||||
return Center(
|
return Center(
|
||||||
child: SpinKitDancingSquare(
|
child: SpinKitDancingSquare(
|
||||||
color: Theme.of(context).primaryColor,
|
color: Theme.of(context).primaryColor,
|
||||||
));
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (searchResultPageState.isSuccess) {
|
if (searchResultPageState.isSuccess) {
|
||||||
|
@ -184,11 +191,12 @@ class SearchResultPage extends HookConsumerWidget {
|
||||||
icon: const Icon(Icons.arrow_back_ios_rounded),
|
icon: const Icon(Icons.arrow_back_ios_rounded),
|
||||||
),
|
),
|
||||||
title: GestureDetector(
|
title: GestureDetector(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
isNewSearch.value = true;
|
isNewSearch.value = true;
|
||||||
searchFocusNode.requestFocus();
|
searchFocusNode.requestFocus();
|
||||||
},
|
},
|
||||||
child: isNewSearch.value ? _buildTextField() : _buildChip()),
|
child: isNewSearch.value ? _buildTextField() : _buildChip(),
|
||||||
|
),
|
||||||
centerTitle: false,
|
centerTitle: false,
|
||||||
),
|
),
|
||||||
body: GestureDetector(
|
body: GestureDetector(
|
||||||
|
|
|
@ -1,12 +1,10 @@
|
||||||
import 'dart:convert';
|
|
||||||
|
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
|
|
||||||
import 'package:immich_mobile/shared/models/immich_asset.model.dart';
|
import 'package:openapi/api.dart';
|
||||||
|
|
||||||
class AssetSelectionPageResult {
|
class AssetSelectionPageResult {
|
||||||
final Set<ImmichAsset> selectedNewAsset;
|
final Set<AssetResponseDto> selectedNewAsset;
|
||||||
final Set<ImmichAsset> selectedAdditionalAsset;
|
final Set<AssetResponseDto> selectedAdditionalAsset;
|
||||||
final bool isAlbumExist;
|
final bool isAlbumExist;
|
||||||
|
|
||||||
AssetSelectionPageResult({
|
AssetSelectionPageResult({
|
||||||
|
@ -16,8 +14,8 @@ class AssetSelectionPageResult {
|
||||||
});
|
});
|
||||||
|
|
||||||
AssetSelectionPageResult copyWith({
|
AssetSelectionPageResult copyWith({
|
||||||
Set<ImmichAsset>? selectedNewAsset,
|
Set<AssetResponseDto>? selectedNewAsset,
|
||||||
Set<ImmichAsset>? selectedAdditionalAsset,
|
Set<AssetResponseDto>? selectedAdditionalAsset,
|
||||||
bool? isAlbumExist,
|
bool? isAlbumExist,
|
||||||
}) {
|
}) {
|
||||||
return AssetSelectionPageResult(
|
return AssetSelectionPageResult(
|
||||||
|
@ -28,35 +26,6 @@ class AssetSelectionPageResult {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<String, dynamic> toMap() {
|
|
||||||
final result = <String, dynamic>{};
|
|
||||||
|
|
||||||
result.addAll(
|
|
||||||
{'selectedNewAsset': selectedNewAsset.map((x) => x.toMap()).toList()});
|
|
||||||
result.addAll({
|
|
||||||
'selectedAdditionalAsset':
|
|
||||||
selectedAdditionalAsset.map((x) => x.toMap()).toList()
|
|
||||||
});
|
|
||||||
result.addAll({'isAlbumExist': isAlbumExist});
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
factory AssetSelectionPageResult.fromMap(Map<String, dynamic> map) {
|
|
||||||
return AssetSelectionPageResult(
|
|
||||||
selectedNewAsset: Set<ImmichAsset>.from(
|
|
||||||
map['selectedNewAsset']?.map((x) => ImmichAsset.fromMap(x))),
|
|
||||||
selectedAdditionalAsset: Set<ImmichAsset>.from(
|
|
||||||
map['selectedAdditionalAsset']?.map((x) => ImmichAsset.fromMap(x))),
|
|
||||||
isAlbumExist: map['isAlbumExist'] ?? false,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
String toJson() => json.encode(toMap());
|
|
||||||
|
|
||||||
factory AssetSelectionPageResult.fromJson(String source) =>
|
|
||||||
AssetSelectionPageResult.fromMap(json.decode(source));
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() =>
|
String toString() =>
|
||||||
'AssetSelectionPageResult(selectedNewAsset: $selectedNewAsset, selectedAdditionalAsset: $selectedAdditionalAsset, isAlbumExist: $isAlbumExist)';
|
'AssetSelectionPageResult(selectedNewAsset: $selectedNewAsset, selectedAdditionalAsset: $selectedAdditionalAsset, isAlbumExist: $isAlbumExist)';
|
||||||
|
|
|
@ -1,14 +1,12 @@
|
||||||
import 'dart:convert';
|
|
||||||
|
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
|
|
||||||
import 'package:immich_mobile/shared/models/immich_asset.model.dart';
|
import 'package:openapi/api.dart';
|
||||||
|
|
||||||
class AssetSelectionState {
|
class AssetSelectionState {
|
||||||
final Set<String> selectedMonths;
|
final Set<String> selectedMonths;
|
||||||
final Set<ImmichAsset> selectedNewAssetsForAlbum;
|
final Set<AssetResponseDto> selectedNewAssetsForAlbum;
|
||||||
final Set<ImmichAsset> selectedAdditionalAssetsForAlbum;
|
final Set<AssetResponseDto> selectedAdditionalAssetsForAlbum;
|
||||||
final Set<ImmichAsset> selectedAssetsInAlbumViewer;
|
final Set<AssetResponseDto> selectedAssetsInAlbumViewer;
|
||||||
final bool isMultiselectEnable;
|
final bool isMultiselectEnable;
|
||||||
|
|
||||||
/// Indicate the asset selection page is navigated from existing album
|
/// Indicate the asset selection page is navigated from existing album
|
||||||
|
@ -24,9 +22,9 @@ class AssetSelectionState {
|
||||||
|
|
||||||
AssetSelectionState copyWith({
|
AssetSelectionState copyWith({
|
||||||
Set<String>? selectedMonths,
|
Set<String>? selectedMonths,
|
||||||
Set<ImmichAsset>? selectedNewAssetsForAlbum,
|
Set<AssetResponseDto>? selectedNewAssetsForAlbum,
|
||||||
Set<ImmichAsset>? selectedAdditionalAssetsForAlbum,
|
Set<AssetResponseDto>? selectedAdditionalAssetsForAlbum,
|
||||||
Set<ImmichAsset>? selectedAssetsInAlbumViewer,
|
Set<AssetResponseDto>? selectedAssetsInAlbumViewer,
|
||||||
bool? isMultiselectEnable,
|
bool? isMultiselectEnable,
|
||||||
bool? isAlbumExist,
|
bool? isAlbumExist,
|
||||||
}) {
|
}) {
|
||||||
|
@ -43,49 +41,6 @@ class AssetSelectionState {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<String, dynamic> toMap() {
|
|
||||||
final result = <String, dynamic>{};
|
|
||||||
|
|
||||||
result.addAll({'selectedMonths': selectedMonths.toList()});
|
|
||||||
result.addAll({
|
|
||||||
'selectedNewAssetsForAlbum':
|
|
||||||
selectedNewAssetsForAlbum.map((x) => x.toMap()).toList()
|
|
||||||
});
|
|
||||||
result.addAll({
|
|
||||||
'selectedAdditionalAssetsForAlbum':
|
|
||||||
selectedAdditionalAssetsForAlbum.map((x) => x.toMap()).toList()
|
|
||||||
});
|
|
||||||
result.addAll({
|
|
||||||
'selectedAssetsInAlbumViewer':
|
|
||||||
selectedAssetsInAlbumViewer.map((x) => x.toMap()).toList()
|
|
||||||
});
|
|
||||||
result.addAll({'isMultiselectEnable': isMultiselectEnable});
|
|
||||||
result.addAll({'isAlbumExist': isAlbumExist});
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
factory AssetSelectionState.fromMap(Map<String, dynamic> map) {
|
|
||||||
return AssetSelectionState(
|
|
||||||
selectedMonths: Set<String>.from(map['selectedMonths']),
|
|
||||||
selectedNewAssetsForAlbum: Set<ImmichAsset>.from(
|
|
||||||
map['selectedNewAssetsForAlbum']?.map((x) => ImmichAsset.fromMap(x))),
|
|
||||||
selectedAdditionalAssetsForAlbum: Set<ImmichAsset>.from(
|
|
||||||
map['selectedAdditionalAssetsForAlbum']
|
|
||||||
?.map((x) => ImmichAsset.fromMap(x))),
|
|
||||||
selectedAssetsInAlbumViewer: Set<ImmichAsset>.from(
|
|
||||||
map['selectedAssetsInAlbumViewer']
|
|
||||||
?.map((x) => ImmichAsset.fromMap(x))),
|
|
||||||
isMultiselectEnable: map['isMultiselectEnable'] ?? false,
|
|
||||||
isAlbumExist: map['isAlbumExist'] ?? false,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
String toJson() => json.encode(toMap());
|
|
||||||
|
|
||||||
factory AssetSelectionState.fromJson(String source) =>
|
|
||||||
AssetSelectionState.fromMap(json.decode(source));
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return 'AssetSelectionState(selectedMonths: $selectedMonths, selectedNewAssetsForAlbum: $selectedNewAssetsForAlbum, selectedAdditionalAssetsForAlbum: $selectedAdditionalAssetsForAlbum, selectedAssetsInAlbumViewer: $selectedAssetsInAlbumViewer, isMultiselectEnable: $isMultiselectEnable, isAlbumExist: $isAlbumExist)';
|
return 'AssetSelectionState(selectedMonths: $selectedMonths, selectedNewAssetsForAlbum: $selectedNewAssetsForAlbum, selectedAdditionalAssetsForAlbum: $selectedAdditionalAssetsForAlbum, selectedAssetsInAlbumViewer: $selectedAssetsInAlbumViewer, isMultiselectEnable: $isMultiselectEnable, isAlbumExist: $isAlbumExist)';
|
||||||
|
@ -99,10 +54,14 @@ class AssetSelectionState {
|
||||||
return other is AssetSelectionState &&
|
return other is AssetSelectionState &&
|
||||||
setEquals(other.selectedMonths, selectedMonths) &&
|
setEquals(other.selectedMonths, selectedMonths) &&
|
||||||
setEquals(other.selectedNewAssetsForAlbum, selectedNewAssetsForAlbum) &&
|
setEquals(other.selectedNewAssetsForAlbum, selectedNewAssetsForAlbum) &&
|
||||||
setEquals(other.selectedAdditionalAssetsForAlbum,
|
|
||||||
selectedAdditionalAssetsForAlbum) &&
|
|
||||||
setEquals(
|
setEquals(
|
||||||
other.selectedAssetsInAlbumViewer, selectedAssetsInAlbumViewer) &&
|
other.selectedAdditionalAssetsForAlbum,
|
||||||
|
selectedAdditionalAssetsForAlbum,
|
||||||
|
) &&
|
||||||
|
setEquals(
|
||||||
|
other.selectedAssetsInAlbumViewer,
|
||||||
|
selectedAssetsInAlbumViewer,
|
||||||
|
) &&
|
||||||
other.isMultiselectEnable == isMultiselectEnable &&
|
other.isMultiselectEnable == isMultiselectEnable &&
|
||||||
other.isAlbumExist == isAlbumExist;
|
other.isAlbumExist == isAlbumExist;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,117 +0,0 @@
|
||||||
import 'dart:convert';
|
|
||||||
|
|
||||||
import 'package:collection/collection.dart';
|
|
||||||
|
|
||||||
import 'package:immich_mobile/shared/models/immich_asset.model.dart';
|
|
||||||
import 'package:immich_mobile/shared/models/user.model.dart';
|
|
||||||
|
|
||||||
class SharedAlbum {
|
|
||||||
final String id;
|
|
||||||
final String ownerId;
|
|
||||||
final String albumName;
|
|
||||||
final String createdAt;
|
|
||||||
final String? albumThumbnailAssetId;
|
|
||||||
final List<User> sharedUsers;
|
|
||||||
final List<ImmichAsset>? assets;
|
|
||||||
|
|
||||||
SharedAlbum({
|
|
||||||
required this.id,
|
|
||||||
required this.ownerId,
|
|
||||||
required this.albumName,
|
|
||||||
required this.createdAt,
|
|
||||||
required this.albumThumbnailAssetId,
|
|
||||||
required this.sharedUsers,
|
|
||||||
this.assets,
|
|
||||||
});
|
|
||||||
|
|
||||||
SharedAlbum copyWith({
|
|
||||||
String? id,
|
|
||||||
String? ownerId,
|
|
||||||
String? albumName,
|
|
||||||
String? createdAt,
|
|
||||||
String? albumThumbnailAssetId,
|
|
||||||
List<User>? sharedUsers,
|
|
||||||
List<ImmichAsset>? assets,
|
|
||||||
}) {
|
|
||||||
return SharedAlbum(
|
|
||||||
id: id ?? this.id,
|
|
||||||
ownerId: ownerId ?? this.ownerId,
|
|
||||||
albumName: albumName ?? this.albumName,
|
|
||||||
createdAt: createdAt ?? this.createdAt,
|
|
||||||
albumThumbnailAssetId:
|
|
||||||
albumThumbnailAssetId ?? this.albumThumbnailAssetId,
|
|
||||||
sharedUsers: sharedUsers ?? this.sharedUsers,
|
|
||||||
assets: assets ?? this.assets,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Map<String, dynamic> toMap() {
|
|
||||||
final result = <String, dynamic>{};
|
|
||||||
|
|
||||||
result.addAll({'id': id});
|
|
||||||
result.addAll({'ownerId': ownerId});
|
|
||||||
result.addAll({'albumName': albumName});
|
|
||||||
result.addAll({'createdAt': createdAt});
|
|
||||||
if (albumThumbnailAssetId != null) {
|
|
||||||
result.addAll({'albumThumbnailAssetId': albumThumbnailAssetId});
|
|
||||||
}
|
|
||||||
result.addAll({'sharedUsers': sharedUsers.map((x) => x.toMap()).toList()});
|
|
||||||
if (assets != null) {
|
|
||||||
result.addAll({'assets': assets!.map((x) => x.toMap()).toList()});
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
factory SharedAlbum.fromMap(Map<String, dynamic> map) {
|
|
||||||
return SharedAlbum(
|
|
||||||
id: map['id'] ?? '',
|
|
||||||
ownerId: map['ownerId'] ?? '',
|
|
||||||
albumName: map['albumName'] ?? '',
|
|
||||||
createdAt: map['createdAt'] ?? '',
|
|
||||||
albumThumbnailAssetId: map['albumThumbnailAssetId'],
|
|
||||||
sharedUsers:
|
|
||||||
List<User>.from(map['sharedUsers']?.map((x) => User.fromMap(x))),
|
|
||||||
assets: map['assets'] != null
|
|
||||||
? List<ImmichAsset>.from(
|
|
||||||
map['assets']?.map((x) => ImmichAsset.fromMap(x)))
|
|
||||||
: null,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
String toJson() => json.encode(toMap());
|
|
||||||
|
|
||||||
factory SharedAlbum.fromJson(String source) =>
|
|
||||||
SharedAlbum.fromMap(json.decode(source));
|
|
||||||
|
|
||||||
@override
|
|
||||||
String toString() {
|
|
||||||
return 'SharedAlbum(id: $id, ownerId: $ownerId, albumName: $albumName, createdAt: $createdAt, albumThumbnailAssetId: $albumThumbnailAssetId, sharedUsers: $sharedUsers, assets: $assets)';
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool operator ==(Object other) {
|
|
||||||
if (identical(this, other)) return true;
|
|
||||||
final listEquals = const DeepCollectionEquality().equals;
|
|
||||||
|
|
||||||
return other is SharedAlbum &&
|
|
||||||
other.id == id &&
|
|
||||||
other.ownerId == ownerId &&
|
|
||||||
other.albumName == albumName &&
|
|
||||||
other.createdAt == createdAt &&
|
|
||||||
other.albumThumbnailAssetId == albumThumbnailAssetId &&
|
|
||||||
listEquals(other.sharedUsers, sharedUsers) &&
|
|
||||||
listEquals(other.assets, assets);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
int get hashCode {
|
|
||||||
return id.hashCode ^
|
|
||||||
ownerId.hashCode ^
|
|
||||||
albumName.hashCode ^
|
|
||||||
createdAt.hashCode ^
|
|
||||||
albumThumbnailAssetId.hashCode ^
|
|
||||||
sharedUsers.hashCode ^
|
|
||||||
assets.hashCode;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -13,4 +13,5 @@ class AlbumTitleNotifier extends StateNotifier<String> {
|
||||||
}
|
}
|
||||||
|
|
||||||
final albumTitleProvider = StateNotifierProvider<AlbumTitleNotifier, String>(
|
final albumTitleProvider = StateNotifierProvider<AlbumTitleNotifier, String>(
|
||||||
(ref) => AlbumTitleNotifier());
|
(ref) => AlbumTitleNotifier(),
|
||||||
|
);
|
||||||
|
|
|
@ -30,7 +30,10 @@ class AlbumViewerNotifier extends StateNotifier<AlbumViewerPageState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> changeAlbumTitle(
|
Future<bool> changeAlbumTitle(
|
||||||
String albumId, String ownerId, String newAlbumTitle) async {
|
String albumId,
|
||||||
|
String ownerId,
|
||||||
|
String newAlbumTitle,
|
||||||
|
) async {
|
||||||
SharedAlbumService service = ref.watch(sharedAlbumServiceProvider);
|
SharedAlbumService service = ref.watch(sharedAlbumServiceProvider);
|
||||||
|
|
||||||
bool isSuccess =
|
bool isSuccess =
|
||||||
|
|
|
@ -1,41 +1,46 @@
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/modules/sharing/models/asset_selection_state.model.dart';
|
import 'package:immich_mobile/modules/sharing/models/asset_selection_state.model.dart';
|
||||||
|
|
||||||
import 'package:immich_mobile/shared/models/immich_asset.model.dart';
|
import 'package:openapi/api.dart';
|
||||||
|
|
||||||
class AssetSelectionNotifier extends StateNotifier<AssetSelectionState> {
|
class AssetSelectionNotifier extends StateNotifier<AssetSelectionState> {
|
||||||
AssetSelectionNotifier()
|
AssetSelectionNotifier()
|
||||||
: super(AssetSelectionState(
|
: super(
|
||||||
selectedNewAssetsForAlbum: {},
|
AssetSelectionState(
|
||||||
selectedMonths: {},
|
selectedNewAssetsForAlbum: {},
|
||||||
selectedAdditionalAssetsForAlbum: {},
|
selectedMonths: {},
|
||||||
selectedAssetsInAlbumViewer: {},
|
selectedAdditionalAssetsForAlbum: {},
|
||||||
isAlbumExist: false,
|
selectedAssetsInAlbumViewer: {},
|
||||||
isMultiselectEnable: false,
|
isAlbumExist: false,
|
||||||
));
|
isMultiselectEnable: false,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
void setIsAlbumExist(bool isAlbumExist) {
|
void setIsAlbumExist(bool isAlbumExist) {
|
||||||
state = state.copyWith(isAlbumExist: isAlbumExist);
|
state = state.copyWith(isAlbumExist: isAlbumExist);
|
||||||
}
|
}
|
||||||
|
|
||||||
void removeAssetsInMonth(
|
void removeAssetsInMonth(
|
||||||
String removedMonth, List<ImmichAsset> assetsInMonth) {
|
String removedMonth,
|
||||||
Set<ImmichAsset> currentAssetList = state.selectedNewAssetsForAlbum;
|
List<AssetResponseDto> assetsInMonth,
|
||||||
|
) {
|
||||||
|
Set<AssetResponseDto> currentAssetList = state.selectedNewAssetsForAlbum;
|
||||||
Set<String> currentMonthList = state.selectedMonths;
|
Set<String> currentMonthList = state.selectedMonths;
|
||||||
|
|
||||||
currentMonthList
|
currentMonthList
|
||||||
.removeWhere((selectedMonth) => selectedMonth == removedMonth);
|
.removeWhere((selectedMonth) => selectedMonth == removedMonth);
|
||||||
|
|
||||||
for (ImmichAsset asset in assetsInMonth) {
|
for (AssetResponseDto asset in assetsInMonth) {
|
||||||
currentAssetList.removeWhere((e) => e.id == asset.id);
|
currentAssetList.removeWhere((e) => e.id == asset.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
state = state.copyWith(
|
state = state.copyWith(
|
||||||
selectedNewAssetsForAlbum: currentAssetList,
|
selectedNewAssetsForAlbum: currentAssetList,
|
||||||
selectedMonths: currentMonthList);
|
selectedMonths: currentMonthList,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void addAdditionalAssets(List<ImmichAsset> assets) {
|
void addAdditionalAssets(List<AssetResponseDto> assets) {
|
||||||
state = state.copyWith(
|
state = state.copyWith(
|
||||||
selectedAdditionalAssetsForAlbum: {
|
selectedAdditionalAssetsForAlbum: {
|
||||||
...state.selectedAdditionalAssetsForAlbum,
|
...state.selectedAdditionalAssetsForAlbum,
|
||||||
|
@ -44,7 +49,7 @@ class AssetSelectionNotifier extends StateNotifier<AssetSelectionState> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void addAllAssetsInMonth(String month, List<ImmichAsset> assetsInMonth) {
|
void addAllAssetsInMonth(String month, List<AssetResponseDto> assetsInMonth) {
|
||||||
state = state.copyWith(
|
state = state.copyWith(
|
||||||
selectedMonths: {...state.selectedMonths, month},
|
selectedMonths: {...state.selectedMonths, month},
|
||||||
selectedNewAssetsForAlbum: {
|
selectedNewAssetsForAlbum: {
|
||||||
|
@ -54,7 +59,7 @@ class AssetSelectionNotifier extends StateNotifier<AssetSelectionState> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void addNewAssets(List<ImmichAsset> assets) {
|
void addNewAssets(List<AssetResponseDto> assets) {
|
||||||
state = state.copyWith(
|
state = state.copyWith(
|
||||||
selectedNewAssetsForAlbum: {
|
selectedNewAssetsForAlbum: {
|
||||||
...state.selectedNewAssetsForAlbum,
|
...state.selectedNewAssetsForAlbum,
|
||||||
|
@ -63,20 +68,20 @@ class AssetSelectionNotifier extends StateNotifier<AssetSelectionState> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void removeSelectedNewAssets(List<ImmichAsset> assets) {
|
void removeSelectedNewAssets(List<AssetResponseDto> assets) {
|
||||||
Set<ImmichAsset> currentList = state.selectedNewAssetsForAlbum;
|
Set<AssetResponseDto> currentList = state.selectedNewAssetsForAlbum;
|
||||||
|
|
||||||
for (ImmichAsset asset in assets) {
|
for (AssetResponseDto asset in assets) {
|
||||||
currentList.removeWhere((e) => e.id == asset.id);
|
currentList.removeWhere((e) => e.id == asset.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
state = state.copyWith(selectedNewAssetsForAlbum: currentList);
|
state = state.copyWith(selectedNewAssetsForAlbum: currentList);
|
||||||
}
|
}
|
||||||
|
|
||||||
void removeSelectedAdditionalAssets(List<ImmichAsset> assets) {
|
void removeSelectedAdditionalAssets(List<AssetResponseDto> assets) {
|
||||||
Set<ImmichAsset> currentList = state.selectedAdditionalAssetsForAlbum;
|
Set<AssetResponseDto> currentList = state.selectedAdditionalAssetsForAlbum;
|
||||||
|
|
||||||
for (ImmichAsset asset in assets) {
|
for (AssetResponseDto asset in assets) {
|
||||||
currentList.removeWhere((e) => e.id == asset.id);
|
currentList.removeWhere((e) => e.id == asset.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -104,7 +109,7 @@ class AssetSelectionNotifier extends StateNotifier<AssetSelectionState> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void addAssetsInAlbumViewer(List<ImmichAsset> assets) {
|
void addAssetsInAlbumViewer(List<AssetResponseDto> assets) {
|
||||||
state = state.copyWith(
|
state = state.copyWith(
|
||||||
selectedAssetsInAlbumViewer: {
|
selectedAssetsInAlbumViewer: {
|
||||||
...state.selectedAssetsInAlbumViewer,
|
...state.selectedAssetsInAlbumViewer,
|
||||||
|
@ -113,10 +118,10 @@ class AssetSelectionNotifier extends StateNotifier<AssetSelectionState> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void removeAssetsInAlbumViewer(List<ImmichAsset> assets) {
|
void removeAssetsInAlbumViewer(List<AssetResponseDto> assets) {
|
||||||
Set<ImmichAsset> currentList = state.selectedAssetsInAlbumViewer;
|
Set<AssetResponseDto> currentList = state.selectedAssetsInAlbumViewer;
|
||||||
|
|
||||||
for (ImmichAsset asset in assets) {
|
for (AssetResponseDto asset in assets) {
|
||||||
currentList.removeWhere((e) => e.id == asset.id);
|
currentList.removeWhere((e) => e.id == asset.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,17 +1,19 @@
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/modules/sharing/models/shared_album.model.dart';
|
|
||||||
import 'package:immich_mobile/modules/sharing/services/shared_album.service.dart';
|
import 'package:immich_mobile/modules/sharing/services/shared_album.service.dart';
|
||||||
|
import 'package:openapi/api.dart';
|
||||||
|
|
||||||
class SharedAlbumNotifier extends StateNotifier<List<SharedAlbum>> {
|
class SharedAlbumNotifier extends StateNotifier<List<AlbumResponseDto>> {
|
||||||
SharedAlbumNotifier(this._sharedAlbumService) : super([]);
|
SharedAlbumNotifier(this._sharedAlbumService) : super([]);
|
||||||
|
|
||||||
final SharedAlbumService _sharedAlbumService;
|
final SharedAlbumService _sharedAlbumService;
|
||||||
|
|
||||||
getAllSharedAlbums() async {
|
getAllSharedAlbums() async {
|
||||||
List<SharedAlbum> sharedAlbums =
|
List<AlbumResponseDto>? sharedAlbums =
|
||||||
await _sharedAlbumService.getAllSharedAlbum();
|
await _sharedAlbumService.getAllSharedAlbum();
|
||||||
|
|
||||||
state = sharedAlbums;
|
if (sharedAlbums != null) {
|
||||||
|
state = sharedAlbums;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> deleteAlbum(String albumId) async {
|
Future<bool> deleteAlbum(String albumId) async {
|
||||||
|
@ -37,7 +39,9 @@ class SharedAlbumNotifier extends StateNotifier<List<SharedAlbum>> {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> removeAssetFromAlbum(
|
Future<bool> removeAssetFromAlbum(
|
||||||
String albumId, List<String> assetIds) async {
|
String albumId,
|
||||||
|
List<String> assetIds,
|
||||||
|
) async {
|
||||||
var res = await _sharedAlbumService.removeAssetFromAlbum(albumId, assetIds);
|
var res = await _sharedAlbumService.removeAssetFromAlbum(albumId, assetIds);
|
||||||
|
|
||||||
if (res) {
|
if (res) {
|
||||||
|
@ -49,12 +53,12 @@ class SharedAlbumNotifier extends StateNotifier<List<SharedAlbum>> {
|
||||||
}
|
}
|
||||||
|
|
||||||
final sharedAlbumProvider =
|
final sharedAlbumProvider =
|
||||||
StateNotifierProvider<SharedAlbumNotifier, List<SharedAlbum>>((ref) {
|
StateNotifierProvider<SharedAlbumNotifier, List<AlbumResponseDto>>((ref) {
|
||||||
return SharedAlbumNotifier(ref.watch(sharedAlbumServiceProvider));
|
return SharedAlbumNotifier(ref.watch(sharedAlbumServiceProvider));
|
||||||
});
|
});
|
||||||
|
|
||||||
final sharedAlbumDetailProvider = FutureProvider.autoDispose
|
final sharedAlbumDetailProvider = FutureProvider.autoDispose
|
||||||
.family<SharedAlbum, String>((ref, albumId) async {
|
.family<AlbumResponseDto?, String>((ref, albumId) async {
|
||||||
final SharedAlbumService sharedAlbumService =
|
final SharedAlbumService sharedAlbumService =
|
||||||
ref.watch(sharedAlbumServiceProvider);
|
ref.watch(sharedAlbumServiceProvider);
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/shared/models/user.model.dart';
|
|
||||||
import 'package:immich_mobile/shared/services/user.service.dart';
|
import 'package:immich_mobile/shared/services/user.service.dart';
|
||||||
|
import 'package:openapi/api.dart';
|
||||||
|
|
||||||
final suggestedSharedUsersProvider =
|
final suggestedSharedUsersProvider =
|
||||||
FutureProvider.autoDispose<List<User>>((ref) async {
|
FutureProvider.autoDispose<List<UserResponseDto>>((ref) async {
|
||||||
UserService userService = ref.watch(userServiceProvider);
|
UserService userService = ref.watch(userServiceProvider);
|
||||||
|
|
||||||
return await userService.getAllUsersInfo();
|
return await userService.getAllUsersInfo(isAll: false) ?? [];
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,73 +1,69 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:convert';
|
|
||||||
|
|
||||||
import 'package:dio/dio.dart';
|
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/modules/sharing/models/shared_album.model.dart';
|
import 'package:immich_mobile/shared/services/api.service.dart';
|
||||||
import 'package:immich_mobile/shared/models/immich_asset.model.dart';
|
import 'package:openapi/api.dart';
|
||||||
import 'package:immich_mobile/shared/services/network.service.dart';
|
|
||||||
|
|
||||||
final sharedAlbumServiceProvider =
|
final sharedAlbumServiceProvider = Provider(
|
||||||
Provider((ref) => SharedAlbumService(ref.watch(networkServiceProvider)));
|
(ref) => SharedAlbumService(
|
||||||
|
ref.watch(apiServiceProvider),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
class SharedAlbumService {
|
class SharedAlbumService {
|
||||||
final NetworkService _networkService;
|
final ApiService _apiService;
|
||||||
SharedAlbumService(this._networkService);
|
SharedAlbumService(this._apiService);
|
||||||
|
|
||||||
Future<List<SharedAlbum>> getAllSharedAlbum() async {
|
Future<List<AlbumResponseDto>?> getAllSharedAlbum() async {
|
||||||
try {
|
try {
|
||||||
var res = await _networkService.getRequest(url: 'album?shared=true');
|
return await _apiService.albumApi.getAllAlbums(shared: true);
|
||||||
List<dynamic> decodedData = jsonDecode(res.toString());
|
|
||||||
List<SharedAlbum> result =
|
|
||||||
List.from(decodedData.map((e) => SharedAlbum.fromMap(e)));
|
|
||||||
|
|
||||||
return result;
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint("Error getAllSharedAlbum ${e.toString()}");
|
debugPrint("Error getAllSharedAlbum ${e.toString()}");
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return [];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> createSharedAlbum(String albumName, Set<ImmichAsset> assets,
|
Future<bool> createSharedAlbum(
|
||||||
List<String> sharedUserIds) async {
|
String albumName,
|
||||||
|
Set<AssetResponseDto> assets,
|
||||||
|
List<String> sharedUserIds,
|
||||||
|
) async {
|
||||||
try {
|
try {
|
||||||
var res = await _networkService.postRequest(url: 'album', data: {
|
_apiService.albumApi.createAlbum(
|
||||||
"albumName": albumName,
|
CreateAlbumDto(
|
||||||
"sharedWithUserIds": sharedUserIds,
|
albumName: albumName,
|
||||||
"assetIds": assets.map((asset) => asset.id).toList(),
|
assetIds: assets.map((asset) => asset.id).toList(),
|
||||||
});
|
sharedWithUserIds: sharedUserIds,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
return res != null;
|
return true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint("Error createSharedAlbum ${e.toString()}");
|
debugPrint("Error createSharedAlbum ${e.toString()}");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<SharedAlbum> getAlbumDetail(String albumId) async {
|
Future<AlbumResponseDto?> getAlbumDetail(String albumId) async {
|
||||||
try {
|
try {
|
||||||
var res = await _networkService.getRequest(url: 'album/$albumId');
|
return await _apiService.albumApi.getAlbumInfo(albumId);
|
||||||
dynamic decodedData = jsonDecode(res.toString());
|
|
||||||
SharedAlbum result = SharedAlbum.fromMap(decodedData);
|
|
||||||
|
|
||||||
return result;
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw Exception('Error getAllSharedAlbum ${e.toString()}');
|
debugPrint('Error [getAlbumDetail] ${e.toString()}');
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> addAdditionalAssetToAlbum(
|
Future<bool> addAdditionalAssetToAlbum(
|
||||||
Set<ImmichAsset> assets, String albumId) async {
|
Set<AssetResponseDto> assets,
|
||||||
|
String albumId,
|
||||||
|
) async {
|
||||||
try {
|
try {
|
||||||
var res =
|
var result = await _apiService.albumApi.addAssetsToAlbum(
|
||||||
await _networkService.putRequest(url: 'album/$albumId/assets', data: {
|
albumId,
|
||||||
"albumId": albumId,
|
AddAssetsDto(assetIds: assets.map((asset) => asset.id).toList()),
|
||||||
"assetIds": assets.map((asset) => asset.id).toList(),
|
);
|
||||||
});
|
return result != null;
|
||||||
|
|
||||||
return res != null;
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint("Error addAdditionalAssetToAlbum ${e.toString()}");
|
debugPrint("Error addAdditionalAssetToAlbum ${e.toString()}");
|
||||||
return false;
|
return false;
|
||||||
|
@ -75,14 +71,16 @@ class SharedAlbumService {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> addAdditionalUserToAlbum(
|
Future<bool> addAdditionalUserToAlbum(
|
||||||
List<String> sharedUserIds, String albumId) async {
|
List<String> sharedUserIds,
|
||||||
|
String albumId,
|
||||||
|
) async {
|
||||||
try {
|
try {
|
||||||
var res =
|
var result = await _apiService.albumApi.addUsersToAlbum(
|
||||||
await _networkService.putRequest(url: 'album/$albumId/users', data: {
|
albumId,
|
||||||
"sharedUserIds": sharedUserIds,
|
AddUsersDto(sharedUserIds: sharedUserIds),
|
||||||
});
|
);
|
||||||
|
|
||||||
return res != null;
|
return result != null;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint("Error addAdditionalUserToAlbum ${e.toString()}");
|
debugPrint("Error addAdditionalUserToAlbum ${e.toString()}");
|
||||||
return false;
|
return false;
|
||||||
|
@ -91,12 +89,7 @@ class SharedAlbumService {
|
||||||
|
|
||||||
Future<bool> deleteAlbum(String albumId) async {
|
Future<bool> deleteAlbum(String albumId) async {
|
||||||
try {
|
try {
|
||||||
Response res = await _networkService.deleteRequest(url: 'album/$albumId');
|
await _apiService.albumApi.deleteAlbum(albumId);
|
||||||
|
|
||||||
if (res.statusCode != 200) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint("Error deleteAlbum ${e.toString()}");
|
debugPrint("Error deleteAlbum ${e.toString()}");
|
||||||
|
@ -106,12 +99,7 @@ class SharedAlbumService {
|
||||||
|
|
||||||
Future<bool> leaveAlbum(String albumId) async {
|
Future<bool> leaveAlbum(String albumId) async {
|
||||||
try {
|
try {
|
||||||
Response res =
|
await _apiService.albumApi.removeUserFromAlbum(albumId, "me");
|
||||||
await _networkService.deleteRequest(url: 'album/$albumId/user/me');
|
|
||||||
|
|
||||||
if (res.statusCode != 200) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
@ -121,16 +109,14 @@ class SharedAlbumService {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> removeAssetFromAlbum(
|
Future<bool> removeAssetFromAlbum(
|
||||||
String albumId, List<String> assetIds) async {
|
String albumId,
|
||||||
|
List<String> assetIds,
|
||||||
|
) async {
|
||||||
try {
|
try {
|
||||||
Response res = await _networkService
|
await _apiService.albumApi.removeAssetFromAlbum(
|
||||||
.deleteRequest(url: 'album/$albumId/assets', data: {
|
albumId,
|
||||||
"assetIds": assetIds,
|
RemoveAssetsDto(assetIds: assetIds),
|
||||||
});
|
);
|
||||||
|
|
||||||
if (res.statusCode != 200) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
@ -140,17 +126,18 @@ class SharedAlbumService {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> changeTitleAlbum(
|
Future<bool> changeTitleAlbum(
|
||||||
String albumId, String ownerId, String newAlbumTitle) async {
|
String albumId,
|
||||||
|
String ownerId,
|
||||||
|
String newAlbumTitle,
|
||||||
|
) async {
|
||||||
try {
|
try {
|
||||||
Response res =
|
await _apiService.albumApi.updateAlbumInfo(
|
||||||
await _networkService.patchRequest(url: 'album/$albumId/', data: {
|
albumId,
|
||||||
"ownerId": ownerId,
|
UpdateAlbumDto(
|
||||||
"albumName": newAlbumTitle,
|
ownerId: ownerId,
|
||||||
});
|
albumName: newAlbumTitle,
|
||||||
|
),
|
||||||
if (res.statusCode != 200) {
|
);
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
|
@ -5,12 +5,12 @@ class AlbumActionOutlinedButton extends StatelessWidget {
|
||||||
final String labelText;
|
final String labelText;
|
||||||
final IconData iconData;
|
final IconData iconData;
|
||||||
|
|
||||||
const AlbumActionOutlinedButton(
|
const AlbumActionOutlinedButton({
|
||||||
{Key? key,
|
Key? key,
|
||||||
this.onPressed,
|
this.onPressed,
|
||||||
required this.labelText,
|
required this.labelText,
|
||||||
required this.iconData})
|
required this.iconData,
|
||||||
: super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
@ -31,7 +31,10 @@ class AlbumActionOutlinedButton extends StatelessWidget {
|
||||||
label: Text(
|
label: Text(
|
||||||
labelText,
|
labelText,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 12, fontWeight: FontWeight.bold, color: Colors.black87),
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.black87,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
onPressed: onPressed,
|
onPressed: onPressed,
|
||||||
),
|
),
|
||||||
|
|
|
@ -31,7 +31,10 @@ class AlbumTitleTextField extends ConsumerWidget {
|
||||||
},
|
},
|
||||||
focusNode: albumTitleTextFieldFocusNode,
|
focusNode: albumTitleTextFieldFocusNode,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 28, color: Colors.grey[700], fontWeight: FontWeight.bold),
|
fontSize: 28,
|
||||||
|
color: Colors.grey[700],
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
controller: albumTitleController,
|
controller: albumTitleController,
|
||||||
onTap: () {
|
onTap: () {
|
||||||
isAlbumTitleTextFieldFocus.value = true;
|
isAlbumTitleTextFieldFocus.value = true;
|
||||||
|
|
|
@ -4,24 +4,24 @@ import 'package:flutter/material.dart';
|
||||||
import 'package:fluttertoast/fluttertoast.dart';
|
import 'package:fluttertoast/fluttertoast.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/constants/immich_colors.dart';
|
import 'package:immich_mobile/constants/immich_colors.dart';
|
||||||
import 'package:immich_mobile/modules/sharing/models/shared_album.model.dart';
|
|
||||||
import 'package:immich_mobile/modules/sharing/providers/album_viewer.provider.dart';
|
import 'package:immich_mobile/modules/sharing/providers/album_viewer.provider.dart';
|
||||||
import 'package:immich_mobile/modules/sharing/providers/asset_selection.provider.dart';
|
import 'package:immich_mobile/modules/sharing/providers/asset_selection.provider.dart';
|
||||||
import 'package:immich_mobile/modules/sharing/providers/shared_album.provider.dart';
|
import 'package:immich_mobile/modules/sharing/providers/shared_album.provider.dart';
|
||||||
import 'package:immich_mobile/routing/router.dart';
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
import 'package:immich_mobile/shared/ui/immich_toast.dart';
|
import 'package:immich_mobile/shared/ui/immich_toast.dart';
|
||||||
import 'package:immich_mobile/shared/views/immich_loading_overlay.dart';
|
import 'package:immich_mobile/shared/views/immich_loading_overlay.dart';
|
||||||
|
import 'package:openapi/api.dart';
|
||||||
|
|
||||||
class AlbumViewerAppbar extends HookConsumerWidget with PreferredSizeWidget {
|
class AlbumViewerAppbar extends HookConsumerWidget with PreferredSizeWidget {
|
||||||
const AlbumViewerAppbar({
|
const AlbumViewerAppbar({
|
||||||
Key? key,
|
Key? key,
|
||||||
required AsyncValue<SharedAlbum> albumInfo,
|
required AsyncValue<AlbumResponseDto?> albumInfo,
|
||||||
required this.userId,
|
required this.userId,
|
||||||
required this.albumId,
|
required this.albumId,
|
||||||
}) : _albumInfo = albumInfo,
|
}) : _albumInfo = albumInfo,
|
||||||
super(key: key);
|
super(key: key);
|
||||||
|
|
||||||
final AsyncValue<SharedAlbum> _albumInfo;
|
final AsyncValue<AlbumResponseDto?> _albumInfo;
|
||||||
final String userId;
|
final String userId;
|
||||||
final String albumId;
|
final String albumId;
|
||||||
|
|
||||||
|
@ -105,7 +105,7 @@ class AlbumViewerAppbar extends HookConsumerWidget with PreferredSizeWidget {
|
||||||
|
|
||||||
_buildBottomSheetActionButton() {
|
_buildBottomSheetActionButton() {
|
||||||
if (isMultiSelectionEnable) {
|
if (isMultiSelectionEnable) {
|
||||||
if (_albumInfo.asData?.value.ownerId == userId) {
|
if (_albumInfo.asData?.value?.ownerId == userId) {
|
||||||
return ListTile(
|
return ListTile(
|
||||||
leading: const Icon(Icons.delete_sweep_rounded),
|
leading: const Icon(Icons.delete_sweep_rounded),
|
||||||
title: const Text(
|
title: const Text(
|
||||||
|
@ -118,7 +118,7 @@ class AlbumViewerAppbar extends HookConsumerWidget with PreferredSizeWidget {
|
||||||
return const SizedBox();
|
return const SizedBox();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (_albumInfo.asData?.value.ownerId == userId) {
|
if (_albumInfo.asData?.value?.ownerId == userId) {
|
||||||
return ListTile(
|
return ListTile(
|
||||||
leading: const Icon(Icons.delete_forever_rounded),
|
leading: const Icon(Icons.delete_forever_rounded),
|
||||||
title: const Text(
|
title: const Text(
|
||||||
|
|
|
@ -2,15 +2,17 @@ import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/modules/sharing/models/shared_album.model.dart';
|
|
||||||
import 'package:immich_mobile/modules/sharing/providers/album_viewer.provider.dart';
|
import 'package:immich_mobile/modules/sharing/providers/album_viewer.provider.dart';
|
||||||
|
import 'package:openapi/api.dart';
|
||||||
|
|
||||||
class AlbumViewerEditableTitle extends HookConsumerWidget {
|
class AlbumViewerEditableTitle extends HookConsumerWidget {
|
||||||
final SharedAlbum albumInfo;
|
final AlbumResponseDto albumInfo;
|
||||||
final FocusNode titleFocusNode;
|
final FocusNode titleFocusNode;
|
||||||
const AlbumViewerEditableTitle(
|
const AlbumViewerEditableTitle({
|
||||||
{Key? key, required this.albumInfo, required this.titleFocusNode})
|
Key? key,
|
||||||
: super(key: key);
|
required this.albumInfo,
|
||||||
|
required this.titleFocusNode,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
@ -24,12 +26,15 @@ class AlbumViewerEditableTitle extends HookConsumerWidget {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() {
|
useEffect(
|
||||||
titleFocusNode.addListener(onFocusModeChange);
|
() {
|
||||||
return () {
|
titleFocusNode.addListener(onFocusModeChange);
|
||||||
titleFocusNode.removeListener(onFocusModeChange);
|
return () {
|
||||||
};
|
titleFocusNode.removeListener(onFocusModeChange);
|
||||||
}, []);
|
};
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
return TextField(
|
return TextField(
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
|
|
|
@ -7,11 +7,11 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/constants/hive_box.dart';
|
import 'package:immich_mobile/constants/hive_box.dart';
|
||||||
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
|
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
|
||||||
import 'package:immich_mobile/modules/sharing/providers/asset_selection.provider.dart';
|
import 'package:immich_mobile/modules/sharing/providers/asset_selection.provider.dart';
|
||||||
import 'package:immich_mobile/shared/models/immich_asset.model.dart';
|
|
||||||
import 'package:immich_mobile/routing/router.dart';
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
|
import 'package:openapi/api.dart';
|
||||||
|
|
||||||
class AlbumViewerThumbnail extends HookConsumerWidget {
|
class AlbumViewerThumbnail extends HookConsumerWidget {
|
||||||
final ImmichAsset asset;
|
final AssetResponseDto asset;
|
||||||
|
|
||||||
const AlbumViewerThumbnail({Key? key, required this.asset}) : super(key: key);
|
const AlbumViewerThumbnail({Key? key, required this.asset}) : super(key: key);
|
||||||
|
|
||||||
|
@ -20,7 +20,7 @@ class AlbumViewerThumbnail extends HookConsumerWidget {
|
||||||
final cacheKey = useState(1);
|
final cacheKey = useState(1);
|
||||||
var box = Hive.box(userInfoBox);
|
var box = Hive.box(userInfoBox);
|
||||||
var thumbnailRequestUrl =
|
var thumbnailRequestUrl =
|
||||||
'${box.get(serverEndpointKey)}/asset/file?aid=${asset.deviceAssetId}&did=${asset.deviceId}&isThumb=true';
|
'${box.get(serverEndpointKey)}/asset/thumbnail/${asset.id}';
|
||||||
var deviceId = ref.watch(authenticationProvider).deviceId;
|
var deviceId = ref.watch(authenticationProvider).deviceId;
|
||||||
final selectedAssetsInAlbumViewer =
|
final selectedAssetsInAlbumViewer =
|
||||||
ref.watch(assetSelectionProvider).selectedAssetsInAlbumViewer;
|
ref.watch(assetSelectionProvider).selectedAssetsInAlbumViewer;
|
||||||
|
@ -28,7 +28,7 @@ class AlbumViewerThumbnail extends HookConsumerWidget {
|
||||||
ref.watch(assetSelectionProvider).isMultiselectEnable;
|
ref.watch(assetSelectionProvider).isMultiselectEnable;
|
||||||
|
|
||||||
_viewAsset() {
|
_viewAsset() {
|
||||||
if (asset.type == 'IMAGE') {
|
if (asset.type == AssetTypeEnum.IMAGE) {
|
||||||
AutoRouter.of(context).push(
|
AutoRouter.of(context).push(
|
||||||
ImageViewerRoute(
|
ImageViewerRoute(
|
||||||
imageUrl:
|
imageUrl:
|
||||||
|
@ -41,9 +41,10 @@ class AlbumViewerThumbnail extends HookConsumerWidget {
|
||||||
} else {
|
} else {
|
||||||
AutoRouter.of(context).push(
|
AutoRouter.of(context).push(
|
||||||
VideoViewerRoute(
|
VideoViewerRoute(
|
||||||
videoUrl:
|
videoUrl:
|
||||||
'${box.get(serverEndpointKey)}/asset/file?aid=${asset.deviceAssetId}&did=${asset.deviceId}',
|
'${box.get(serverEndpointKey)}/asset/file?aid=${asset.deviceAssetId}&did=${asset.deviceId}',
|
||||||
asset: asset),
|
asset: asset,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -170,16 +171,13 @@ class AlbumViewerThumbnail extends HookConsumerWidget {
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: isMultiSelectionEnable ? _handleSelectionGesture : _viewAsset,
|
onTap: isMultiSelectionEnable ? _handleSelectionGesture : _viewAsset,
|
||||||
onLongPress: _enableMultiSelection,
|
onLongPress: _enableMultiSelection,
|
||||||
child: Hero(
|
child: Stack(
|
||||||
tag: asset.id,
|
children: [
|
||||||
child: Stack(
|
_buildThumbnailImage(),
|
||||||
children: [
|
_buildAssetStoreLocationIcon(),
|
||||||
_buildThumbnailImage(),
|
if (asset.type != AssetTypeEnum.IMAGE) _buildVideoLabel(),
|
||||||
_buildAssetStoreLocationIcon(),
|
if (isMultiSelectionEnable) _buildAssetSelectionIcon(),
|
||||||
if (asset.type != 'IMAGE') _buildVideoLabel(),
|
],
|
||||||
if (isMultiSelectionEnable) _buildAssetSelectionIcon(),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/modules/sharing/ui/selection_thumbnail_image.dart';
|
import 'package:immich_mobile/modules/sharing/ui/selection_thumbnail_image.dart';
|
||||||
import 'package:immich_mobile/shared/models/immich_asset.model.dart';
|
import 'package:openapi/api.dart';
|
||||||
|
|
||||||
class AssetGridByMonth extends HookConsumerWidget {
|
class AssetGridByMonth extends HookConsumerWidget {
|
||||||
final List<ImmichAsset> assetGroup;
|
final List<AssetResponseDto> assetGroup;
|
||||||
const AssetGridByMonth({Key? key, required this.assetGroup})
|
const AssetGridByMonth({Key? key, required this.assetGroup})
|
||||||
: super(key: key);
|
: super(key: key);
|
||||||
@override
|
@override
|
||||||
|
|
|
@ -2,15 +2,17 @@ import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/modules/sharing/providers/asset_selection.provider.dart';
|
import 'package:immich_mobile/modules/sharing/providers/asset_selection.provider.dart';
|
||||||
import 'package:immich_mobile/shared/models/immich_asset.model.dart';
|
import 'package:openapi/api.dart';
|
||||||
|
|
||||||
class MonthGroupTitle extends HookConsumerWidget {
|
class MonthGroupTitle extends HookConsumerWidget {
|
||||||
final String month;
|
final String month;
|
||||||
final List<ImmichAsset> assetGroup;
|
final List<AssetResponseDto> assetGroup;
|
||||||
|
|
||||||
const MonthGroupTitle(
|
const MonthGroupTitle({
|
||||||
{Key? key, required this.month, required this.assetGroup})
|
Key? key,
|
||||||
: super(key: key);
|
required this.month,
|
||||||
|
required this.assetGroup,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
@ -75,7 +77,11 @@ class MonthGroupTitle extends HookConsumerWidget {
|
||||||
return SliverToBoxAdapter(
|
return SliverToBoxAdapter(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.only(
|
padding: const EdgeInsets.only(
|
||||||
top: 29.0, bottom: 29.0, left: 14.0, right: 8.0),
|
top: 29.0,
|
||||||
|
bottom: 29.0,
|
||||||
|
left: 14.0,
|
||||||
|
right: 8.0,
|
||||||
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
GestureDetector(
|
GestureDetector(
|
||||||
|
|
|
@ -5,10 +5,10 @@ import 'package:hive_flutter/hive_flutter.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/constants/hive_box.dart';
|
import 'package:immich_mobile/constants/hive_box.dart';
|
||||||
import 'package:immich_mobile/modules/sharing/providers/asset_selection.provider.dart';
|
import 'package:immich_mobile/modules/sharing/providers/asset_selection.provider.dart';
|
||||||
import 'package:immich_mobile/shared/models/immich_asset.model.dart';
|
import 'package:openapi/api.dart';
|
||||||
|
|
||||||
class SelectionThumbnailImage extends HookConsumerWidget {
|
class SelectionThumbnailImage extends HookConsumerWidget {
|
||||||
final ImmichAsset asset;
|
final AssetResponseDto asset;
|
||||||
|
|
||||||
const SelectionThumbnailImage({Key? key, required this.asset})
|
const SelectionThumbnailImage({Key? key, required this.asset})
|
||||||
: super(key: key);
|
: super(key: key);
|
||||||
|
@ -18,14 +18,14 @@ class SelectionThumbnailImage extends HookConsumerWidget {
|
||||||
final cacheKey = useState(1);
|
final cacheKey = useState(1);
|
||||||
var box = Hive.box(userInfoBox);
|
var box = Hive.box(userInfoBox);
|
||||||
var thumbnailRequestUrl =
|
var thumbnailRequestUrl =
|
||||||
'${box.get(serverEndpointKey)}/asset/file?aid=${asset.deviceAssetId}&did=${asset.deviceId}&isThumb=true';
|
'${box.get(serverEndpointKey)}/asset/thumbnail/${asset.id}';
|
||||||
var selectedAsset =
|
var selectedAsset =
|
||||||
ref.watch(assetSelectionProvider).selectedNewAssetsForAlbum;
|
ref.watch(assetSelectionProvider).selectedNewAssetsForAlbum;
|
||||||
var newAssetsForAlbum =
|
var newAssetsForAlbum =
|
||||||
ref.watch(assetSelectionProvider).selectedAdditionalAssetsForAlbum;
|
ref.watch(assetSelectionProvider).selectedAdditionalAssetsForAlbum;
|
||||||
var isAlbumExist = ref.watch(assetSelectionProvider).isAlbumExist;
|
var isAlbumExist = ref.watch(assetSelectionProvider).isAlbumExist;
|
||||||
|
|
||||||
Widget _buildSelectionIcon(ImmichAsset asset) {
|
Widget _buildSelectionIcon(AssetResponseDto asset) {
|
||||||
if (selectedAsset.contains(asset) && !isAlbumExist) {
|
if (selectedAsset.contains(asset) && !isAlbumExist) {
|
||||||
return Icon(
|
return Icon(
|
||||||
Icons.check_circle,
|
Icons.check_circle,
|
||||||
|
@ -103,7 +103,7 @@ class SelectionThumbnailImage extends HookConsumerWidget {
|
||||||
cacheKey: "${asset.id}-${cacheKey.value}",
|
cacheKey: "${asset.id}-${cacheKey.value}",
|
||||||
width: 150,
|
width: 150,
|
||||||
height: 150,
|
height: 150,
|
||||||
memCacheHeight: asset.type == 'IMAGE' ? 150 : 150,
|
memCacheHeight: asset.type == AssetTypeEnum.IMAGE ? 150 : 150,
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
imageUrl: thumbnailRequestUrl,
|
imageUrl: thumbnailRequestUrl,
|
||||||
httpHeaders: {
|
httpHeaders: {
|
||||||
|
@ -131,14 +131,14 @@ class SelectionThumbnailImage extends HookConsumerWidget {
|
||||||
child: _buildSelectionIcon(asset),
|
child: _buildSelectionIcon(asset),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (asset.type != 'IMAGE')
|
if (asset.type != AssetTypeEnum.IMAGE)
|
||||||
Positioned(
|
Positioned(
|
||||||
bottom: 5,
|
bottom: 5,
|
||||||
right: 5,
|
right: 5,
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
'${asset.duration?.substring(0, 7)}',
|
asset.duration.substring(0, 7),
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
fontSize: 10,
|
fontSize: 10,
|
||||||
|
|
|
@ -4,10 +4,10 @@ import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:hive_flutter/hive_flutter.dart';
|
import 'package:hive_flutter/hive_flutter.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/constants/hive_box.dart';
|
import 'package:immich_mobile/constants/hive_box.dart';
|
||||||
import 'package:immich_mobile/shared/models/immich_asset.model.dart';
|
import 'package:openapi/api.dart';
|
||||||
|
|
||||||
class SharedAlbumThumbnailImage extends HookConsumerWidget {
|
class SharedAlbumThumbnailImage extends HookConsumerWidget {
|
||||||
final ImmichAsset asset;
|
final AssetResponseDto asset;
|
||||||
|
|
||||||
const SharedAlbumThumbnailImage({Key? key, required this.asset})
|
const SharedAlbumThumbnailImage({Key? key, required this.asset})
|
||||||
: super(key: key);
|
: super(key: key);
|
||||||
|
@ -18,7 +18,7 @@ class SharedAlbumThumbnailImage extends HookConsumerWidget {
|
||||||
|
|
||||||
var box = Hive.box(userInfoBox);
|
var box = Hive.box(userInfoBox);
|
||||||
var thumbnailRequestUrl =
|
var thumbnailRequestUrl =
|
||||||
'${box.get(serverEndpointKey)}/asset/file?aid=${asset.deviceAssetId}&did=${asset.deviceId}&isThumb=true';
|
'${box.get(serverEndpointKey)}/asset/thumbnail/${asset.id}';
|
||||||
|
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
|
@ -30,7 +30,7 @@ class SharedAlbumThumbnailImage extends HookConsumerWidget {
|
||||||
cacheKey: "${asset.id}-${cacheKey.value}",
|
cacheKey: "${asset.id}-${cacheKey.value}",
|
||||||
width: 500,
|
width: 500,
|
||||||
height: 500,
|
height: 500,
|
||||||
memCacheHeight: asset.type == 'IMAGE' ? 500 : 500,
|
memCacheHeight: 500,
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
imageUrl: thumbnailRequestUrl,
|
imageUrl: thumbnailRequestUrl,
|
||||||
httpHeaders: {"Authorization": "Bearer ${box.get(accessTokenKey)}"},
|
httpHeaders: {"Authorization": "Bearer ${box.get(accessTokenKey)}"},
|
||||||
|
|
|
@ -40,7 +40,8 @@ class SharingSliverAppBar extends StatelessWidget {
|
||||||
child: TextButton.icon(
|
child: TextButton.icon(
|
||||||
style: ButtonStyle(
|
style: ButtonStyle(
|
||||||
backgroundColor: MaterialStateProperty.all(
|
backgroundColor: MaterialStateProperty.all(
|
||||||
Theme.of(context).primaryColor.withAlpha(20)),
|
Theme.of(context).primaryColor.withAlpha(20),
|
||||||
|
),
|
||||||
// foregroundColor: MaterialStateProperty.all(Colors.white),
|
// foregroundColor: MaterialStateProperty.all(Colors.white),
|
||||||
),
|
),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
|
@ -65,7 +66,8 @@ class SharingSliverAppBar extends StatelessWidget {
|
||||||
child: TextButton.icon(
|
child: TextButton.icon(
|
||||||
style: ButtonStyle(
|
style: ButtonStyle(
|
||||||
backgroundColor: MaterialStateProperty.all(
|
backgroundColor: MaterialStateProperty.all(
|
||||||
Theme.of(context).primaryColor.withAlpha(20)),
|
Theme.of(context).primaryColor.withAlpha(20),
|
||||||
|
),
|
||||||
// foregroundColor: MaterialStateProperty.all(Colors.white),
|
// foregroundColor: MaterialStateProperty.all(Colors.white),
|
||||||
),
|
),
|
||||||
onPressed: null,
|
onPressed: null,
|
||||||
|
|
|
@ -7,7 +7,6 @@ import 'package:immich_mobile/constants/immich_colors.dart';
|
||||||
import 'package:immich_mobile/modules/home/ui/draggable_scrollbar.dart';
|
import 'package:immich_mobile/modules/home/ui/draggable_scrollbar.dart';
|
||||||
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
|
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
|
||||||
import 'package:immich_mobile/modules/sharing/models/asset_selection_page_result.model.dart';
|
import 'package:immich_mobile/modules/sharing/models/asset_selection_page_result.model.dart';
|
||||||
import 'package:immich_mobile/modules/sharing/models/shared_album.model.dart';
|
|
||||||
import 'package:immich_mobile/modules/sharing/providers/asset_selection.provider.dart';
|
import 'package:immich_mobile/modules/sharing/providers/asset_selection.provider.dart';
|
||||||
import 'package:immich_mobile/modules/sharing/providers/shared_album.provider.dart';
|
import 'package:immich_mobile/modules/sharing/providers/shared_album.provider.dart';
|
||||||
import 'package:immich_mobile/modules/sharing/services/shared_album.service.dart';
|
import 'package:immich_mobile/modules/sharing/services/shared_album.service.dart';
|
||||||
|
@ -19,7 +18,7 @@ import 'package:immich_mobile/routing/router.dart';
|
||||||
import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
|
import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
|
||||||
import 'package:immich_mobile/shared/ui/immich_sliver_persistent_app_bar_delegate.dart';
|
import 'package:immich_mobile/shared/ui/immich_sliver_persistent_app_bar_delegate.dart';
|
||||||
import 'package:immich_mobile/shared/views/immich_loading_overlay.dart';
|
import 'package:immich_mobile/shared/views/immich_loading_overlay.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:openapi/api.dart';
|
||||||
|
|
||||||
class AlbumViewerPage extends HookConsumerWidget {
|
class AlbumViewerPage extends HookConsumerWidget {
|
||||||
final String albumId;
|
final String albumId;
|
||||||
|
@ -30,18 +29,18 @@ class AlbumViewerPage extends HookConsumerWidget {
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
FocusNode titleFocusNode = useFocusNode();
|
FocusNode titleFocusNode = useFocusNode();
|
||||||
ScrollController scrollController = useScrollController();
|
ScrollController scrollController = useScrollController();
|
||||||
AsyncValue<SharedAlbum> albumInfo =
|
AsyncValue<AlbumResponseDto?> albumInfo =
|
||||||
ref.watch(sharedAlbumDetailProvider(albumId));
|
ref.watch(sharedAlbumDetailProvider(albumId));
|
||||||
|
|
||||||
final userId = ref.watch(authenticationProvider).userId;
|
final userId = ref.watch(authenticationProvider).userId;
|
||||||
|
|
||||||
/// Find out if the assets in album exist on the device
|
/// Find out if the assets in album exist on the device
|
||||||
/// If they exist, add to selected asset state to show they are already selected.
|
/// If they exist, add to selected asset state to show they are already selected.
|
||||||
void _onAddPhotosPressed(SharedAlbum albumInfo) async {
|
void _onAddPhotosPressed(AlbumResponseDto albumInfo) async {
|
||||||
if (albumInfo.assets?.isNotEmpty == true) {
|
if (albumInfo.assets.isNotEmpty == true) {
|
||||||
ref
|
ref
|
||||||
.watch(assetSelectionProvider.notifier)
|
.watch(assetSelectionProvider.notifier)
|
||||||
.addNewAssets(albumInfo.assets!.toList());
|
.addNewAssets(albumInfo.assets.toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
ref.watch(assetSelectionProvider.notifier).setIsAlbumExist(true);
|
ref.watch(assetSelectionProvider.notifier).setIsAlbumExist(true);
|
||||||
|
@ -57,7 +56,9 @@ class AlbumViewerPage extends HookConsumerWidget {
|
||||||
var isSuccess = await ref
|
var isSuccess = await ref
|
||||||
.watch(sharedAlbumServiceProvider)
|
.watch(sharedAlbumServiceProvider)
|
||||||
.addAdditionalAssetToAlbum(
|
.addAdditionalAssetToAlbum(
|
||||||
returnPayload.selectedAdditionalAsset, albumId);
|
returnPayload.selectedAdditionalAsset,
|
||||||
|
albumId,
|
||||||
|
);
|
||||||
|
|
||||||
if (isSuccess) {
|
if (isSuccess) {
|
||||||
ref.refresh(sharedAlbumDetailProvider(albumId));
|
ref.refresh(sharedAlbumDetailProvider(albumId));
|
||||||
|
@ -72,10 +73,11 @@ class AlbumViewerPage extends HookConsumerWidget {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onAddUsersPressed(SharedAlbum albumInfo) async {
|
void _onAddUsersPressed(AlbumResponseDto albumInfo) async {
|
||||||
List<String>? sharedUserIds = await AutoRouter.of(context)
|
List<String>? sharedUserIds =
|
||||||
.push<List<String>?>(
|
await AutoRouter.of(context).push<List<String>?>(
|
||||||
SelectAdditionalUserForSharingRoute(albumInfo: albumInfo));
|
SelectAdditionalUserForSharingRoute(albumInfo: albumInfo),
|
||||||
|
);
|
||||||
|
|
||||||
if (sharedUserIds != null) {
|
if (sharedUserIds != null) {
|
||||||
ImmichLoadingOverlayController.appLoader.show();
|
ImmichLoadingOverlayController.appLoader.show();
|
||||||
|
@ -92,7 +94,7 @@ class AlbumViewerPage extends HookConsumerWidget {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildTitle(SharedAlbum albumInfo) {
|
Widget _buildTitle(AlbumResponseDto albumInfo) {
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.only(left: 8, right: 8, top: 16),
|
padding: const EdgeInsets.only(left: 8, right: 8, top: 16),
|
||||||
child: userId == albumInfo.ownerId
|
child: userId == albumInfo.ownerId
|
||||||
|
@ -102,19 +104,24 @@ class AlbumViewerPage extends HookConsumerWidget {
|
||||||
)
|
)
|
||||||
: Padding(
|
: Padding(
|
||||||
padding: const EdgeInsets.only(left: 8.0),
|
padding: const EdgeInsets.only(left: 8.0),
|
||||||
child: Text(albumInfo.albumName,
|
child: Text(
|
||||||
style: const TextStyle(
|
albumInfo.albumName,
|
||||||
fontSize: 24, fontWeight: FontWeight.bold)),
|
style: const TextStyle(
|
||||||
|
fontSize: 24,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildAlbumDateRange(SharedAlbum albumInfo) {
|
Widget _buildAlbumDateRange(AlbumResponseDto albumInfo) {
|
||||||
String startDate = "";
|
String startDate = "";
|
||||||
DateTime parsedStartDate =
|
DateTime parsedStartDate =
|
||||||
DateTime.parse(albumInfo.assets!.first.createdAt);
|
DateTime.parse(albumInfo.assets.first.createdAt);
|
||||||
DateTime parsedEndDate = DateTime.parse(
|
DateTime parsedEndDate = DateTime.parse(
|
||||||
albumInfo.assets?.last.createdAt ?? '11111111'); //Need default.
|
albumInfo.assets.last.createdAt,
|
||||||
|
); //Need default.
|
||||||
|
|
||||||
if (parsedStartDate.year == parsedEndDate.year) {
|
if (parsedStartDate.year == parsedEndDate.year) {
|
||||||
startDate = DateFormat('LLL d').format(parsedStartDate);
|
startDate = DateFormat('LLL d').format(parsedStartDate);
|
||||||
|
@ -129,18 +136,21 @@ class AlbumViewerPage extends HookConsumerWidget {
|
||||||
child: Text(
|
child: Text(
|
||||||
"$startDate-$endDate",
|
"$startDate-$endDate",
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 14, fontWeight: FontWeight.bold, color: Colors.grey),
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.grey,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildHeader(SharedAlbum albumInfo) {
|
Widget _buildHeader(AlbumResponseDto albumInfo) {
|
||||||
return SliverToBoxAdapter(
|
return SliverToBoxAdapter(
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
_buildTitle(albumInfo),
|
_buildTitle(albumInfo),
|
||||||
if (albumInfo.assets?.isNotEmpty == true)
|
if (albumInfo.assets.isNotEmpty == true)
|
||||||
_buildAlbumDateRange(albumInfo),
|
_buildAlbumDateRange(albumInfo),
|
||||||
SizedBox(
|
SizedBox(
|
||||||
height: 60,
|
height: 60,
|
||||||
|
@ -172,8 +182,8 @@ class AlbumViewerPage extends HookConsumerWidget {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildImageGrid(SharedAlbum albumInfo) {
|
Widget _buildImageGrid(AlbumResponseDto albumInfo) {
|
||||||
if (albumInfo.assets?.isNotEmpty == true) {
|
if (albumInfo.assets.isNotEmpty) {
|
||||||
return SliverPadding(
|
return SliverPadding(
|
||||||
padding: const EdgeInsets.only(top: 10.0),
|
padding: const EdgeInsets.only(top: 10.0),
|
||||||
sliver: SliverGrid(
|
sliver: SliverGrid(
|
||||||
|
@ -184,9 +194,9 @@ class AlbumViewerPage extends HookConsumerWidget {
|
||||||
),
|
),
|
||||||
delegate: SliverChildBuilderDelegate(
|
delegate: SliverChildBuilderDelegate(
|
||||||
(BuildContext context, int index) {
|
(BuildContext context, int index) {
|
||||||
return AlbumViewerThumbnail(asset: albumInfo.assets![index]);
|
return AlbumViewerThumbnail(asset: albumInfo.assets[index]);
|
||||||
},
|
},
|
||||||
childCount: albumInfo.assets?.length,
|
childCount: albumInfo.assets.length,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -194,7 +204,7 @@ class AlbumViewerPage extends HookConsumerWidget {
|
||||||
return const SliverToBoxAdapter();
|
return const SliverToBoxAdapter();
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildControlButton(SharedAlbum albumInfo) {
|
Widget _buildControlButton(AlbumResponseDto albumInfo) {
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.only(left: 16.0, top: 8, bottom: 8),
|
padding: const EdgeInsets.only(left: 16.0, top: 8, bottom: 8),
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
|
@ -219,7 +229,7 @@ class AlbumViewerPage extends HookConsumerWidget {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildBody(SharedAlbum albumInfo) {
|
Widget _buildBody(AlbumResponseDto albumInfo) {
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
titleFocusNode.unfocus();
|
titleFocusNode.unfocus();
|
||||||
|
@ -252,9 +262,16 @@ class AlbumViewerPage extends HookConsumerWidget {
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AlbumViewerAppbar(
|
appBar: AlbumViewerAppbar(
|
||||||
albumInfo: albumInfo, userId: userId, albumId: albumId),
|
albumInfo: albumInfo,
|
||||||
|
userId: userId,
|
||||||
|
albumId: albumId,
|
||||||
|
),
|
||||||
body: albumInfo.when(
|
body: albumInfo.when(
|
||||||
data: (albumInfo) => _buildBody(albumInfo),
|
data: (albumInfo) => albumInfo != null
|
||||||
|
? _buildBody(albumInfo)
|
||||||
|
: const Center(
|
||||||
|
child: CircularProgressIndicator(),
|
||||||
|
),
|
||||||
error: (e, _) => Center(child: Text("Error loading album info $e")),
|
error: (e, _) => Center(child: Text("Error loading album info $e")),
|
||||||
loading: () => const Center(
|
loading: () => const Center(
|
||||||
child: ImmichLoadingIndicator(),
|
child: ImmichLoadingIndicator(),
|
||||||
|
|
|
@ -56,10 +56,11 @@ class CreateSharedAlbumPage extends HookConsumerWidget {
|
||||||
left: 10,
|
left: 10,
|
||||||
),
|
),
|
||||||
child: AlbumTitleTextField(
|
child: AlbumTitleTextField(
|
||||||
isAlbumTitleEmpty: isAlbumTitleEmpty,
|
isAlbumTitleEmpty: isAlbumTitleEmpty,
|
||||||
albumTitleTextFieldFocusNode: albumTitleTextFieldFocusNode,
|
albumTitleTextFieldFocusNode: albumTitleTextFieldFocusNode,
|
||||||
albumTitleController: albumTitleController,
|
albumTitleController: albumTitleController,
|
||||||
isAlbumTitleTextFieldFocus: isAlbumTitleTextFieldFocus),
|
isAlbumTitleTextFieldFocus: isAlbumTitleTextFieldFocus,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -67,8 +68,8 @@ class CreateSharedAlbumPage extends HookConsumerWidget {
|
||||||
if (selectedAssets.isEmpty) {
|
if (selectedAssets.isEmpty) {
|
||||||
return SliverToBoxAdapter(
|
return SliverToBoxAdapter(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: EdgeInsets.only(top: 200, left: 18),
|
padding: const EdgeInsets.only(top: 200, left: 18),
|
||||||
child: Text(
|
child: const Text(
|
||||||
'create_shared_album_page_share_add_assets',
|
'create_shared_album_page_share_add_assets',
|
||||||
style: TextStyle(fontSize: 12),
|
style: TextStyle(fontSize: 12),
|
||||||
).tr(),
|
).tr(),
|
||||||
|
@ -86,13 +87,16 @@ class CreateSharedAlbumPage extends HookConsumerWidget {
|
||||||
padding: const EdgeInsets.only(top: 16, left: 18, right: 18),
|
padding: const EdgeInsets.only(top: 16, left: 18, right: 18),
|
||||||
child: OutlinedButton.icon(
|
child: OutlinedButton.icon(
|
||||||
style: OutlinedButton.styleFrom(
|
style: OutlinedButton.styleFrom(
|
||||||
alignment: Alignment.centerLeft,
|
alignment: Alignment.centerLeft,
|
||||||
padding:
|
padding:
|
||||||
const EdgeInsets.symmetric(vertical: 22, horizontal: 16),
|
const EdgeInsets.symmetric(vertical: 22, horizontal: 16),
|
||||||
side: const BorderSide(
|
side: const BorderSide(
|
||||||
color: Color.fromARGB(255, 206, 206, 206)),
|
color: Color.fromARGB(255, 206, 206, 206),
|
||||||
shape: RoundedRectangleBorder(
|
),
|
||||||
borderRadius: BorderRadius.circular(5))),
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(5),
|
||||||
|
),
|
||||||
|
),
|
||||||
onPressed: _onSelectPhotosButtonPressed,
|
onPressed: _onSelectPhotosButtonPressed,
|
||||||
icon: const Icon(Icons.add_rounded),
|
icon: const Icon(Icons.add_rounded),
|
||||||
label: Padding(
|
label: Padding(
|
||||||
|
@ -100,9 +104,10 @@ class CreateSharedAlbumPage extends HookConsumerWidget {
|
||||||
child: Text(
|
child: Text(
|
||||||
'create_shared_album_page_share_select_photos',
|
'create_shared_album_page_share_select_photos',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
color: Colors.grey[700],
|
color: Colors.grey[700],
|
||||||
fontWeight: FontWeight.bold),
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
).tr(),
|
).tr(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -147,7 +152,8 @@ class CreateSharedAlbumPage extends HookConsumerWidget {
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: _onBackgroundTapped,
|
onTap: _onBackgroundTapped,
|
||||||
child: SharedAlbumThumbnailImage(
|
child: SharedAlbumThumbnailImage(
|
||||||
asset: selectedAssets.toList()[index]),
|
asset: selectedAssets.toList()[index],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
childCount: selectedAssets.length,
|
childCount: selectedAssets.length,
|
||||||
|
@ -160,58 +166,60 @@ class CreateSharedAlbumPage extends HookConsumerWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
centerTitle: false,
|
centerTitle: false,
|
||||||
leading: IconButton(
|
leading: IconButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
ref.watch(assetSelectionProvider.notifier).removeAll();
|
ref.watch(assetSelectionProvider.notifier).removeAll();
|
||||||
AutoRouter.of(context).pop();
|
AutoRouter.of(context).pop();
|
||||||
},
|
},
|
||||||
icon: const Icon(Icons.close_rounded)),
|
icon: const Icon(Icons.close_rounded),
|
||||||
title: const Text(
|
),
|
||||||
'share_create_album',
|
title: const Text(
|
||||||
style: TextStyle(color: Colors.black),
|
'share_create_album',
|
||||||
).tr(),
|
style: TextStyle(color: Colors.black),
|
||||||
actions: [
|
).tr(),
|
||||||
TextButton(
|
actions: [
|
||||||
onPressed: albumTitleController.text.isNotEmpty
|
TextButton(
|
||||||
? _showSelectUserPage
|
onPressed: albumTitleController.text.isNotEmpty
|
||||||
: null,
|
? _showSelectUserPage
|
||||||
child: Text(
|
: null,
|
||||||
'create_shared_album_page_share'.tr(),
|
child: Text(
|
||||||
style: TextStyle(
|
'create_shared_album_page_share'.tr(),
|
||||||
fontWeight: FontWeight.bold,
|
style: const TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
body: GestureDetector(
|
||||||
|
onTap: _onBackgroundTapped,
|
||||||
|
child: CustomScrollView(
|
||||||
|
slivers: [
|
||||||
|
SliverAppBar(
|
||||||
|
elevation: 5,
|
||||||
|
automaticallyImplyLeading: false,
|
||||||
|
// leading: Container(),
|
||||||
|
pinned: true,
|
||||||
|
floating: false,
|
||||||
|
bottom: PreferredSize(
|
||||||
|
preferredSize: const Size.fromHeight(66.0),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
_buildTitleInputField(),
|
||||||
|
if (selectedAssets.isNotEmpty) _buildControlButton(),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
_buildTitle(),
|
||||||
|
_buildSelectPhotosButton(),
|
||||||
|
_buildSelectedImageGrid(),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
body: GestureDetector(
|
),
|
||||||
onTap: _onBackgroundTapped,
|
);
|
||||||
child: CustomScrollView(
|
|
||||||
slivers: [
|
|
||||||
SliverAppBar(
|
|
||||||
elevation: 5,
|
|
||||||
automaticallyImplyLeading: false,
|
|
||||||
// leading: Container(),
|
|
||||||
pinned: true,
|
|
||||||
floating: false,
|
|
||||||
bottom: PreferredSize(
|
|
||||||
preferredSize: const Size.fromHeight(66.0),
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
_buildTitleInputField(),
|
|
||||||
if (selectedAssets.isNotEmpty) _buildControlButton(),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
_buildTitle(),
|
|
||||||
_buildSelectPhotosButton(),
|
|
||||||
_buildSelectedImageGrid(),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,29 +3,28 @@ import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/modules/sharing/models/shared_album.model.dart';
|
|
||||||
import 'package:immich_mobile/modules/sharing/providers/suggested_shared_users.provider.dart';
|
import 'package:immich_mobile/modules/sharing/providers/suggested_shared_users.provider.dart';
|
||||||
import 'package:immich_mobile/shared/models/user.model.dart';
|
|
||||||
import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
|
import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
|
||||||
|
import 'package:openapi/api.dart';
|
||||||
|
|
||||||
class SelectAdditionalUserForSharingPage extends HookConsumerWidget {
|
class SelectAdditionalUserForSharingPage extends HookConsumerWidget {
|
||||||
final SharedAlbum albumInfo;
|
final AlbumResponseDto albumInfo;
|
||||||
|
|
||||||
const SelectAdditionalUserForSharingPage({Key? key, required this.albumInfo})
|
const SelectAdditionalUserForSharingPage({Key? key, required this.albumInfo})
|
||||||
: super(key: key);
|
: super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
AsyncValue<List<User>> suggestedShareUsers =
|
AsyncValue<List<UserResponseDto>> suggestedShareUsers =
|
||||||
ref.watch(suggestedSharedUsersProvider);
|
ref.watch(suggestedSharedUsersProvider);
|
||||||
final sharedUsersList = useState<Set<User>>({});
|
final sharedUsersList = useState<Set<UserResponseDto>>({});
|
||||||
|
|
||||||
_addNewUsersHandler() {
|
_addNewUsersHandler() {
|
||||||
AutoRouter.of(context)
|
AutoRouter.of(context)
|
||||||
.pop(sharedUsersList.value.map((e) => e.id).toList());
|
.pop(sharedUsersList.value.map((e) => e.id).toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
_buildTileIcon(User user) {
|
_buildTileIcon(UserResponseDto user) {
|
||||||
if (sharedUsersList.value.contains(user)) {
|
if (sharedUsersList.value.contains(user)) {
|
||||||
return CircleAvatar(
|
return CircleAvatar(
|
||||||
backgroundColor: Theme.of(context).primaryColor,
|
backgroundColor: Theme.of(context).primaryColor,
|
||||||
|
@ -43,7 +42,7 @@ class SelectAdditionalUserForSharingPage extends HookConsumerWidget {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_buildUserList(List<User> users) {
|
_buildUserList(List<UserResponseDto> users) {
|
||||||
List<Widget> usersChip = [];
|
List<Widget> usersChip = [];
|
||||||
|
|
||||||
for (var user in sharedUsersList.value) {
|
for (var user in sharedUsersList.value) {
|
||||||
|
@ -55,9 +54,10 @@ class SelectAdditionalUserForSharingPage extends HookConsumerWidget {
|
||||||
label: Text(
|
label: Text(
|
||||||
user.email,
|
user.email,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
color: Colors.black87,
|
color: Colors.black87,
|
||||||
fontWeight: FontWeight.bold),
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -70,13 +70,14 @@ class SelectAdditionalUserForSharingPage extends HookConsumerWidget {
|
||||||
children: [...usersChip],
|
children: [...usersChip],
|
||||||
),
|
),
|
||||||
Padding(
|
Padding(
|
||||||
padding: EdgeInsets.all(16.0),
|
padding: const EdgeInsets.all(16.0),
|
||||||
child: Text(
|
child: Text(
|
||||||
'select_additional_user_for_sharing_page_suggestions'.tr(),
|
'select_additional_user_for_sharing_page_suggestions'.tr(),
|
||||||
style: TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
color: Colors.grey,
|
color: Colors.grey,
|
||||||
fontWeight: FontWeight.bold),
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
ListView.builder(
|
ListView.builder(
|
||||||
|
@ -87,13 +88,16 @@ class SelectAdditionalUserForSharingPage extends HookConsumerWidget {
|
||||||
title: Text(
|
title: Text(
|
||||||
users[index].email,
|
users[index].email,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 14, fontWeight: FontWeight.bold),
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
if (sharedUsersList.value.contains(users[index])) {
|
if (sharedUsersList.value.contains(users[index])) {
|
||||||
sharedUsersList.value = sharedUsersList.value
|
sharedUsersList.value = sharedUsersList.value
|
||||||
.where((selectedUser) =>
|
.where(
|
||||||
selectedUser.id != users[index].id)
|
(selectedUser) => selectedUser.id != users[index].id,
|
||||||
|
)
|
||||||
.toSet();
|
.toSet();
|
||||||
} else {
|
} else {
|
||||||
sharedUsersList.value = {
|
sharedUsersList.value = {
|
||||||
|
@ -139,7 +143,8 @@ class SelectAdditionalUserForSharingPage extends HookConsumerWidget {
|
||||||
data: (users) {
|
data: (users) {
|
||||||
for (var sharedUsers in albumInfo.sharedUsers) {
|
for (var sharedUsers in albumInfo.sharedUsers) {
|
||||||
users.removeWhere(
|
users.removeWhere(
|
||||||
(u) => u.id == sharedUsers.id || u.id == albumInfo.ownerId);
|
(u) => u.id == sharedUsers.id || u.id == albumInfo.ownerId,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return _buildUserList(users);
|
return _buildUserList(users);
|
||||||
|
|
|
@ -9,15 +9,16 @@ import 'package:immich_mobile/modules/sharing/providers/shared_album.provider.da
|
||||||
import 'package:immich_mobile/modules/sharing/providers/suggested_shared_users.provider.dart';
|
import 'package:immich_mobile/modules/sharing/providers/suggested_shared_users.provider.dart';
|
||||||
import 'package:immich_mobile/modules/sharing/services/shared_album.service.dart';
|
import 'package:immich_mobile/modules/sharing/services/shared_album.service.dart';
|
||||||
import 'package:immich_mobile/routing/router.dart';
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
import 'package:immich_mobile/shared/models/user.model.dart';
|
|
||||||
import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
|
import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
|
||||||
|
import 'package:openapi/api.dart';
|
||||||
|
|
||||||
class SelectUserForSharingPage extends HookConsumerWidget {
|
class SelectUserForSharingPage extends HookConsumerWidget {
|
||||||
const SelectUserForSharingPage({Key? key}) : super(key: key);
|
const SelectUserForSharingPage({Key? key}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final sharedUsersList = useState<Set<User>>({});
|
final sharedUsersList = useState<Set<UserResponseDto>>({});
|
||||||
AsyncValue<List<User>> suggestedShareUsers =
|
AsyncValue<List<UserResponseDto>> suggestedShareUsers =
|
||||||
ref.watch(suggestedSharedUsersProvider);
|
ref.watch(suggestedSharedUsersProvider);
|
||||||
|
|
||||||
_createSharedAlbum() async {
|
_createSharedAlbum() async {
|
||||||
|
@ -37,10 +38,14 @@ class SelectUserForSharingPage extends HookConsumerWidget {
|
||||||
.navigate(const TabControllerRoute(children: [SharingRoute()]));
|
.navigate(const TabControllerRoute(children: [SharingRoute()]));
|
||||||
}
|
}
|
||||||
|
|
||||||
ScaffoldMessenger(child: SnackBar(content: Text('select_user_for_sharing_page_err_album').tr()));
|
ScaffoldMessenger(
|
||||||
|
child: SnackBar(
|
||||||
|
content: const Text('select_user_for_sharing_page_err_album').tr(),
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
_buildTileIcon(User user) {
|
_buildTileIcon(UserResponseDto user) {
|
||||||
if (sharedUsersList.value.contains(user)) {
|
if (sharedUsersList.value.contains(user)) {
|
||||||
return CircleAvatar(
|
return CircleAvatar(
|
||||||
backgroundColor: Theme.of(context).primaryColor,
|
backgroundColor: Theme.of(context).primaryColor,
|
||||||
|
@ -58,7 +63,7 @@ class SelectUserForSharingPage extends HookConsumerWidget {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_buildUserList(List<User> users) {
|
_buildUserList(List<UserResponseDto> users) {
|
||||||
List<Widget> usersChip = [];
|
List<Widget> usersChip = [];
|
||||||
|
|
||||||
for (var user in sharedUsersList.value) {
|
for (var user in sharedUsersList.value) {
|
||||||
|
@ -70,9 +75,10 @@ class SelectUserForSharingPage extends HookConsumerWidget {
|
||||||
label: Text(
|
label: Text(
|
||||||
user.email,
|
user.email,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
color: Colors.black87,
|
color: Colors.black87,
|
||||||
fontWeight: FontWeight.bold),
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -85,13 +91,14 @@ class SelectUserForSharingPage extends HookConsumerWidget {
|
||||||
children: [...usersChip],
|
children: [...usersChip],
|
||||||
),
|
),
|
||||||
Padding(
|
Padding(
|
||||||
padding: EdgeInsets.all(16.0),
|
padding: const EdgeInsets.all(16.0),
|
||||||
child: Text(
|
child: const Text(
|
||||||
'share_suggestions',
|
'select_user_for_sharing_page_share_suggestions',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
color: Colors.grey,
|
color: Colors.grey,
|
||||||
fontWeight: FontWeight.bold),
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
).tr(),
|
).tr(),
|
||||||
),
|
),
|
||||||
ListView.builder(
|
ListView.builder(
|
||||||
|
@ -102,13 +109,16 @@ class SelectUserForSharingPage extends HookConsumerWidget {
|
||||||
title: Text(
|
title: Text(
|
||||||
users[index].email,
|
users[index].email,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 14, fontWeight: FontWeight.bold),
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
if (sharedUsersList.value.contains(users[index])) {
|
if (sharedUsersList.value.contains(users[index])) {
|
||||||
sharedUsersList.value = sharedUsersList.value
|
sharedUsersList.value = sharedUsersList.value
|
||||||
.where((selectedUser) =>
|
.where(
|
||||||
selectedUser.id != users[index].id)
|
(selectedUser) => selectedUser.id != users[index].id,
|
||||||
|
)
|
||||||
.toSet();
|
.toSet();
|
||||||
} else {
|
} else {
|
||||||
sharedUsersList.value = {
|
sharedUsersList.value = {
|
||||||
|
@ -141,12 +151,13 @@ class SelectUserForSharingPage extends HookConsumerWidget {
|
||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed:
|
onPressed:
|
||||||
sharedUsersList.value.isEmpty ? null : _createSharedAlbum,
|
sharedUsersList.value.isEmpty ? null : _createSharedAlbum,
|
||||||
child: const Text(
|
child: const Text(
|
||||||
"share_create_album",
|
"share_create_album",
|
||||||
style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold),
|
style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold),
|
||||||
).tr())
|
).tr(),
|
||||||
|
)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
body: suggestedShareUsers.when(
|
body: suggestedShareUsers.when(
|
||||||
|
|
|
@ -5,10 +5,10 @@ import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:hive/hive.dart';
|
import 'package:hive/hive.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/constants/hive_box.dart';
|
import 'package:immich_mobile/constants/hive_box.dart';
|
||||||
import 'package:immich_mobile/modules/sharing/models/shared_album.model.dart';
|
|
||||||
import 'package:immich_mobile/modules/sharing/providers/shared_album.provider.dart';
|
import 'package:immich_mobile/modules/sharing/providers/shared_album.provider.dart';
|
||||||
import 'package:immich_mobile/modules/sharing/ui/sharing_sliver_appbar.dart';
|
import 'package:immich_mobile/modules/sharing/ui/sharing_sliver_appbar.dart';
|
||||||
import 'package:immich_mobile/routing/router.dart';
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
|
import 'package:openapi/api.dart';
|
||||||
import 'package:transparent_image/transparent_image.dart';
|
import 'package:transparent_image/transparent_image.dart';
|
||||||
|
|
||||||
class SharingPage extends HookConsumerWidget {
|
class SharingPage extends HookConsumerWidget {
|
||||||
|
@ -18,13 +18,16 @@ class SharingPage extends HookConsumerWidget {
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
var box = Hive.box(userInfoBox);
|
var box = Hive.box(userInfoBox);
|
||||||
var thumbnailRequestUrl = '${box.get(serverEndpointKey)}/asset/thumbnail';
|
var thumbnailRequestUrl = '${box.get(serverEndpointKey)}/asset/thumbnail';
|
||||||
final List<SharedAlbum> sharedAlbums = ref.watch(sharedAlbumProvider);
|
final List<AlbumResponseDto> sharedAlbums = ref.watch(sharedAlbumProvider);
|
||||||
|
|
||||||
useEffect(() {
|
useEffect(
|
||||||
ref.read(sharedAlbumProvider.notifier).getAllSharedAlbums();
|
() {
|
||||||
|
ref.read(sharedAlbumProvider.notifier).getAllSharedAlbums();
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}, []);
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
_buildAlbumList() {
|
_buildAlbumList() {
|
||||||
return SliverList(
|
return SliverList(
|
||||||
|
@ -60,9 +63,10 @@ class SharingPage extends HookConsumerWidget {
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
color: Colors.grey.shade800),
|
color: Colors.grey.shade800,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
AutoRouter.of(context)
|
AutoRouter.of(context)
|
||||||
|
@ -133,9 +137,9 @@ class SharingPage extends HookConsumerWidget {
|
||||||
slivers: [
|
slivers: [
|
||||||
const SharingSliverAppBar(),
|
const SharingSliverAppBar(),
|
||||||
SliverPadding(
|
SliverPadding(
|
||||||
padding: EdgeInsets.symmetric(horizontal: 12, vertical: 12),
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12),
|
||||||
sliver: SliverToBoxAdapter(
|
sliver: SliverToBoxAdapter(
|
||||||
child: Text(
|
child: const Text(
|
||||||
"sharing_page_album",
|
"sharing_page_album",
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
|
|
|
@ -1,21 +1,25 @@
|
||||||
import 'dart:convert';
|
|
||||||
|
|
||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
import 'package:immich_mobile/shared/services/network.service.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
|
import 'package:immich_mobile/shared/services/api.service.dart';
|
||||||
|
|
||||||
class AuthGuard extends AutoRouteGuard {
|
class AuthGuard extends AutoRouteGuard {
|
||||||
final NetworkService _networkService = NetworkService();
|
final ApiService _apiService;
|
||||||
|
AuthGuard(this._apiService);
|
||||||
@override
|
@override
|
||||||
void onNavigation(NavigationResolver resolver, StackRouter router) async {
|
void onNavigation(NavigationResolver resolver, StackRouter router) async {
|
||||||
try {
|
try {
|
||||||
var res = await _networkService.postRequest(url: 'auth/validateToken');
|
var res = await _apiService.authenticationApi.validateAccessToken();
|
||||||
var jsonReponse = jsonDecode(res.toString());
|
|
||||||
if (jsonReponse['authStatus']) {
|
if (res != null && res.authStatus) {
|
||||||
resolver.next(true);
|
resolver.next(true);
|
||||||
|
} else {
|
||||||
|
router.replaceAll([const LoginRoute()]);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
router.removeUntil((route) => route.name == "LoginRoute");
|
debugPrint("Error [onNavigation] ${e.toString()}");
|
||||||
|
router.replaceAll([const LoginRoute()]);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/modules/backup/views/album_preview_page.dart';
|
import 'package:immich_mobile/modules/backup/views/album_preview_page.dart';
|
||||||
import 'package:immich_mobile/modules/backup/views/backup_album_selection_page.dart';
|
import 'package:immich_mobile/modules/backup/views/backup_album_selection_page.dart';
|
||||||
import 'package:immich_mobile/modules/backup/views/failed_backup_status_page.dart';
|
import 'package:immich_mobile/modules/backup/views/failed_backup_status_page.dart';
|
||||||
|
@ -9,7 +10,6 @@ import 'package:immich_mobile/modules/home/views/home_page.dart';
|
||||||
import 'package:immich_mobile/modules/search/views/search_page.dart';
|
import 'package:immich_mobile/modules/search/views/search_page.dart';
|
||||||
import 'package:immich_mobile/modules/search/views/search_result_page.dart';
|
import 'package:immich_mobile/modules/search/views/search_result_page.dart';
|
||||||
import 'package:immich_mobile/modules/sharing/models/asset_selection_page_result.model.dart';
|
import 'package:immich_mobile/modules/sharing/models/asset_selection_page_result.model.dart';
|
||||||
import 'package:immich_mobile/modules/sharing/models/shared_album.model.dart';
|
|
||||||
import 'package:immich_mobile/modules/sharing/views/album_viewer_page.dart';
|
import 'package:immich_mobile/modules/sharing/views/album_viewer_page.dart';
|
||||||
import 'package:immich_mobile/modules/sharing/views/asset_selection_page.dart';
|
import 'package:immich_mobile/modules/sharing/views/asset_selection_page.dart';
|
||||||
import 'package:immich_mobile/modules/sharing/views/create_shared_album_page.dart';
|
import 'package:immich_mobile/modules/sharing/views/create_shared_album_page.dart';
|
||||||
|
@ -17,12 +17,13 @@ import 'package:immich_mobile/modules/sharing/views/select_additional_user_for_s
|
||||||
import 'package:immich_mobile/modules/sharing/views/select_user_for_sharing_page.dart';
|
import 'package:immich_mobile/modules/sharing/views/select_user_for_sharing_page.dart';
|
||||||
import 'package:immich_mobile/modules/sharing/views/sharing_page.dart';
|
import 'package:immich_mobile/modules/sharing/views/sharing_page.dart';
|
||||||
import 'package:immich_mobile/routing/auth_guard.dart';
|
import 'package:immich_mobile/routing/auth_guard.dart';
|
||||||
import 'package:immich_mobile/shared/models/immich_asset.model.dart';
|
|
||||||
import 'package:immich_mobile/modules/backup/views/backup_controller_page.dart';
|
import 'package:immich_mobile/modules/backup/views/backup_controller_page.dart';
|
||||||
import 'package:immich_mobile/modules/asset_viewer/views/image_viewer_page.dart';
|
import 'package:immich_mobile/modules/asset_viewer/views/image_viewer_page.dart';
|
||||||
|
import 'package:immich_mobile/shared/services/api.service.dart';
|
||||||
import 'package:immich_mobile/shared/views/splash_screen.dart';
|
import 'package:immich_mobile/shared/views/splash_screen.dart';
|
||||||
import 'package:immich_mobile/shared/views/tab_controller_page.dart';
|
import 'package:immich_mobile/shared/views/tab_controller_page.dart';
|
||||||
import 'package:immich_mobile/modules/asset_viewer/views/video_viewer_page.dart';
|
import 'package:immich_mobile/modules/asset_viewer/views/video_viewer_page.dart';
|
||||||
|
import 'package:openapi/api.dart';
|
||||||
import 'package:photo_manager/photo_manager.dart';
|
import 'package:photo_manager/photo_manager.dart';
|
||||||
|
|
||||||
part 'router.gr.dart';
|
part 'router.gr.dart';
|
||||||
|
@ -74,5 +75,9 @@ part 'router.gr.dart';
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
class AppRouter extends _$AppRouter {
|
class AppRouter extends _$AppRouter {
|
||||||
AppRouter() : super(authGuard: AuthGuard());
|
final ApiService _apiService;
|
||||||
|
AppRouter(this._apiService) : super(authGuard: AuthGuard(_apiService));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final appRouterProvider =
|
||||||
|
Provider((ref) => AppRouter(ref.watch(apiServiceProvider)));
|
||||||
|
|
|
@ -234,7 +234,7 @@ class ImageViewerRoute extends PageRouteInfo<ImageViewerRouteArgs> {
|
||||||
required String imageUrl,
|
required String imageUrl,
|
||||||
required String heroTag,
|
required String heroTag,
|
||||||
required String thumbnailUrl,
|
required String thumbnailUrl,
|
||||||
required ImmichAsset asset})
|
required AssetResponseDto asset})
|
||||||
: super(ImageViewerRoute.name,
|
: super(ImageViewerRoute.name,
|
||||||
path: '/image-viewer-page',
|
path: '/image-viewer-page',
|
||||||
args: ImageViewerRouteArgs(
|
args: ImageViewerRouteArgs(
|
||||||
|
@ -263,7 +263,7 @@ class ImageViewerRouteArgs {
|
||||||
|
|
||||||
final String thumbnailUrl;
|
final String thumbnailUrl;
|
||||||
|
|
||||||
final ImmichAsset asset;
|
final AssetResponseDto asset;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
|
@ -275,7 +275,7 @@ class ImageViewerRouteArgs {
|
||||||
/// [VideoViewerPage]
|
/// [VideoViewerPage]
|
||||||
class VideoViewerRoute extends PageRouteInfo<VideoViewerRouteArgs> {
|
class VideoViewerRoute extends PageRouteInfo<VideoViewerRouteArgs> {
|
||||||
VideoViewerRoute(
|
VideoViewerRoute(
|
||||||
{Key? key, required String videoUrl, required ImmichAsset asset})
|
{Key? key, required String videoUrl, required AssetResponseDto asset})
|
||||||
: super(VideoViewerRoute.name,
|
: super(VideoViewerRoute.name,
|
||||||
path: '/video-viewer-page',
|
path: '/video-viewer-page',
|
||||||
args: VideoViewerRouteArgs(
|
args: VideoViewerRouteArgs(
|
||||||
|
@ -292,7 +292,7 @@ class VideoViewerRouteArgs {
|
||||||
|
|
||||||
final String videoUrl;
|
final String videoUrl;
|
||||||
|
|
||||||
final ImmichAsset asset;
|
final AssetResponseDto asset;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
|
@ -390,7 +390,7 @@ class AlbumViewerRouteArgs {
|
||||||
class SelectAdditionalUserForSharingRoute
|
class SelectAdditionalUserForSharingRoute
|
||||||
extends PageRouteInfo<SelectAdditionalUserForSharingRouteArgs> {
|
extends PageRouteInfo<SelectAdditionalUserForSharingRouteArgs> {
|
||||||
SelectAdditionalUserForSharingRoute(
|
SelectAdditionalUserForSharingRoute(
|
||||||
{Key? key, required SharedAlbum albumInfo})
|
{Key? key, required AlbumResponseDto albumInfo})
|
||||||
: super(SelectAdditionalUserForSharingRoute.name,
|
: super(SelectAdditionalUserForSharingRoute.name,
|
||||||
path: '/select-additional-user-for-sharing-page',
|
path: '/select-additional-user-for-sharing-page',
|
||||||
args: SelectAdditionalUserForSharingRouteArgs(
|
args: SelectAdditionalUserForSharingRouteArgs(
|
||||||
|
@ -405,7 +405,7 @@ class SelectAdditionalUserForSharingRouteArgs {
|
||||||
|
|
||||||
final Key? key;
|
final Key? key;
|
||||||
|
|
||||||
final SharedAlbum albumInfo;
|
final AlbumResponseDto albumInfo;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
|
|
|
@ -23,7 +23,9 @@ class TabNavigationObserver extends AutoRouterObserver {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> didChangeTabRoute(
|
Future<void> didChangeTabRoute(
|
||||||
TabPageRoute route, TabPageRoute previousRoute) async {
|
TabPageRoute route,
|
||||||
|
TabPageRoute previousRoute,
|
||||||
|
) async {
|
||||||
// Perform tasks on re-visit to SearchRoute
|
// Perform tasks on re-visit to SearchRoute
|
||||||
if (route.name == 'SearchRoute') {
|
if (route.name == 'SearchRoute') {
|
||||||
// Refresh Location State
|
// Refresh Location State
|
||||||
|
|
|
@ -1,100 +0,0 @@
|
||||||
import 'dart:convert';
|
|
||||||
|
|
||||||
class DeviceInfoRemote {
|
|
||||||
final int id;
|
|
||||||
final String userId;
|
|
||||||
final String deviceId;
|
|
||||||
final String deviceType;
|
|
||||||
final String notificationToken;
|
|
||||||
final String createdAt;
|
|
||||||
final bool isAutoBackup;
|
|
||||||
|
|
||||||
DeviceInfoRemote({
|
|
||||||
required this.id,
|
|
||||||
required this.userId,
|
|
||||||
required this.deviceId,
|
|
||||||
required this.deviceType,
|
|
||||||
required this.notificationToken,
|
|
||||||
required this.createdAt,
|
|
||||||
required this.isAutoBackup,
|
|
||||||
});
|
|
||||||
|
|
||||||
DeviceInfoRemote copyWith({
|
|
||||||
int? id,
|
|
||||||
String? userId,
|
|
||||||
String? deviceId,
|
|
||||||
String? deviceType,
|
|
||||||
String? notificationToken,
|
|
||||||
String? createdAt,
|
|
||||||
bool? isAutoBackup,
|
|
||||||
}) {
|
|
||||||
return DeviceInfoRemote(
|
|
||||||
id: id ?? this.id,
|
|
||||||
userId: userId ?? this.userId,
|
|
||||||
deviceId: deviceId ?? this.deviceId,
|
|
||||||
deviceType: deviceType ?? this.deviceType,
|
|
||||||
notificationToken: notificationToken ?? this.notificationToken,
|
|
||||||
createdAt: createdAt ?? this.createdAt,
|
|
||||||
isAutoBackup: isAutoBackup ?? this.isAutoBackup,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Map<String, dynamic> toMap() {
|
|
||||||
return {
|
|
||||||
'id': id,
|
|
||||||
'userId': userId,
|
|
||||||
'deviceId': deviceId,
|
|
||||||
'deviceType': deviceType,
|
|
||||||
'notificationToken': notificationToken,
|
|
||||||
'createdAt': createdAt,
|
|
||||||
'isAutoBackup': isAutoBackup,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
factory DeviceInfoRemote.fromMap(Map<String, dynamic> map) {
|
|
||||||
return DeviceInfoRemote(
|
|
||||||
id: map['id']?.toInt() ?? 0,
|
|
||||||
userId: map['userId'] ?? '',
|
|
||||||
deviceId: map['deviceId'] ?? '',
|
|
||||||
deviceType: map['deviceType'] ?? '',
|
|
||||||
notificationToken: map['notificationToken'] ?? '',
|
|
||||||
createdAt: map['createdAt'] ?? '',
|
|
||||||
isAutoBackup: map['isAutoBackup'] ?? false,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
String toJson() => json.encode(toMap());
|
|
||||||
|
|
||||||
factory DeviceInfoRemote.fromJson(String source) =>
|
|
||||||
DeviceInfoRemote.fromMap(json.decode(source));
|
|
||||||
|
|
||||||
@override
|
|
||||||
String toString() {
|
|
||||||
return 'DeviceInfo(id: $id, userId: $userId, deviceId: $deviceId, deviceType: $deviceType, notificationToken: $notificationToken, createdAt: $createdAt, isAutoBackup: $isAutoBackup)';
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool operator ==(Object other) {
|
|
||||||
if (identical(this, other)) return true;
|
|
||||||
|
|
||||||
return other is DeviceInfoRemote &&
|
|
||||||
other.id == id &&
|
|
||||||
other.userId == userId &&
|
|
||||||
other.deviceId == deviceId &&
|
|
||||||
other.deviceType == deviceType &&
|
|
||||||
other.notificationToken == notificationToken &&
|
|
||||||
other.createdAt == createdAt &&
|
|
||||||
other.isAutoBackup == isAutoBackup;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
int get hashCode {
|
|
||||||
return id.hashCode ^
|
|
||||||
userId.hashCode ^
|
|
||||||
deviceId.hashCode ^
|
|
||||||
deviceType.hashCode ^
|
|
||||||
notificationToken.hashCode ^
|
|
||||||
createdAt.hashCode ^
|
|
||||||
isAutoBackup.hashCode;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,212 +0,0 @@
|
||||||
import 'dart:convert';
|
|
||||||
|
|
||||||
class ImmichExif {
|
|
||||||
final int? id;
|
|
||||||
final String? assetId;
|
|
||||||
final String? make;
|
|
||||||
final String? model;
|
|
||||||
final String? imageName;
|
|
||||||
final int? exifImageWidth;
|
|
||||||
final int? exifImageHeight;
|
|
||||||
final int? fileSizeInByte;
|
|
||||||
final String? orientation;
|
|
||||||
final String? dateTimeOriginal;
|
|
||||||
final String? modifyDate;
|
|
||||||
final String? lensModel;
|
|
||||||
final double? fNumber;
|
|
||||||
final double? focalLength;
|
|
||||||
final int? iso;
|
|
||||||
final double? exposureTime;
|
|
||||||
final double? latitude;
|
|
||||||
final double? longitude;
|
|
||||||
final String? city;
|
|
||||||
final String? state;
|
|
||||||
final String? country;
|
|
||||||
|
|
||||||
ImmichExif({
|
|
||||||
this.id,
|
|
||||||
this.assetId,
|
|
||||||
this.make,
|
|
||||||
this.model,
|
|
||||||
this.imageName,
|
|
||||||
this.exifImageWidth,
|
|
||||||
this.exifImageHeight,
|
|
||||||
this.fileSizeInByte,
|
|
||||||
this.orientation,
|
|
||||||
this.dateTimeOriginal,
|
|
||||||
this.modifyDate,
|
|
||||||
this.lensModel,
|
|
||||||
this.fNumber,
|
|
||||||
this.focalLength,
|
|
||||||
this.iso,
|
|
||||||
this.exposureTime,
|
|
||||||
this.latitude,
|
|
||||||
this.longitude,
|
|
||||||
this.city,
|
|
||||||
this.state,
|
|
||||||
this.country,
|
|
||||||
});
|
|
||||||
|
|
||||||
ImmichExif copyWith({
|
|
||||||
int? id,
|
|
||||||
String? assetId,
|
|
||||||
String? make,
|
|
||||||
String? model,
|
|
||||||
String? imageName,
|
|
||||||
int? exifImageWidth,
|
|
||||||
int? exifImageHeight,
|
|
||||||
int? fileSizeInByte,
|
|
||||||
String? orientation,
|
|
||||||
String? dateTimeOriginal,
|
|
||||||
String? modifyDate,
|
|
||||||
String? lensModel,
|
|
||||||
double? fNumber,
|
|
||||||
double? focalLength,
|
|
||||||
int? iso,
|
|
||||||
double? exposureTime,
|
|
||||||
double? latitude,
|
|
||||||
double? longitude,
|
|
||||||
String? city,
|
|
||||||
String? state,
|
|
||||||
String? country,
|
|
||||||
}) {
|
|
||||||
return ImmichExif(
|
|
||||||
id: id ?? this.id,
|
|
||||||
assetId: assetId ?? this.assetId,
|
|
||||||
make: make ?? this.make,
|
|
||||||
model: model ?? this.model,
|
|
||||||
imageName: imageName ?? this.imageName,
|
|
||||||
exifImageWidth: exifImageWidth ?? this.exifImageWidth,
|
|
||||||
exifImageHeight: exifImageHeight ?? this.exifImageHeight,
|
|
||||||
fileSizeInByte: fileSizeInByte ?? this.fileSizeInByte,
|
|
||||||
orientation: orientation ?? this.orientation,
|
|
||||||
dateTimeOriginal: dateTimeOriginal ?? this.dateTimeOriginal,
|
|
||||||
modifyDate: modifyDate ?? this.modifyDate,
|
|
||||||
lensModel: lensModel ?? this.lensModel,
|
|
||||||
fNumber: fNumber ?? this.fNumber,
|
|
||||||
focalLength: focalLength ?? this.focalLength,
|
|
||||||
iso: iso ?? this.iso,
|
|
||||||
exposureTime: exposureTime ?? this.exposureTime,
|
|
||||||
latitude: latitude ?? this.latitude,
|
|
||||||
longitude: longitude ?? this.longitude,
|
|
||||||
city: city ?? this.city,
|
|
||||||
state: state ?? this.state,
|
|
||||||
country: country ?? this.country,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Map<String, dynamic> toMap() {
|
|
||||||
return {
|
|
||||||
'id': id,
|
|
||||||
'assetId': assetId,
|
|
||||||
'make': make,
|
|
||||||
'model': model,
|
|
||||||
'imageName': imageName,
|
|
||||||
'exifImageWidth': exifImageWidth,
|
|
||||||
'exifImageHeight': exifImageHeight,
|
|
||||||
'fileSizeInByte': fileSizeInByte,
|
|
||||||
'orientation': orientation,
|
|
||||||
'dateTimeOriginal': dateTimeOriginal,
|
|
||||||
'modifyDate': modifyDate,
|
|
||||||
'lensModel': lensModel,
|
|
||||||
'fNumber': fNumber,
|
|
||||||
'focalLength': focalLength,
|
|
||||||
'iso': iso,
|
|
||||||
'exposureTime': exposureTime,
|
|
||||||
'latitude': latitude,
|
|
||||||
'longitude': longitude,
|
|
||||||
'city': city,
|
|
||||||
'state': state,
|
|
||||||
'country': country,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
factory ImmichExif.fromMap(Map<String, dynamic> map) {
|
|
||||||
return ImmichExif(
|
|
||||||
id: map['id']?.toInt(),
|
|
||||||
assetId: map['assetId'],
|
|
||||||
make: map['make'],
|
|
||||||
model: map['model'],
|
|
||||||
imageName: map['imageName'],
|
|
||||||
exifImageWidth: map['exifImageWidth']?.toInt(),
|
|
||||||
exifImageHeight: map['exifImageHeight']?.toInt(),
|
|
||||||
fileSizeInByte: map['fileSizeInByte']?.toInt(),
|
|
||||||
orientation: map['orientation'],
|
|
||||||
dateTimeOriginal: map['dateTimeOriginal'],
|
|
||||||
modifyDate: map['modifyDate'],
|
|
||||||
lensModel: map['lensModel'],
|
|
||||||
fNumber: map['fNumber']?.toDouble(),
|
|
||||||
focalLength: map['focalLength']?.toDouble(),
|
|
||||||
iso: map['iso']?.toInt(),
|
|
||||||
exposureTime: map['exposureTime']?.toDouble(),
|
|
||||||
latitude: map['latitude']?.toDouble(),
|
|
||||||
longitude: map['longitude']?.toDouble(),
|
|
||||||
city: map['city'],
|
|
||||||
state: map['state'],
|
|
||||||
country: map['country'],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
String toJson() => json.encode(toMap());
|
|
||||||
|
|
||||||
factory ImmichExif.fromJson(String source) =>
|
|
||||||
ImmichExif.fromMap(json.decode(source));
|
|
||||||
|
|
||||||
@override
|
|
||||||
String toString() {
|
|
||||||
return 'ImmichExif(id: $id, assetId: $assetId, make: $make, model: $model, imageName: $imageName, exifImageWidth: $exifImageWidth, exifImageHeight: $exifImageHeight, fileSizeInByte: $fileSizeInByte, orientation: $orientation, dateTimeOriginal: $dateTimeOriginal, modifyDate: $modifyDate, lensModel: $lensModel, fNumber: $fNumber, focalLength: $focalLength, iso: $iso, exposureTime: $exposureTime, latitude: $latitude, longitude: $longitude, city: $city, state: $state, country: $country)';
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool operator ==(Object other) {
|
|
||||||
if (identical(this, other)) return true;
|
|
||||||
|
|
||||||
return other is ImmichExif &&
|
|
||||||
other.id == id &&
|
|
||||||
other.assetId == assetId &&
|
|
||||||
other.make == make &&
|
|
||||||
other.model == model &&
|
|
||||||
other.imageName == imageName &&
|
|
||||||
other.exifImageWidth == exifImageWidth &&
|
|
||||||
other.exifImageHeight == exifImageHeight &&
|
|
||||||
other.fileSizeInByte == fileSizeInByte &&
|
|
||||||
other.orientation == orientation &&
|
|
||||||
other.dateTimeOriginal == dateTimeOriginal &&
|
|
||||||
other.modifyDate == modifyDate &&
|
|
||||||
other.lensModel == lensModel &&
|
|
||||||
other.fNumber == fNumber &&
|
|
||||||
other.focalLength == focalLength &&
|
|
||||||
other.iso == iso &&
|
|
||||||
other.exposureTime == exposureTime &&
|
|
||||||
other.latitude == latitude &&
|
|
||||||
other.longitude == longitude &&
|
|
||||||
other.city == city &&
|
|
||||||
other.state == state &&
|
|
||||||
other.country == country;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
int get hashCode {
|
|
||||||
return id.hashCode ^
|
|
||||||
assetId.hashCode ^
|
|
||||||
make.hashCode ^
|
|
||||||
model.hashCode ^
|
|
||||||
imageName.hashCode ^
|
|
||||||
exifImageWidth.hashCode ^
|
|
||||||
exifImageHeight.hashCode ^
|
|
||||||
fileSizeInByte.hashCode ^
|
|
||||||
orientation.hashCode ^
|
|
||||||
dateTimeOriginal.hashCode ^
|
|
||||||
modifyDate.hashCode ^
|
|
||||||
lensModel.hashCode ^
|
|
||||||
fNumber.hashCode ^
|
|
||||||
focalLength.hashCode ^
|
|
||||||
iso.hashCode ^
|
|
||||||
exposureTime.hashCode ^
|
|
||||||
latitude.hashCode ^
|
|
||||||
longitude.hashCode ^
|
|
||||||
city.hashCode ^
|
|
||||||
state.hashCode ^
|
|
||||||
country.hashCode;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,110 +0,0 @@
|
||||||
import 'dart:convert';
|
|
||||||
|
|
||||||
import 'package:equatable/equatable.dart';
|
|
||||||
|
|
||||||
class ImmichAsset extends Equatable {
|
|
||||||
final String id;
|
|
||||||
final String deviceAssetId;
|
|
||||||
final String userId;
|
|
||||||
final String deviceId;
|
|
||||||
final String type;
|
|
||||||
final String createdAt;
|
|
||||||
final String modifiedAt;
|
|
||||||
final bool isFavorite;
|
|
||||||
final String? duration;
|
|
||||||
final String originalPath;
|
|
||||||
final String resizePath;
|
|
||||||
|
|
||||||
const ImmichAsset({
|
|
||||||
required this.id,
|
|
||||||
required this.deviceAssetId,
|
|
||||||
required this.userId,
|
|
||||||
required this.deviceId,
|
|
||||||
required this.type,
|
|
||||||
required this.createdAt,
|
|
||||||
required this.modifiedAt,
|
|
||||||
required this.isFavorite,
|
|
||||||
this.duration,
|
|
||||||
required this.originalPath,
|
|
||||||
required this.resizePath,
|
|
||||||
});
|
|
||||||
|
|
||||||
ImmichAsset copyWith({
|
|
||||||
String? id,
|
|
||||||
String? deviceAssetId,
|
|
||||||
String? userId,
|
|
||||||
String? deviceId,
|
|
||||||
String? type,
|
|
||||||
String? createdAt,
|
|
||||||
String? modifiedAt,
|
|
||||||
bool? isFavorite,
|
|
||||||
String? duration,
|
|
||||||
String? originalPath,
|
|
||||||
String? resizePath,
|
|
||||||
}) {
|
|
||||||
return ImmichAsset(
|
|
||||||
id: id ?? this.id,
|
|
||||||
deviceAssetId: deviceAssetId ?? this.deviceAssetId,
|
|
||||||
userId: userId ?? this.userId,
|
|
||||||
deviceId: deviceId ?? this.deviceId,
|
|
||||||
type: type ?? this.type,
|
|
||||||
createdAt: createdAt ?? this.createdAt,
|
|
||||||
modifiedAt: modifiedAt ?? this.modifiedAt,
|
|
||||||
isFavorite: isFavorite ?? this.isFavorite,
|
|
||||||
duration: duration ?? this.duration,
|
|
||||||
originalPath: originalPath ?? this.originalPath,
|
|
||||||
resizePath: resizePath ?? this.resizePath,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Map<String, dynamic> toMap() {
|
|
||||||
final result = <String, dynamic>{};
|
|
||||||
|
|
||||||
result.addAll({'id': id});
|
|
||||||
result.addAll({'deviceAssetId': deviceAssetId});
|
|
||||||
result.addAll({'userId': userId});
|
|
||||||
result.addAll({'deviceId': deviceId});
|
|
||||||
result.addAll({'type': type});
|
|
||||||
result.addAll({'createdAt': createdAt});
|
|
||||||
result.addAll({'modifiedAt': modifiedAt});
|
|
||||||
result.addAll({'isFavorite': isFavorite});
|
|
||||||
if (duration != null) {
|
|
||||||
result.addAll({'duration': duration});
|
|
||||||
}
|
|
||||||
result.addAll({'originalPath': originalPath});
|
|
||||||
result.addAll({'resizePath': resizePath});
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
factory ImmichAsset.fromMap(Map<String, dynamic> map) {
|
|
||||||
return ImmichAsset(
|
|
||||||
id: map['id'] ?? '',
|
|
||||||
deviceAssetId: map['deviceAssetId'] ?? '',
|
|
||||||
userId: map['userId'] ?? '',
|
|
||||||
deviceId: map['deviceId'] ?? '',
|
|
||||||
type: map['type'] ?? '',
|
|
||||||
createdAt: map['createdAt'] ?? '',
|
|
||||||
modifiedAt: map['modifiedAt'] ?? '',
|
|
||||||
isFavorite: map['isFavorite'] ?? false,
|
|
||||||
duration: map['duration'],
|
|
||||||
originalPath: map['originalPath'] ?? '',
|
|
||||||
resizePath: map['resizePath'] ?? '',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
String toJson() => json.encode(toMap());
|
|
||||||
|
|
||||||
factory ImmichAsset.fromJson(String source) =>
|
|
||||||
ImmichAsset.fromMap(json.decode(source));
|
|
||||||
|
|
||||||
@override
|
|
||||||
String toString() {
|
|
||||||
return 'ImmichAsset(id: $id, deviceAssetId: $deviceAssetId, userId: $userId, deviceId: $deviceId, type: $type, createdAt: $createdAt, modifiedAt: $modifiedAt, isFavorite: $isFavorite, duration: $duration, originalPath: $originalPath, resizePath: $resizePath)';
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
List<Object> get props {
|
|
||||||
return [id];
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,135 +0,0 @@
|
||||||
import 'dart:convert';
|
|
||||||
|
|
||||||
import 'package:immich_mobile/shared/models/exif.model.dart';
|
|
||||||
|
|
||||||
class ImmichAssetWithExif {
|
|
||||||
final String id;
|
|
||||||
final String deviceAssetId;
|
|
||||||
final String userId;
|
|
||||||
final String deviceId;
|
|
||||||
final String type;
|
|
||||||
final String createdAt;
|
|
||||||
final String modifiedAt;
|
|
||||||
final String originalPath;
|
|
||||||
final bool isFavorite;
|
|
||||||
final String? duration;
|
|
||||||
final ImmichExif? exifInfo;
|
|
||||||
|
|
||||||
ImmichAssetWithExif({
|
|
||||||
required this.id,
|
|
||||||
required this.deviceAssetId,
|
|
||||||
required this.userId,
|
|
||||||
required this.deviceId,
|
|
||||||
required this.type,
|
|
||||||
required this.createdAt,
|
|
||||||
required this.modifiedAt,
|
|
||||||
required this.originalPath,
|
|
||||||
required this.isFavorite,
|
|
||||||
this.duration,
|
|
||||||
this.exifInfo,
|
|
||||||
});
|
|
||||||
|
|
||||||
ImmichAssetWithExif copyWith({
|
|
||||||
String? id,
|
|
||||||
String? deviceAssetId,
|
|
||||||
String? userId,
|
|
||||||
String? deviceId,
|
|
||||||
String? type,
|
|
||||||
String? createdAt,
|
|
||||||
String? modifiedAt,
|
|
||||||
String? originalPath,
|
|
||||||
bool? isFavorite,
|
|
||||||
String? duration,
|
|
||||||
ImmichExif? exifInfo,
|
|
||||||
}) {
|
|
||||||
return ImmichAssetWithExif(
|
|
||||||
id: id ?? this.id,
|
|
||||||
deviceAssetId: deviceAssetId ?? this.deviceAssetId,
|
|
||||||
userId: userId ?? this.userId,
|
|
||||||
deviceId: deviceId ?? this.deviceId,
|
|
||||||
type: type ?? this.type,
|
|
||||||
createdAt: createdAt ?? this.createdAt,
|
|
||||||
modifiedAt: modifiedAt ?? this.modifiedAt,
|
|
||||||
originalPath: originalPath ?? this.originalPath,
|
|
||||||
isFavorite: isFavorite ?? this.isFavorite,
|
|
||||||
duration: duration ?? this.duration,
|
|
||||||
exifInfo: exifInfo ?? this.exifInfo,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Map<String, dynamic> toMap() {
|
|
||||||
return {
|
|
||||||
'id': id,
|
|
||||||
'deviceAssetId': deviceAssetId,
|
|
||||||
'userId': userId,
|
|
||||||
'deviceId': deviceId,
|
|
||||||
'type': type,
|
|
||||||
'createdAt': createdAt,
|
|
||||||
'modifiedAt': modifiedAt,
|
|
||||||
'originalPath': originalPath,
|
|
||||||
'isFavorite': isFavorite,
|
|
||||||
'duration': duration,
|
|
||||||
'exifInfo': exifInfo?.toMap(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
factory ImmichAssetWithExif.fromMap(Map<String, dynamic> map) {
|
|
||||||
return ImmichAssetWithExif(
|
|
||||||
id: map['id'] ?? '',
|
|
||||||
deviceAssetId: map['deviceAssetId'] ?? '',
|
|
||||||
userId: map['userId'] ?? '',
|
|
||||||
deviceId: map['deviceId'] ?? '',
|
|
||||||
type: map['type'] ?? '',
|
|
||||||
createdAt: map['createdAt'] ?? '',
|
|
||||||
modifiedAt: map['modifiedAt'] ?? '',
|
|
||||||
originalPath: map['originalPath'] ?? '',
|
|
||||||
isFavorite: map['isFavorite'] ?? false,
|
|
||||||
duration: map['duration'],
|
|
||||||
exifInfo:
|
|
||||||
map['exifInfo'] != null ? ImmichExif.fromMap(map['exifInfo']) : null,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
String toJson() => json.encode(toMap());
|
|
||||||
|
|
||||||
factory ImmichAssetWithExif.fromJson(String source) =>
|
|
||||||
ImmichAssetWithExif.fromMap(json.decode(source));
|
|
||||||
|
|
||||||
@override
|
|
||||||
String toString() {
|
|
||||||
return 'ImmichAssetWithExif(id: $id, deviceAssetId: $deviceAssetId, userId: $userId, deviceId: $deviceId, type: $type, createdAt: $createdAt, modifiedAt: $modifiedAt, originalPath: $originalPath, isFavorite: $isFavorite, duration: $duration, exifInfo: $exifInfo)';
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool operator ==(Object other) {
|
|
||||||
if (identical(this, other)) return true;
|
|
||||||
|
|
||||||
return other is ImmichAssetWithExif &&
|
|
||||||
other.id == id &&
|
|
||||||
other.deviceAssetId == deviceAssetId &&
|
|
||||||
other.userId == userId &&
|
|
||||||
other.deviceId == deviceId &&
|
|
||||||
other.type == type &&
|
|
||||||
other.createdAt == createdAt &&
|
|
||||||
other.modifiedAt == modifiedAt &&
|
|
||||||
other.originalPath == originalPath &&
|
|
||||||
other.isFavorite == isFavorite &&
|
|
||||||
other.duration == duration &&
|
|
||||||
other.exifInfo == exifInfo;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
int get hashCode {
|
|
||||||
return id.hashCode ^
|
|
||||||
deviceAssetId.hashCode ^
|
|
||||||
userId.hashCode ^
|
|
||||||
deviceId.hashCode ^
|
|
||||||
type.hashCode ^
|
|
||||||
createdAt.hashCode ^
|
|
||||||
modifiedAt.hashCode ^
|
|
||||||
originalPath.hashCode ^
|
|
||||||
isFavorite.hashCode ^
|
|
||||||
duration.hashCode ^
|
|
||||||
exifInfo.hashCode;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,55 +0,0 @@
|
||||||
import 'dart:convert';
|
|
||||||
|
|
||||||
class MapboxInfo {
|
|
||||||
final bool isEnable;
|
|
||||||
final String mapboxSecret;
|
|
||||||
MapboxInfo({
|
|
||||||
required this.isEnable,
|
|
||||||
required this.mapboxSecret,
|
|
||||||
});
|
|
||||||
|
|
||||||
MapboxInfo copyWith({
|
|
||||||
bool? isEnable,
|
|
||||||
String? mapboxSecret,
|
|
||||||
}) {
|
|
||||||
return MapboxInfo(
|
|
||||||
isEnable: isEnable ?? this.isEnable,
|
|
||||||
mapboxSecret: mapboxSecret ?? this.mapboxSecret,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Map<String, dynamic> toMap() {
|
|
||||||
return {
|
|
||||||
'isEnable': isEnable,
|
|
||||||
'mapboxSecret': mapboxSecret,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
factory MapboxInfo.fromMap(Map<String, dynamic> map) {
|
|
||||||
return MapboxInfo(
|
|
||||||
isEnable: map['isEnable'] ?? false,
|
|
||||||
mapboxSecret: map['mapboxSecret'] ?? '',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
String toJson() => json.encode(toMap());
|
|
||||||
|
|
||||||
factory MapboxInfo.fromJson(String source) =>
|
|
||||||
MapboxInfo.fromMap(json.decode(source));
|
|
||||||
|
|
||||||
@override
|
|
||||||
String toString() =>
|
|
||||||
'MapboxInfo(isEnable: $isEnable, mapboxSecret: $mapboxSecret)';
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool operator ==(Object other) {
|
|
||||||
if (identical(this, other)) return true;
|
|
||||||
|
|
||||||
return other is MapboxInfo &&
|
|
||||||
other.isEnable == isEnable &&
|
|
||||||
other.mapboxSecret == mapboxSecret;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
int get hashCode => isEnable.hashCode ^ mapboxSecret.hashCode;
|
|
||||||
}
|
|
|
@ -1,99 +0,0 @@
|
||||||
import 'dart:convert';
|
|
||||||
|
|
||||||
class ServerInfo {
|
|
||||||
final String diskSize;
|
|
||||||
final String diskUse;
|
|
||||||
final String diskAvailable;
|
|
||||||
final int diskSizeRaw;
|
|
||||||
final int diskUseRaw;
|
|
||||||
final int diskAvailableRaw;
|
|
||||||
final double diskUsagePercentage;
|
|
||||||
ServerInfo({
|
|
||||||
required this.diskSize,
|
|
||||||
required this.diskUse,
|
|
||||||
required this.diskAvailable,
|
|
||||||
required this.diskSizeRaw,
|
|
||||||
required this.diskUseRaw,
|
|
||||||
required this.diskAvailableRaw,
|
|
||||||
required this.diskUsagePercentage,
|
|
||||||
});
|
|
||||||
|
|
||||||
ServerInfo copyWith({
|
|
||||||
String? diskSize,
|
|
||||||
String? diskUse,
|
|
||||||
String? diskAvailable,
|
|
||||||
int? diskSizeRaw,
|
|
||||||
int? diskUseRaw,
|
|
||||||
int? diskAvailableRaw,
|
|
||||||
double? diskUsagePercentage,
|
|
||||||
}) {
|
|
||||||
return ServerInfo(
|
|
||||||
diskSize: diskSize ?? this.diskSize,
|
|
||||||
diskUse: diskUse ?? this.diskUse,
|
|
||||||
diskAvailable: diskAvailable ?? this.diskAvailable,
|
|
||||||
diskSizeRaw: diskSizeRaw ?? this.diskSizeRaw,
|
|
||||||
diskUseRaw: diskUseRaw ?? this.diskUseRaw,
|
|
||||||
diskAvailableRaw: diskAvailableRaw ?? this.diskAvailableRaw,
|
|
||||||
diskUsagePercentage: diskUsagePercentage ?? this.diskUsagePercentage,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Map<String, dynamic> toMap() {
|
|
||||||
return {
|
|
||||||
'diskSize': diskSize,
|
|
||||||
'diskUse': diskUse,
|
|
||||||
'diskAvailable': diskAvailable,
|
|
||||||
'diskSizeRaw': diskSizeRaw,
|
|
||||||
'diskUseRaw': diskUseRaw,
|
|
||||||
'diskAvailableRaw': diskAvailableRaw,
|
|
||||||
'diskUsagePercentage': diskUsagePercentage,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
factory ServerInfo.fromMap(Map<String, dynamic> map) {
|
|
||||||
return ServerInfo(
|
|
||||||
diskSize: map['diskSize'] ?? '',
|
|
||||||
diskUse: map['diskUse'] ?? '',
|
|
||||||
diskAvailable: map['diskAvailable'] ?? '',
|
|
||||||
diskSizeRaw: map['diskSizeRaw']?.toInt() ?? 0,
|
|
||||||
diskUseRaw: map['diskUseRaw']?.toInt() ?? 0,
|
|
||||||
diskAvailableRaw: map['diskAvailableRaw']?.toInt() ?? 0,
|
|
||||||
diskUsagePercentage: map['diskUsagePercentage']?.toDouble() ?? 0.0,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
String toJson() => json.encode(toMap());
|
|
||||||
|
|
||||||
factory ServerInfo.fromJson(String source) =>
|
|
||||||
ServerInfo.fromMap(json.decode(source));
|
|
||||||
|
|
||||||
@override
|
|
||||||
String toString() {
|
|
||||||
return 'ServerInfo(diskSize: $diskSize, diskUse: $diskUse, diskAvailable: $diskAvailable, diskSizeRaw: $diskSizeRaw, diskUseRaw: $diskUseRaw, diskAvailableRaw: $diskAvailableRaw, diskUsagePercentage: $diskUsagePercentage)';
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool operator ==(Object other) {
|
|
||||||
if (identical(this, other)) return true;
|
|
||||||
|
|
||||||
return other is ServerInfo &&
|
|
||||||
other.diskSize == diskSize &&
|
|
||||||
other.diskUse == diskUse &&
|
|
||||||
other.diskAvailable == diskAvailable &&
|
|
||||||
other.diskSizeRaw == diskSizeRaw &&
|
|
||||||
other.diskUseRaw == diskUseRaw &&
|
|
||||||
other.diskAvailableRaw == diskAvailableRaw &&
|
|
||||||
other.diskUsagePercentage == diskUsagePercentage;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
int get hashCode {
|
|
||||||
return diskSize.hashCode ^
|
|
||||||
diskUse.hashCode ^
|
|
||||||
diskAvailable.hashCode ^
|
|
||||||
diskSizeRaw.hashCode ^
|
|
||||||
diskUseRaw.hashCode ^
|
|
||||||
diskAvailableRaw.hashCode ^
|
|
||||||
diskUsagePercentage.hashCode;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,29 +1,22 @@
|
||||||
import 'dart:convert';
|
import 'package:openapi/api.dart';
|
||||||
|
|
||||||
import 'package:immich_mobile/shared/models/mapbox_info.model.dart';
|
|
||||||
import 'package:immich_mobile/shared/models/server_version.model.dart';
|
|
||||||
|
|
||||||
class ServerInfoState {
|
class ServerInfoState {
|
||||||
final MapboxInfo mapboxInfo;
|
final ServerVersionReponseDto serverVersion;
|
||||||
final ServerVersion serverVersion;
|
|
||||||
final bool isVersionMismatch;
|
final bool isVersionMismatch;
|
||||||
final String versionMismatchErrorMessage;
|
final String versionMismatchErrorMessage;
|
||||||
|
|
||||||
ServerInfoState({
|
ServerInfoState({
|
||||||
required this.mapboxInfo,
|
|
||||||
required this.serverVersion,
|
required this.serverVersion,
|
||||||
required this.isVersionMismatch,
|
required this.isVersionMismatch,
|
||||||
required this.versionMismatchErrorMessage,
|
required this.versionMismatchErrorMessage,
|
||||||
});
|
});
|
||||||
|
|
||||||
ServerInfoState copyWith({
|
ServerInfoState copyWith({
|
||||||
MapboxInfo? mapboxInfo,
|
ServerVersionReponseDto? serverVersion,
|
||||||
ServerVersion? serverVersion,
|
|
||||||
bool? isVersionMismatch,
|
bool? isVersionMismatch,
|
||||||
String? versionMismatchErrorMessage,
|
String? versionMismatchErrorMessage,
|
||||||
}) {
|
}) {
|
||||||
return ServerInfoState(
|
return ServerInfoState(
|
||||||
mapboxInfo: mapboxInfo ?? this.mapboxInfo,
|
|
||||||
serverVersion: serverVersion ?? this.serverVersion,
|
serverVersion: serverVersion ?? this.serverVersion,
|
||||||
isVersionMismatch: isVersionMismatch ?? this.isVersionMismatch,
|
isVersionMismatch: isVersionMismatch ?? this.isVersionMismatch,
|
||||||
versionMismatchErrorMessage:
|
versionMismatchErrorMessage:
|
||||||
|
@ -31,32 +24,9 @@ class ServerInfoState {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<String, dynamic> toMap() {
|
|
||||||
return {
|
|
||||||
'mapboxInfo': mapboxInfo.toMap(),
|
|
||||||
'serverVersion': serverVersion.toMap(),
|
|
||||||
'isVersionMismatch': isVersionMismatch,
|
|
||||||
'versionMismatchErrorMessage': versionMismatchErrorMessage,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
factory ServerInfoState.fromMap(Map<String, dynamic> map) {
|
|
||||||
return ServerInfoState(
|
|
||||||
mapboxInfo: MapboxInfo.fromMap(map['mapboxInfo']),
|
|
||||||
serverVersion: ServerVersion.fromMap(map['serverVersion']),
|
|
||||||
isVersionMismatch: map['isVersionMismatch'] ?? false,
|
|
||||||
versionMismatchErrorMessage: map['versionMismatchErrorMessage'] ?? '',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
String toJson() => json.encode(toMap());
|
|
||||||
|
|
||||||
factory ServerInfoState.fromJson(String source) =>
|
|
||||||
ServerInfoState.fromMap(json.decode(source));
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return 'ServerInfoState(mapboxInfo: $mapboxInfo, serverVersion: $serverVersion, isVersionMismatch: $isVersionMismatch, versionMismatchErrorMessage: $versionMismatchErrorMessage)';
|
return 'ServerInfoState( serverVersion: $serverVersion, isVersionMismatch: $isVersionMismatch, versionMismatchErrorMessage: $versionMismatchErrorMessage)';
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -64,7 +34,6 @@ class ServerInfoState {
|
||||||
if (identical(this, other)) return true;
|
if (identical(this, other)) return true;
|
||||||
|
|
||||||
return other is ServerInfoState &&
|
return other is ServerInfoState &&
|
||||||
other.mapboxInfo == mapboxInfo &&
|
|
||||||
other.serverVersion == serverVersion &&
|
other.serverVersion == serverVersion &&
|
||||||
other.isVersionMismatch == isVersionMismatch &&
|
other.isVersionMismatch == isVersionMismatch &&
|
||||||
other.versionMismatchErrorMessage == versionMismatchErrorMessage;
|
other.versionMismatchErrorMessage == versionMismatchErrorMessage;
|
||||||
|
@ -72,8 +41,7 @@ class ServerInfoState {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get hashCode {
|
int get hashCode {
|
||||||
return mapboxInfo.hashCode ^
|
return serverVersion.hashCode ^
|
||||||
serverVersion.hashCode ^
|
|
||||||
isVersionMismatch.hashCode ^
|
isVersionMismatch.hashCode ^
|
||||||
versionMismatchErrorMessage.hashCode;
|
versionMismatchErrorMessage.hashCode;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,73 +0,0 @@
|
||||||
import 'dart:convert';
|
|
||||||
|
|
||||||
class ServerVersion {
|
|
||||||
final int major;
|
|
||||||
final int minor;
|
|
||||||
final int patch;
|
|
||||||
final int build;
|
|
||||||
|
|
||||||
ServerVersion({
|
|
||||||
required this.major,
|
|
||||||
required this.minor,
|
|
||||||
required this.patch,
|
|
||||||
required this.build,
|
|
||||||
});
|
|
||||||
|
|
||||||
ServerVersion copyWith({
|
|
||||||
int? major,
|
|
||||||
int? minor,
|
|
||||||
int? patch,
|
|
||||||
int? build,
|
|
||||||
}) {
|
|
||||||
return ServerVersion(
|
|
||||||
major: major ?? this.major,
|
|
||||||
minor: minor ?? this.minor,
|
|
||||||
patch: patch ?? this.patch,
|
|
||||||
build: build ?? this.build,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Map<String, dynamic> toMap() {
|
|
||||||
return {
|
|
||||||
'major': major,
|
|
||||||
'minor': minor,
|
|
||||||
'patch': patch,
|
|
||||||
'build': build,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
factory ServerVersion.fromMap(Map<String, dynamic> map) {
|
|
||||||
return ServerVersion(
|
|
||||||
major: map['major']?.toInt() ?? 0,
|
|
||||||
minor: map['minor']?.toInt() ?? 0,
|
|
||||||
patch: map['patch']?.toInt() ?? 0,
|
|
||||||
build: map['build']?.toInt() ?? 0,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
String toJson() => json.encode(toMap());
|
|
||||||
|
|
||||||
factory ServerVersion.fromJson(String source) =>
|
|
||||||
ServerVersion.fromMap(json.decode(source));
|
|
||||||
|
|
||||||
@override
|
|
||||||
String toString() {
|
|
||||||
return 'ServerVersion(major: $major, minor: $minor, patch: $patch, build: $build)';
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool operator ==(Object other) {
|
|
||||||
if (identical(this, other)) return true;
|
|
||||||
|
|
||||||
return other is ServerVersion &&
|
|
||||||
other.major == major &&
|
|
||||||
other.minor == minor &&
|
|
||||||
other.patch == patch &&
|
|
||||||
other.build == build;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
int get hashCode {
|
|
||||||
return major.hashCode ^ minor.hashCode ^ patch.hashCode ^ build.hashCode;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,76 +0,0 @@
|
||||||
import 'dart:convert';
|
|
||||||
|
|
||||||
class User {
|
|
||||||
final String id;
|
|
||||||
final String email;
|
|
||||||
final String createdAt;
|
|
||||||
final String firstName;
|
|
||||||
final String lastName;
|
|
||||||
|
|
||||||
User({
|
|
||||||
required this.id,
|
|
||||||
required this.email,
|
|
||||||
required this.createdAt,
|
|
||||||
required this.firstName,
|
|
||||||
required this.lastName,
|
|
||||||
});
|
|
||||||
|
|
||||||
User copyWith({
|
|
||||||
String? id,
|
|
||||||
String? email,
|
|
||||||
String? createdAt,
|
|
||||||
String? firstName,
|
|
||||||
String? lastName,
|
|
||||||
}) {
|
|
||||||
return User(
|
|
||||||
id: id ?? this.id,
|
|
||||||
email: email ?? this.email,
|
|
||||||
createdAt: createdAt ?? this.createdAt,
|
|
||||||
firstName: firstName ?? this.firstName,
|
|
||||||
lastName: lastName ?? this.lastName,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Map<String, dynamic> toMap() {
|
|
||||||
final result = <String, dynamic>{};
|
|
||||||
|
|
||||||
result.addAll({'id': id});
|
|
||||||
result.addAll({'email': email});
|
|
||||||
result.addAll({'createdAt': createdAt});
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
factory User.fromMap(Map<String, dynamic> map) {
|
|
||||||
return User(
|
|
||||||
id: map['id'] ?? '',
|
|
||||||
email: map['email'] ?? '',
|
|
||||||
createdAt: map['createdAt'] ?? '',
|
|
||||||
firstName: map['firstName'] ?? '',
|
|
||||||
lastName: map['lastName'] ?? '',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
String toJson() => json.encode(toMap());
|
|
||||||
|
|
||||||
factory User.fromJson(String source) => User.fromMap(json.decode(source));
|
|
||||||
|
|
||||||
@override
|
|
||||||
String toString() =>
|
|
||||||
'UserInfo(id: $id, email: $email, createdAt: $createdAt)';
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool operator ==(Object other) {
|
|
||||||
if (identical(this, other)) return true;
|
|
||||||
|
|
||||||
return other is User &&
|
|
||||||
other.id == id &&
|
|
||||||
other.email == email &&
|
|
||||||
other.createdAt == createdAt &&
|
|
||||||
other.firstName == firstName &&
|
|
||||||
other.lastName == lastName;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
int get hashCode => id.hashCode ^ email.hashCode ^ createdAt.hashCode;
|
|
||||||
}
|
|
|
@ -1,21 +1,20 @@
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/modules/home/models/delete_asset_response.model.dart';
|
|
||||||
import 'package:immich_mobile/modules/home/services/asset.service.dart';
|
import 'package:immich_mobile/modules/home/services/asset.service.dart';
|
||||||
import 'package:immich_mobile/shared/models/immich_asset.model.dart';
|
|
||||||
import 'package:immich_mobile/shared/services/device_info.service.dart';
|
import 'package:immich_mobile/shared/services/device_info.service.dart';
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
|
import 'package:openapi/api.dart';
|
||||||
import 'package:photo_manager/photo_manager.dart';
|
import 'package:photo_manager/photo_manager.dart';
|
||||||
|
|
||||||
class AssetNotifier extends StateNotifier<List<ImmichAsset>> {
|
class AssetNotifier extends StateNotifier<List<AssetResponseDto>> {
|
||||||
final AssetService _assetService;
|
final AssetService _assetService;
|
||||||
final DeviceInfoService _deviceInfoService = DeviceInfoService();
|
final DeviceInfoService _deviceInfoService = DeviceInfoService();
|
||||||
|
|
||||||
AssetNotifier(this._assetService) : super([]);
|
AssetNotifier(this._assetService) : super([]);
|
||||||
|
|
||||||
getAllAsset() async {
|
getAllAsset() async {
|
||||||
List<ImmichAsset>? allAssets = await _assetService.getAllAsset();
|
var allAssets = await _assetService.getAllAsset();
|
||||||
|
|
||||||
if (allAssets != null) {
|
if (allAssets != null) {
|
||||||
state = allAssets;
|
state = allAssets;
|
||||||
|
@ -26,11 +25,11 @@ class AssetNotifier extends StateNotifier<List<ImmichAsset>> {
|
||||||
state = [];
|
state = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
onNewAssetUploaded(ImmichAsset newAsset) {
|
onNewAssetUploaded(AssetResponseDto newAsset) {
|
||||||
state = [...state, newAsset];
|
state = [...state, newAsset];
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteAssets(Set<ImmichAsset> deleteAssets) async {
|
deleteAssets(Set<AssetResponseDto> deleteAssets) async {
|
||||||
var deviceInfo = await _deviceInfoService.getDeviceInfo();
|
var deviceInfo = await _deviceInfoService.getDeviceInfo();
|
||||||
var deviceId = deviceInfo["deviceId"];
|
var deviceId = deviceInfo["deviceId"];
|
||||||
var deleteIdList = <String>[];
|
var deleteIdList = <String>[];
|
||||||
|
@ -53,14 +52,15 @@ class AssetNotifier extends StateNotifier<List<ImmichAsset>> {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete asset on server
|
// Delete asset on server
|
||||||
List<DeleteAssetResponse>? deleteAssetResult =
|
List<DeleteAssetResponseDto>? deleteAssetResult =
|
||||||
await _assetService.deleteAssets(deleteAssets);
|
await _assetService.deleteAssets(deleteAssets);
|
||||||
|
|
||||||
if (deleteAssetResult == null) {
|
if (deleteAssetResult == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (var asset in deleteAssetResult) {
|
for (var asset in deleteAssetResult) {
|
||||||
if (asset.status == 'success') {
|
if (asset.status == DeleteAssetStatus.SUCCESS) {
|
||||||
state =
|
state =
|
||||||
state.where((immichAsset) => immichAsset.id != asset.id).toList();
|
state.where((immichAsset) => immichAsset.id != asset.id).toList();
|
||||||
}
|
}
|
||||||
|
@ -69,7 +69,7 @@ class AssetNotifier extends StateNotifier<List<ImmichAsset>> {
|
||||||
}
|
}
|
||||||
|
|
||||||
final assetProvider =
|
final assetProvider =
|
||||||
StateNotifierProvider<AssetNotifier, List<ImmichAsset>>((ref) {
|
StateNotifierProvider<AssetNotifier, List<AssetResponseDto>>((ref) {
|
||||||
return AssetNotifier(ref.watch(assetServiceProvider));
|
return AssetNotifier(ref.watch(assetServiceProvider));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -77,17 +77,25 @@ final assetGroupByDateTimeProvider = StateProvider((ref) {
|
||||||
var assets = ref.watch(assetProvider);
|
var assets = ref.watch(assetProvider);
|
||||||
|
|
||||||
assets.sortByCompare<DateTime>(
|
assets.sortByCompare<DateTime>(
|
||||||
(e) => DateTime.parse(e.createdAt), (a, b) => b.compareTo(a));
|
(e) => DateTime.parse(e.createdAt),
|
||||||
return assets.groupListsBy((element) =>
|
(a, b) => b.compareTo(a),
|
||||||
DateFormat('y-MM-dd').format(DateTime.parse(element.createdAt)));
|
);
|
||||||
|
return assets.groupListsBy(
|
||||||
|
(element) =>
|
||||||
|
DateFormat('y-MM-dd').format(DateTime.parse(element.createdAt)),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
final assetGroupByMonthYearProvider = StateProvider((ref) {
|
final assetGroupByMonthYearProvider = StateProvider((ref) {
|
||||||
var assets = ref.watch(assetProvider);
|
var assets = ref.watch(assetProvider);
|
||||||
|
|
||||||
assets.sortByCompare<DateTime>(
|
assets.sortByCompare<DateTime>(
|
||||||
(e) => DateTime.parse(e.createdAt), (a, b) => b.compareTo(a));
|
(e) => DateTime.parse(e.createdAt),
|
||||||
|
(a, b) => b.compareTo(a),
|
||||||
|
);
|
||||||
|
|
||||||
return assets.groupListsBy((element) =>
|
return assets.groupListsBy(
|
||||||
DateFormat('MMMM, y').format(DateTime.parse(element.createdAt)));
|
(element) =>
|
||||||
|
DateFormat('MMMM, y').format(DateTime.parse(element.createdAt)),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
|
@ -56,4 +56,5 @@ class ReleaseInfoNotifier extends StateNotifier<String> {
|
||||||
}
|
}
|
||||||
|
|
||||||
final releaseInfoProvider = StateNotifierProvider<ReleaseInfoNotifier, String>(
|
final releaseInfoProvider = StateNotifierProvider<ReleaseInfoNotifier, String>(
|
||||||
(ref) => ReleaseInfoNotifier());
|
(ref) => ReleaseInfoNotifier(),
|
||||||
|
);
|
||||||
|
|
|
@ -1,18 +1,20 @@
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
|
||||||
import 'package:immich_mobile/shared/models/mapbox_info.model.dart';
|
|
||||||
import 'package:immich_mobile/shared/models/server_info_state.model.dart';
|
import 'package:immich_mobile/shared/models/server_info_state.model.dart';
|
||||||
import 'package:immich_mobile/shared/models/server_version.model.dart';
|
|
||||||
import 'package:immich_mobile/shared/services/server_info.service.dart';
|
import 'package:immich_mobile/shared/services/server_info.service.dart';
|
||||||
|
import 'package:openapi/api.dart';
|
||||||
import 'package:package_info_plus/package_info_plus.dart';
|
import 'package:package_info_plus/package_info_plus.dart';
|
||||||
|
|
||||||
class ServerInfoNotifier extends StateNotifier<ServerInfoState> {
|
class ServerInfoNotifier extends StateNotifier<ServerInfoState> {
|
||||||
ServerInfoNotifier(this._serverInfoService)
|
ServerInfoNotifier(this._serverInfoService)
|
||||||
: super(
|
: super(
|
||||||
ServerInfoState(
|
ServerInfoState(
|
||||||
mapboxInfo: MapboxInfo(isEnable: false, mapboxSecret: ""),
|
serverVersion: ServerVersionReponseDto(
|
||||||
serverVersion:
|
major: 0,
|
||||||
ServerVersion(major: 0, patch: 0, minor: 0, build: 0),
|
patch_: 0,
|
||||||
|
minor: 0,
|
||||||
|
build: 0,
|
||||||
|
),
|
||||||
isVersionMismatch: false,
|
isVersionMismatch: false,
|
||||||
versionMismatchErrorMessage: "",
|
versionMismatchErrorMessage: "",
|
||||||
),
|
),
|
||||||
|
@ -21,7 +23,8 @@ class ServerInfoNotifier extends StateNotifier<ServerInfoState> {
|
||||||
final ServerInfoService _serverInfoService;
|
final ServerInfoService _serverInfoService;
|
||||||
|
|
||||||
getServerVersion() async {
|
getServerVersion() async {
|
||||||
ServerVersion? serverVersion = await _serverInfoService.getServerVersion();
|
ServerVersionReponseDto? serverVersion =
|
||||||
|
await _serverInfoService.getServerVersion();
|
||||||
|
|
||||||
if (serverVersion == null) {
|
if (serverVersion == null) {
|
||||||
state = state.copyWith(
|
state = state.copyWith(
|
||||||
|
@ -59,7 +62,9 @@ class ServerInfoNotifier extends StateNotifier<ServerInfoState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
state = state.copyWith(
|
state = state.copyWith(
|
||||||
isVersionMismatch: false, versionMismatchErrorMessage: "");
|
isVersionMismatch: false,
|
||||||
|
versionMismatchErrorMessage: "",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<String, int> _getDetailVersion(String version) {
|
Map<String, int> _getDetailVersion(String version) {
|
||||||
|
|
|
@ -5,8 +5,8 @@ import 'package:hive/hive.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/constants/hive_box.dart';
|
import 'package:immich_mobile/constants/hive_box.dart';
|
||||||
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
|
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
|
||||||
import 'package:immich_mobile/shared/models/immich_asset.model.dart';
|
|
||||||
import 'package:immich_mobile/shared/providers/asset.provider.dart';
|
import 'package:immich_mobile/shared/providers/asset.provider.dart';
|
||||||
|
import 'package:openapi/api.dart';
|
||||||
import 'package:socket_io_client/socket_io_client.dart';
|
import 'package:socket_io_client/socket_io_client.dart';
|
||||||
|
|
||||||
class WebscoketState {
|
class WebscoketState {
|
||||||
|
@ -92,8 +92,11 @@ class WebsocketNotifier extends StateNotifier<WebscoketState> {
|
||||||
|
|
||||||
socket.on('on_upload_success', (data) {
|
socket.on('on_upload_success', (data) {
|
||||||
var jsonString = jsonDecode(data.toString());
|
var jsonString = jsonDecode(data.toString());
|
||||||
ImmichAsset newAsset = ImmichAsset.fromMap(jsonString);
|
AssetResponseDto? newAsset = AssetResponseDto.fromJson(jsonString);
|
||||||
ref.watch(assetProvider.notifier).onNewAssetUploaded(newAsset);
|
|
||||||
|
if (newAsset != null) {
|
||||||
|
ref.watch(assetProvider.notifier).onNewAssetUploaded(newAsset);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint("[WEBSOCKET] Catch Websocket Error - ${e.toString()}");
|
debugPrint("[WEBSOCKET] Catch Websocket Error - ${e.toString()}");
|
||||||
|
@ -119,8 +122,11 @@ class WebsocketNotifier extends StateNotifier<WebscoketState> {
|
||||||
debugPrint("[Websocket] Start listening to event on_upload_success");
|
debugPrint("[Websocket] Start listening to event on_upload_success");
|
||||||
state.socket?.on('on_upload_success', (data) {
|
state.socket?.on('on_upload_success', (data) {
|
||||||
var jsonString = jsonDecode(data.toString());
|
var jsonString = jsonDecode(data.toString());
|
||||||
ImmichAsset newAsset = ImmichAsset.fromMap(jsonString);
|
AssetResponseDto? newAsset = AssetResponseDto.fromJson(jsonString);
|
||||||
ref.watch(assetProvider.notifier).onNewAssetUploaded(newAsset);
|
|
||||||
|
if (newAsset != null) {
|
||||||
|
ref.watch(assetProvider.notifier).onNewAssetUploaded(newAsset);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
30
mobile/lib/shared/services/api.service.dart
Normal file
30
mobile/lib/shared/services/api.service.dart
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:openapi/api.dart';
|
||||||
|
|
||||||
|
final apiServiceProvider = Provider((ref) => ApiService());
|
||||||
|
|
||||||
|
class ApiService {
|
||||||
|
late ApiClient _apiClient;
|
||||||
|
|
||||||
|
late UserApi userApi;
|
||||||
|
late AuthenticationApi authenticationApi;
|
||||||
|
late AlbumApi albumApi;
|
||||||
|
late AssetApi assetApi;
|
||||||
|
late ServerInfoApi serverInfoApi;
|
||||||
|
late DeviceInfoApi deviceInfoApi;
|
||||||
|
|
||||||
|
setEndpoint(String endpoint) {
|
||||||
|
_apiClient = ApiClient(basePath: endpoint);
|
||||||
|
|
||||||
|
userApi = UserApi(_apiClient);
|
||||||
|
authenticationApi = AuthenticationApi(_apiClient);
|
||||||
|
albumApi = AlbumApi(_apiClient);
|
||||||
|
assetApi = AssetApi(_apiClient);
|
||||||
|
serverInfoApi = ServerInfoApi(_apiClient);
|
||||||
|
deviceInfoApi = DeviceInfoApi(_apiClient);
|
||||||
|
}
|
||||||
|
|
||||||
|
setAccessToken(String accessToken) {
|
||||||
|
_apiClient.addDefaultHeader('Authorization', 'bearer $accessToken');
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,6 +2,7 @@ import 'package:flutter_udid/flutter_udid.dart';
|
||||||
import 'dart:io' show Platform;
|
import 'dart:io' show Platform;
|
||||||
|
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:openapi/api.dart';
|
||||||
|
|
||||||
final deviceInfoServiceProvider = Provider((_) => DeviceInfoService());
|
final deviceInfoServiceProvider = Provider((_) => DeviceInfoService());
|
||||||
|
|
||||||
|
@ -9,12 +10,12 @@ class DeviceInfoService {
|
||||||
Future<Map<String, dynamic>> getDeviceInfo() async {
|
Future<Map<String, dynamic>> getDeviceInfo() async {
|
||||||
// Get device info
|
// Get device info
|
||||||
var deviceId = await FlutterUdid.consistentUdid;
|
var deviceId = await FlutterUdid.consistentUdid;
|
||||||
var deviceType = "";
|
var deviceType = DeviceTypeEnum.ANDROID;
|
||||||
|
|
||||||
if (Platform.isAndroid) {
|
if (Platform.isAndroid) {
|
||||||
deviceType = "ANDROID";
|
deviceType = DeviceTypeEnum.ANDROID;
|
||||||
} else if (Platform.isIOS) {
|
} else if (Platform.isIOS) {
|
||||||
deviceType = "IOS";
|
deviceType = DeviceTypeEnum.IOS;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {"deviceId": deviceId, "deviceType": deviceType};
|
return {"deviceId": deviceId, "deviceType": deviceType};
|
||||||
|
|
|
@ -1,21 +0,0 @@
|
||||||
import 'package:hive/hive.dart';
|
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|
||||||
import 'package:immich_mobile/constants/hive_box.dart';
|
|
||||||
|
|
||||||
final localStorageServiceProvider = Provider((_) => LocalStorageService());
|
|
||||||
|
|
||||||
class LocalStorageService {
|
|
||||||
late Box _box;
|
|
||||||
|
|
||||||
LocalStorageService() {
|
|
||||||
_box = Hive.box(userInfoBox);
|
|
||||||
}
|
|
||||||
|
|
||||||
T get<T>(String key) {
|
|
||||||
return _box.get(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
put<T>(String key, T value) {
|
|
||||||
return _box.put(key, value);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -33,10 +33,11 @@ class NetworkService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<dynamic> getRequest(
|
Future<dynamic> getRequest({
|
||||||
{required String url,
|
required String url,
|
||||||
bool isByteResponse = false,
|
bool isByteResponse = false,
|
||||||
bool isStreamReponse = false}) async {
|
bool isStreamReponse = false,
|
||||||
|
}) async {
|
||||||
try {
|
try {
|
||||||
var savedEndpoint = Hive.box(userInfoBox).get(serverEndpointKey);
|
var savedEndpoint = Hive.box(userInfoBox).get(serverEndpointKey);
|
||||||
|
|
||||||
|
|
|
@ -1,33 +1,33 @@
|
||||||
import 'package:dio/dio.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/shared/models/server_info.model.dart';
|
import 'package:immich_mobile/shared/services/api.service.dart';
|
||||||
import 'package:immich_mobile/shared/models/server_version.model.dart';
|
import 'package:openapi/api.dart';
|
||||||
import 'package:immich_mobile/shared/services/network.service.dart';
|
|
||||||
|
|
||||||
final serverInfoServiceProvider =
|
final serverInfoServiceProvider = Provider(
|
||||||
Provider((ref) => ServerInfoService(ref.watch(networkServiceProvider)));
|
(ref) => ServerInfoService(
|
||||||
|
ref.watch(apiServiceProvider),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
class ServerInfoService {
|
class ServerInfoService {
|
||||||
final NetworkService _networkService;
|
final ApiService _apiService;
|
||||||
ServerInfoService(this._networkService);
|
ServerInfoService(this._apiService);
|
||||||
|
|
||||||
Future<ServerInfo> getServerInfo() async {
|
Future<ServerInfoResponseDto?> getServerInfo() async {
|
||||||
Response response = await _networkService.getRequest(url: 'server-info');
|
try {
|
||||||
|
return await _apiService.serverInfoApi.getServerInfo();
|
||||||
return ServerInfo.fromJson(response.toString());
|
} catch (e) {
|
||||||
|
debugPrint("Error [getServerInfo] ${e.toString()}");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<ServerVersion?> getServerVersion() async {
|
Future<ServerVersionReponseDto?> getServerVersion() async {
|
||||||
try {
|
try {
|
||||||
Response response =
|
return await _apiService.serverInfoApi.getServerVersion();
|
||||||
await _networkService.getRequest(url: 'server-info/version');
|
|
||||||
|
|
||||||
return ServerVersion.fromJson(response.toString());
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint("Error getting server info");
|
debugPrint("Error getting server info");
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,70 +1,49 @@
|
||||||
import 'dart:convert';
|
|
||||||
|
|
||||||
import 'package:dio/dio.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hive/hive.dart';
|
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:http/http.dart';
|
||||||
import 'package:http_parser/http_parser.dart';
|
import 'package:http_parser/http_parser.dart';
|
||||||
import 'package:image_picker/image_picker.dart';
|
import 'package:image_picker/image_picker.dart';
|
||||||
import 'package:immich_mobile/constants/hive_box.dart';
|
import 'package:immich_mobile/shared/services/api.service.dart';
|
||||||
import 'package:immich_mobile/shared/models/upload_profile_image_repsonse.model.dart';
|
|
||||||
import 'package:immich_mobile/shared/models/user.model.dart';
|
|
||||||
import 'package:immich_mobile/shared/services/network.service.dart';
|
|
||||||
import 'package:immich_mobile/utils/dio_http_interceptor.dart';
|
|
||||||
import 'package:immich_mobile/utils/files_helper.dart';
|
import 'package:immich_mobile/utils/files_helper.dart';
|
||||||
|
import 'package:openapi/api.dart';
|
||||||
|
|
||||||
final userServiceProvider =
|
final userServiceProvider = Provider(
|
||||||
Provider((ref) => UserService(ref.watch(networkServiceProvider)));
|
(ref) => UserService(
|
||||||
|
ref.watch(apiServiceProvider),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
class UserService {
|
class UserService {
|
||||||
final NetworkService _networkService;
|
final ApiService _apiService;
|
||||||
UserService(this._networkService);
|
|
||||||
|
|
||||||
Future<List<User>> getAllUsersInfo() async {
|
UserService(this._apiService);
|
||||||
|
|
||||||
|
Future<List<UserResponseDto>?> getAllUsersInfo({required bool isAll}) async {
|
||||||
try {
|
try {
|
||||||
var res = await _networkService.getRequest(url: 'user');
|
return await _apiService.userApi.getAllUsers(isAll);
|
||||||
List<dynamic> decodedData = jsonDecode(res.toString());
|
|
||||||
List<User> result = List.from(decodedData.map((e) => User.fromMap(e)));
|
|
||||||
|
|
||||||
return result;
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint("Error getAllUsersInfo ${e.toString()}");
|
debugPrint("Error [getAllUsersInfo] ${e.toString()}");
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return [];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<UploadProfileImageResponse?> uploadProfileImage(XFile image) async {
|
Future<CreateProfileImageResponseDto?> uploadProfileImage(XFile image) async {
|
||||||
var dio = Dio();
|
|
||||||
dio.interceptors.add(AuthenticatedRequestInterceptor());
|
|
||||||
String savedEndpoint = Hive.box(userInfoBox).get(serverEndpointKey);
|
|
||||||
var mimeType = FileHelper.getMimeType(image.path);
|
|
||||||
|
|
||||||
final imageData = MultipartFile.fromBytes(
|
|
||||||
await image.readAsBytes(),
|
|
||||||
filename: image.name,
|
|
||||||
contentType: MediaType(
|
|
||||||
mimeType["type"],
|
|
||||||
mimeType["subType"],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
final formData = FormData.fromMap({'file': imageData});
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
Response res = await dio.post(
|
var mimeType = FileHelper.getMimeType(image.path);
|
||||||
'$savedEndpoint/user/profile-image',
|
|
||||||
data: formData,
|
return await _apiService.userApi.createProfileImage(
|
||||||
|
MultipartFile.fromBytes(
|
||||||
|
'file',
|
||||||
|
await image.readAsBytes(),
|
||||||
|
filename: image.name,
|
||||||
|
contentType: MediaType(
|
||||||
|
mimeType["type"],
|
||||||
|
mimeType["subType"],
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
var payload = UploadProfileImageResponse.fromJson(res.toString());
|
|
||||||
|
|
||||||
return payload;
|
|
||||||
} on DioError catch (e) {
|
|
||||||
debugPrint("Error uploading file: ${e.response}");
|
|
||||||
return null;
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint("Error uploading file: $e");
|
debugPrint("Error [uploadProfileImage] ${e.toString()}");
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,7 +22,10 @@ class ImmichSliverPersistentAppBarDelegate
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(
|
Widget build(
|
||||||
BuildContext context, double shrinkOffset, bool overlapsContent) {
|
BuildContext context,
|
||||||
|
double shrinkOffset,
|
||||||
|
bool overlapsContent,
|
||||||
|
) {
|
||||||
return SizedBox.expand(child: child);
|
return SizedBox.expand(child: child);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue