Compare commits

...

9 Commits

Author SHA1 Message Date
Alex
b69f6e0df7 Update inline font for f-droid publication metric (#107)
* Added local font
* Up Patch 1.5.1+9
2022-04-04 09:08:53 -05:00
Alex
be2794a372 Optimization/fix slow backup when asset list is long. (#104)
* Handle pause/restart listening to event on_upload_success and reload asset list after navigating back from BackupControllerPage
* Remove unused api endpoint
2022-04-03 12:31:45 -05:00
Alex Tran
2ff25b49f4 Up Minor 1.5.0+8 2022-04-02 12:46:29 -05:00
Alex Tran
135d72d4cd Fixed issue with docker-compose cannot navigate to relative path of Dockercompose file in issue #90 2022-04-02 12:37:57 -05:00
Alex
90ef64efa3 Download asset to local and error fixing (#100)
* Update photo_manager pub package
* Added download endpoint for assets
* Successfully save a photo to the local device's gallery
* Save save a video to the local device's gallery
* Fixed #97
* Added download loading indicator
* Refactor and increase the font size for curated search thumbnail images
* Reposition loading animation on the search result page
2022-04-02 12:31:53 -05:00
Alex Tran
60df387459 Update fdroid app description 2022-03-30 13:14:09 -05:00
Alex Tran
fc1acf6f01 Remove release build on github action 2022-03-29 22:10:21 -05:00
Alex Tran
cfc5229964 Fixed issue with container cannot find module 2022-03-29 20:25:00 -05:00
Alex Tran
f9ddeac265 Fixed issue with container cannot find module 2022-03-29 20:17:40 -05:00
51 changed files with 655 additions and 375 deletions

View File

@@ -2,8 +2,6 @@ name: Build Server - Release
on:
workflow_dispatch:
release:
types: [published]
jobs:
buildandpush:

View File

@@ -8,4 +8,7 @@ dev-scale:
docker-compose -f ./docker/docker-compose.dev.yml up --build -V --scale immich_server=3 --remove-orphans
prod:
docker-compose -f ./docker/docker-compose.yml up --build -V --remove-orphans
docker-compose -f ./docker/docker-compose.yml up --build -V --remove-orphans
prod-scale:
docker-compose -f ./docker/docker-compose.yml up --build -V --scale immich_server=3 --scale immich_microservices=3 --remove-orphans

View File

@@ -1,13 +1,17 @@
# Deployment checklist for iOS/Android/Server
[] Up version in [mobile/pubspec.yml](/mobile/pubspec.yaml)
[ ] Up version in [mobile/pubspec.yml](/mobile/pubspec.yaml)
[] Up version in [docker/docker-compose.yml](/docker/docker-compose.yml) for `immich_server` service
[ ] Up version in [docker/docker-compose.yml](/docker/docker-compose.yml) for `immich_server` service
[] Up version in [docker/docker-compose.gpu.yml](/docker/docker-compose.gpu.yml) for `immich_server` service
[ ] Up version in [docker/docker-compose.gpu.yml](/docker/docker-compose.gpu.yml) for `immich_server` service
[] Up version in [server/src/constants/server_version.constant.ts](/server/src/constants/server_version.constant.ts)
[ ] Up version in [docker/docker-compose.dev.yml](/docker/docker-compose.gpu.yml) for `immich_server` service
[] Up version in iOS Fastlane [/mobile/ios/fastlane/Fastfile](/mobile/ios/fastlane/Fastfile)
[ ] Up version in [server/src/constants/server_version.constant.ts](/server/src/constants/server_version.constant.ts)
[ ] Up version in iOS Fastlane [/mobile/ios/fastlane/Fastfile](/mobile/ios/fastlane/Fastfile)
[ ] Add changelog to [Android Fastlane F-droid folder](/mobile/android/fastlane/metadata/android/en-US/changelogs)
All of the version should be the same.

View File

@@ -2,10 +2,10 @@ version: "3.8"
services:
immich_server:
image: immich-server-dev:1.3.2
image: immich-server-dev:1.5.0
build:
context: ../server
dockerfile: ../server/Dockerfile
dockerfile: Dockerfile
command: npm run start:dev
expose:
- "3000"
@@ -24,10 +24,10 @@ services:
- immich_network
immich_microservices:
image: immich-microservices-dev:1.3.2
image: immich-microservices-dev:1.5.0
build:
context: ../microservices
dockerfile: ../microservices/Dockerfile
dockerfile: Dockerfile
command: npm run start:dev
expose:
- "3001"

View File

@@ -2,10 +2,10 @@ version: "3.8"
services:
immich_server:
image: immich-server-dev:1.4.0
image: immich-server-dev:1.5.0
build:
context: ../server
dockerfile: ../server/Dockerfile
dockerfile: Dockerfile
command: npm run start:dev
expose:
- "3000"
@@ -22,10 +22,10 @@ services:
- immich_network
immich_microservices:
image: immich-microservices-dev:1.4.0
image: immich-microservices-dev:1.5.0
build:
context: ../microservices
dockerfile: ../microservices/Dockerfile
dockerfile: Dockerfile
command: npm run start:dev
deploy:
resources:

View File

@@ -2,16 +2,17 @@ version: "3.8"
services:
immich_server:
image: immich-server:1.4.0
image: immich-server:1.5.0
build:
context: ../server
dockerfile: ../server/Dockerfile
dockerfile: Dockerfile
entrypoint: ["/bin/sh", "./entrypoint.sh"]
restart: unless-stopped
expose:
- "3000"
volumes:
- ../server:/usr/src/app
- ${UPLOAD_LOCATION}:/usr/src/app/upload
- /usr/src/app/node_modules
env_file:
- .env
environment:
@@ -21,18 +22,20 @@ services:
- database
networks:
- immich_network
restart: unless-stopped
immich_microservices:
image: immich-microservices:1.4.0
image: immich-microservices:1.5.0
build:
context: ../microservices
dockerfile: ../microservices/Dockerfile
dockerfile: Dockerfile
entrypoint: ["/bin/sh", "./entrypoint.sh"]
restart: unless-stopped
expose:
- "3001"
volumes:
- ../microservices:/usr/src/app
- ${UPLOAD_LOCATION}:/usr/src/app/upload
- /usr/src/app/node_modules
env_file:
- .env
environment:
@@ -41,7 +44,7 @@ services:
- database
networks:
- immich_network
restart: unless-stopped
redis:
container_name: immich_redis
@@ -81,7 +84,28 @@ services:
depends_on:
- immich_server
# immich_tf_fastapi:
# container_name: immich_tf_fastapi
# image: tensor_flow_fastapi:1.0.0
# restart: always
# command: uvicorn app.main:app --proxy-headers --host 0.0.0.0 --port 8000 --reload
# build:
# context: ../machine_learning
# target: cpu
# dockerfile: ../machine_learning/Dockerfile
# volumes:
# - ../machine_learning/app:/code/app
# - ${UPLOAD_LOCATION}:/code/app/upload
# ports:
# - 2285:8000
# expose:
# - "8000"
# depends_on:
# - database
# networks:
# - immich_network
networks:
immich_network:
volumes:
pgdata:
pgdata:

View File

@@ -39,6 +39,7 @@ export class ImageClassifierService {
}
}
tf.dispose(decodedImage);
return tags;
}
} catch (e) {

View File

@@ -29,6 +29,7 @@ export class ObjectDetectionService {
}
}
tf.dispose(decodedImage);
return [...tags];
}
} catch (e) {

View File

@@ -51,7 +51,7 @@ android {
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId "app.alextran.immich"
minSdkVersion 20
minSdkVersion 21
targetSdkVersion flutter.targetSdkVersion
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName

View File

@@ -20,4 +20,7 @@
</application>
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.ACCESS_MEDIA_LOCATION" />
</manifest>

View File

@@ -0,0 +1,2 @@
* User can now download assets to local device
* Increased the font size for curated image thumbnail information on the seach page

View File

@@ -0,0 +1 @@
* Added inline font, remove google-font dependency in pubspec.

View File

@@ -1 +1 @@
This is a client app for the self-hostable Immich Server (which can be found with the app's source repo). You will need to run/manage the server on your own in order to use the app.
This is a client app for the self-hostable Immich Server

Binary file not shown.

Binary file not shown.

BIN
mobile/fonts/WorkSans.ttf Normal file

Binary file not shown.

View File

@@ -13,7 +13,7 @@ PODS:
- Flutter
- path_provider_ios (0.0.1):
- Flutter
- photo_manager (1.0.0):
- photo_manager (2.0.0):
- Flutter
- FlutterMacOS
- SAMKeychain (1.5.3)
@@ -70,7 +70,7 @@ SPEC CHECKSUMS:
FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a
package_info_plus: 6c92f08e1f853dc01228d6f553146438dafcd14e
path_provider_ios: 7d7ce634493af4477d156294792024ec3485acd5
photo_manager: 84fa94fbeb82e607333ea9a13c43b58e0903a463
photo_manager: 4f6810b7dfc4feb03b461ac1a70dacf91fba7604
SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c
sqflite: 6d358c025f5b867b29ed92fc697fd34924e11904
Toast: 91b396c56ee72a5790816f40d3a94dd357abc196

View File

@@ -1,66 +1,72 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>Immich</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>immich_mobile</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>2</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>MGLMapboxMetricsEnabledSettingShownInApp</key>
<true/>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
<key>NSLocationAlwaysUsageDescription</key>
<string>Enable location setting to show position of assets on map</string>
<key>NSLocationWhenInUseUsageDescription</key>
<string>Enable location setting to show position of assets on map</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>We need to manage backup your photos album</string>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UIUserInterfaceStyle</key>
<string>Light</string>
<key>UIViewControllerBasedStatusBarAppearance</key>
<true/>
<key>io.flutter.embedded_views_preview</key>
<true/>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
</dict>
</plist>
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>Immich</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>immich_mobile</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>2</string>
<key>LSRequiresIPhoneOS</key>
<true />
<key>MGLMapboxMetricsEnabledSettingShownInApp</key>
<true />
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true />
</dict>
<key>NSLocationAlwaysUsageDescription</key>
<string>Enable location setting to show position of assets on map</string>
<key>NSLocationWhenInUseUsageDescription</key>
<string>Enable location setting to show position of assets on map</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>We need to manage backup your photos album</string>
<key>NSPhotoLibraryAddUsageDescription</key>
<string>We need to manage backup your photos album</string>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UIUserInterfaceStyle</key>
<string>Light</string>
<key>UIViewControllerBasedStatusBarAppearance</key>
<true />
<key>io.flutter.embedded_views_preview</key>
<true />
<key>ITSAppUsesNonExemptEncryption</key>
<false />
</dict>
</plist>

View File

@@ -19,11 +19,11 @@ platform :ios do
desc "iOS Beta"
lane :beta do
increment_version_number(
version_number: "1.4.0"
version_number: "1.5.1"
)
increment_build_number(
build_number: latest_testflight_build_number + 1,
)
increment_build_number({
build_number: 0
})
build_app(scheme: "Runner",
workspace: "Runner.xcworkspace",
xcargs: "-allowProvisioningUpdates")

View File

@@ -10,7 +10,6 @@ import 'package:immich_mobile/shared/providers/backup.provider.dart';
import 'package:immich_mobile/shared/providers/server_info.provider.dart';
import 'package:immich_mobile/shared/providers/websocket.provider.dart';
import 'constants/hive_box.dart';
import 'package:google_fonts/google_fonts.dart';
void main() async {
await Hive.initFlutter();
@@ -94,9 +93,11 @@ class _ImmichAppState extends ConsumerState<ImmichApp> with WidgetsBindingObserv
theme: ThemeData(
brightness: Brightness.light,
primarySwatch: Colors.indigo,
textTheme: GoogleFonts.workSansTextTheme(
Theme.of(context).textTheme.apply(fontSizeFactor: 1.0),
),
// textTheme: GoogleFonts.workSansTextTheme(
// Theme.of(context).textTheme.apply(fontSizeFactor: 1.0),
// ),
fontFamily: 'WorkSans',
snackBarTheme: const SnackBarThemeData(contentTextStyle: TextStyle(fontFamily: 'WorkSans')),
scaffoldBackgroundColor: const Color(0xFFf6f8fe),
appBarTheme: const AppBarTheme(
backgroundColor: Colors.white,

View File

@@ -1,28 +1,34 @@
import 'dart:convert';
enum DownloadAssetStatus { idle, loading, success, error }
class ImageViewerPageState {
final bool isBottomSheetEnable;
// enum
final DownloadAssetStatus downloadAssetStatus;
ImageViewerPageState({
required this.isBottomSheetEnable,
required this.downloadAssetStatus,
});
ImageViewerPageState copyWith({
bool? isBottomSheetEnable,
DownloadAssetStatus? downloadAssetStatus,
}) {
return ImageViewerPageState(
isBottomSheetEnable: isBottomSheetEnable ?? this.isBottomSheetEnable,
downloadAssetStatus: downloadAssetStatus ?? this.downloadAssetStatus,
);
}
Map<String, dynamic> toMap() {
return {
'isBottomSheetEnable': isBottomSheetEnable,
};
final result = <String, dynamic>{};
result.addAll({'downloadAssetStatus': downloadAssetStatus.index});
return result;
}
factory ImageViewerPageState.fromMap(Map<String, dynamic> map) {
return ImageViewerPageState(
isBottomSheetEnable: map['isBottomSheetEnable'] ?? false,
downloadAssetStatus: DownloadAssetStatus.values[map['downloadAssetStatus'] ?? 0],
);
}
@@ -31,15 +37,15 @@ class ImageViewerPageState {
factory ImageViewerPageState.fromJson(String source) => ImageViewerPageState.fromMap(json.decode(source));
@override
String toString() => 'ImageViewerPageState(isBottomSheetEnable: $isBottomSheetEnable)';
String toString() => 'ImageViewerPageState(downloadAssetStatus: $downloadAssetStatus)';
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is ImageViewerPageState && other.isBottomSheetEnable == isBottomSheetEnable;
return other is ImageViewerPageState && other.downloadAssetStatus == downloadAssetStatus;
}
@override
int get hashCode => isBottomSheetEnable.hashCode;
int get hashCode => downloadAssetStatus.hashCode;
}

View File

@@ -0,0 +1,6 @@
class RequestDownloadAssetInfo {
final String assetId;
final String deviceId;
RequestDownloadAssetInfo(this.assetId, this.deviceId);
}

View File

@@ -1,21 +1,43 @@
import 'package:flutter/material.dart';
import 'package:fluttertoast/fluttertoast.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/home/models/home_page_state.model.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';
class ImageViewerPageStateNotifier extends StateNotifier<ImageViewerPageState> {
ImageViewerPageStateNotifier() : super(ImageViewerPageState(isBottomSheetEnable: false));
class ImageViewerStateNotifier extends StateNotifier<ImageViewerPageState> {
final ImageViewerService _imageViewerService = ImageViewerService();
void toggleBottomSheet() {
bool isBottomSheetEnable = state.isBottomSheetEnable;
ImageViewerStateNotifier() : super(ImageViewerPageState(downloadAssetStatus: DownloadAssetStatus.idle));
if (isBottomSheetEnable) {
state.copyWith(isBottomSheetEnable: false);
void downloadAsset(ImmichAsset asset, BuildContext context) async {
state = state.copyWith(downloadAssetStatus: DownloadAssetStatus.loading);
bool isSuccess = await _imageViewerService.downloadAssetToDevice(asset);
if (isSuccess) {
state = state.copyWith(downloadAssetStatus: DownloadAssetStatus.success);
ImmichToast.show(
context: context,
msg: "Download Success",
toastType: ToastType.success,
gravity: ToastGravity.BOTTOM,
);
} else {
state.copyWith(isBottomSheetEnable: true);
state = state.copyWith(downloadAssetStatus: DownloadAssetStatus.error);
ImmichToast.show(
context: context,
msg: "Download Error",
toastType: ToastType.error,
gravity: ToastGravity.BOTTOM,
);
}
state = state.copyWith(downloadAssetStatus: DownloadAssetStatus.idle);
}
}
final homePageStateProvider = StateNotifierProvider<ImageViewerPageStateNotifier, ImageViewerPageState>(
((ref) => ImageViewerPageStateNotifier()));
final imageViewerStateProvider =
StateNotifierProvider<ImageViewerStateNotifier, ImageViewerPageState>(((ref) => ImageViewerStateNotifier()));

View File

@@ -0,0 +1,50 @@
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:immich_mobile/constants/hive_box.dart';
import 'package:immich_mobile/shared/models/immich_asset.model.dart';
import 'package:path/path.dart' as p;
import 'package:http/http.dart' as http;
import 'package:photo_manager/photo_manager.dart';
import 'package:path_provider/path_provider.dart';
class ImageViewerService {
Future<bool> downloadAssetToDevice(ImmichAsset asset) async {
try {
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(
filePath,
headers: {"Authorization": "Bearer ${Hive.box(userInfoBox).get(accessTokenKey)}"},
);
final AssetEntity? entity;
if (asset.type == 'IMAGE') {
entity = await PhotoManager.editor.saveImage(
res.bodyBytes,
title: p.basename(asset.originalPath),
);
} else {
final tempDir = await getTemporaryDirectory();
File tempFile = await File('${tempDir.path}/$fileName').create();
tempFile.writeAsBytesSync(res.bodyBytes);
entity = await PhotoManager.editor.saveVideo(tempFile, title: fileName);
}
if (entity != null) {
return true;
}
} catch (e) {
debugPrint("Error saving file $e");
return false;
}
return false;
}
}

View File

@@ -0,0 +1,24 @@
import 'package:flutter/material.dart';
import 'package:flutter_spinkit/flutter_spinkit.dart';
class DownloadLoadingIndicator extends StatelessWidget {
const DownloadLoadingIndicator({
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Container(
height: 60,
width: 60,
decoration: BoxDecoration(
color: Theme.of(context).primaryColor,
borderRadius: BorderRadius.circular(10),
),
child: const SpinKitDancingSquare(
color: Colors.white,
size: 30.0,
),
);
}
}

View File

@@ -1,14 +1,19 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/shared/models/immich_asset.model.dart';
class TopControlAppBar extends StatelessWidget with PreferredSizeWidget {
const TopControlAppBar({Key? key, required this.asset, required this.onMoreInfoPressed}) : super(key: key);
class TopControlAppBar extends ConsumerWidget with PreferredSizeWidget {
const TopControlAppBar(
{Key? key, required this.asset, required this.onMoreInfoPressed, required this.onDownloadPressed})
: super(key: key);
final ImmichAsset asset;
final Function onMoreInfoPressed;
final Function onDownloadPressed;
@override
Widget build(BuildContext context) {
Widget build(BuildContext context, WidgetRef ref) {
double iconSize = 18.0;
return AppBar(
@@ -29,7 +34,7 @@ class TopControlAppBar extends StatelessWidget with PreferredSizeWidget {
iconSize: iconSize,
splashRadius: iconSize,
onPressed: () {
print("download");
onDownloadPressed();
},
icon: const Icon(Icons.cloud_download_rounded),
),

View File

@@ -4,6 +4,9 @@ import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hive/hive.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/hive_box.dart';
import 'package:immich_mobile/modules/asset_viewer/models/image_viewer_page_state.model.dart';
import 'package:immich_mobile/modules/asset_viewer/providers/image_viewer_page_state.provider.dart';
import 'package:immich_mobile/modules/asset_viewer/ui/download_loading_indicator.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/home/services/asset.service.dart';
@@ -25,6 +28,7 @@ class ImageViewerPage extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final downloadAssetStatus = ref.watch(imageViewerStateProvider).downloadAssetStatus;
var box = Hive.box(userInfoBox);
getAssetExif() async {
@@ -42,65 +46,77 @@ class ImageViewerPage extends HookConsumerWidget {
asset: asset,
onMoreInfoPressed: () {
showModalBottomSheet(
backgroundColor: Colors.black,
barrierColor: Colors.transparent,
isScrollControlled: false,
context: context,
builder: (context) {
return ExifBottomSheet(assetDetail: assetDetail!);
});
backgroundColor: Colors.black,
barrierColor: Colors.transparent,
isScrollControlled: false,
context: context,
builder: (context) {
return ExifBottomSheet(assetDetail: assetDetail!);
},
);
},
onDownloadPressed: () {
ref.watch(imageViewerStateProvider.notifier).downloadAsset(asset, context);
},
),
body: SafeArea(
child: Center(
child: Hero(
tag: heroTag,
child: CachedNetworkImage(
fit: BoxFit.cover,
imageUrl: imageUrl,
httpHeaders: {"Authorization": "Bearer ${box.get(accessTokenKey)}"},
fadeInDuration: const Duration(milliseconds: 250),
errorWidget: (context, url, error) => ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 300),
child: Wrap(
spacing: 32,
runSpacing: 32,
alignment: WrapAlignment.center,
children: [
const Text(
"Failed To Render Image - Possibly Corrupted Data",
textAlign: TextAlign.center,
style: TextStyle(fontSize: 16, color: Colors.white),
child: Stack(
children: [
Center(
child: Hero(
tag: heroTag,
child: CachedNetworkImage(
fit: BoxFit.cover,
imageUrl: imageUrl,
httpHeaders: {"Authorization": "Bearer ${box.get(accessTokenKey)}"},
fadeInDuration: const Duration(milliseconds: 250),
errorWidget: (context, url, error) => ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 300),
child: Wrap(
spacing: 32,
runSpacing: 32,
alignment: WrapAlignment.center,
children: [
const Text(
"Failed To Render Image - Possibly Corrupted Data",
textAlign: TextAlign.center,
style: TextStyle(fontSize: 16, color: Colors.white),
),
SingleChildScrollView(
child: Text(
error.toString(),
textAlign: TextAlign.center,
style: TextStyle(fontSize: 12, color: Colors.grey[400]),
),
),
],
),
SingleChildScrollView(
child: Text(
error.toString(),
textAlign: TextAlign.center,
style: TextStyle(fontSize: 12, color: Colors.grey[400]),
),
placeholder: (context, url) {
return CachedNetworkImage(
cacheKey: thumbnailUrl,
fit: BoxFit.cover,
imageUrl: thumbnailUrl,
httpHeaders: {"Authorization": "Bearer ${box.get(accessTokenKey)}"},
placeholderFadeInDuration: const Duration(milliseconds: 0),
progressIndicatorBuilder: (context, url, downloadProgress) => Transform.scale(
scale: 0.2,
child: CircularProgressIndicator(value: downloadProgress.progress),
),
),
],
errorWidget: (context, url, error) => Icon(
Icons.error,
color: Colors.grey[300],
),
);
},
),
),
placeholder: (context, url) {
return CachedNetworkImage(
cacheKey: thumbnailUrl,
fit: BoxFit.cover,
imageUrl: thumbnailUrl,
httpHeaders: {"Authorization": "Bearer ${box.get(accessTokenKey)}"},
placeholderFadeInDuration: const Duration(milliseconds: 0),
progressIndicatorBuilder: (context, url, downloadProgress) => Transform.scale(
scale: 0.2,
child: CircularProgressIndicator(value: downloadProgress.progress),
),
errorWidget: (context, url, error) => Icon(
Icons.error,
color: Colors.grey[300],
),
);
},
),
),
if (downloadAssetStatus == DownloadAssetStatus.loading)
const Center(
child: DownloadLoadingIndicator(),
),
],
),
),
);

View File

@@ -1,35 +1,74 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hive/hive.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/hive_box.dart';
import 'package:chewie/chewie.dart';
import 'package:immich_mobile/modules/asset_viewer/models/image_viewer_page_state.model.dart';
import 'package:immich_mobile/modules/asset_viewer/providers/image_viewer_page_state.provider.dart';
import 'package:immich_mobile/modules/asset_viewer/ui/download_loading_indicator.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/home/services/asset.service.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:video_player/video_player.dart';
class VideoViewerPage extends StatelessWidget {
// ignore: must_be_immutable
class VideoViewerPage extends HookConsumerWidget {
final String videoUrl;
final ImmichAsset asset;
ImmichAssetWithExif? assetDetail;
final AssetService _assetService = AssetService();
const VideoViewerPage({Key? key, required this.videoUrl}) : super(key: key);
VideoViewerPage({Key? key, required this.videoUrl, required this.asset}) : super(key: key);
@override
Widget build(BuildContext context) {
Widget build(BuildContext context, WidgetRef ref) {
final downloadAssetStatus = ref.watch(imageViewerStateProvider).downloadAssetStatus;
String jwtToken = Hive.box(userInfoBox).get(accessTokenKey);
getAssetExif() async {
assetDetail = await _assetService.getAssetById(asset.id);
}
useEffect(() {
getAssetExif();
return null;
}, []);
return Scaffold(
backgroundColor: Colors.black,
appBar: AppBar(
systemOverlayStyle: SystemUiOverlayStyle.light,
backgroundColor: Colors.black,
leading: IconButton(
onPressed: () {
AutoRouter.of(context).pop();
appBar: TopControlAppBar(
asset: asset,
onMoreInfoPressed: () {
showModalBottomSheet(
backgroundColor: Colors.black,
barrierColor: Colors.transparent,
isScrollControlled: false,
context: context,
builder: (context) {
return ExifBottomSheet(assetDetail: assetDetail!);
},
icon: const Icon(Icons.arrow_back_ios)),
);
},
onDownloadPressed: () {
ref.watch(imageViewerStateProvider.notifier).downloadAsset(asset, context);
},
),
body: SafeArea(
child: VideoThumbnailPlayer(
url: videoUrl,
jwtToken: jwtToken,
child: Stack(
children: [
VideoThumbnailPlayer(
url: videoUrl,
jwtToken: jwtToken,
),
if (downloadAssetStatus == DownloadAssetStatus.loading)
const Center(
child: DownloadLoadingIndicator(),
),
],
),
),
);

View File

@@ -1,7 +1,6 @@
import 'package:auto_route/auto_route.dart';
import 'package:badges/badges.dart';
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
@@ -79,12 +78,11 @@ class ImmichSliverAppBar extends ConsumerWidget {
),
title: Text(
'IMMICH',
style: GoogleFonts.snowburstOne(
textStyle: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 22,
color: Theme.of(context).primaryColor,
),
style: TextStyle(
fontFamily: 'SnowburstOne',
fontWeight: FontWeight.bold,
fontSize: 22,
color: Theme.of(context).primaryColor,
),
),
actions: [
@@ -121,8 +119,12 @@ class ImmichSliverAppBar extends ConsumerWidget {
),
child: const Icon(Icons.backup_rounded)),
tooltip: 'Backup Controller',
onPressed: () {
AutoRouter.of(context).push(const BackupControllerRoute());
onPressed: () async {
var onPop = await AutoRouter.of(context).push(const BackupControllerRoute());
if (onPop != null && onPop == true) {
onPopBack!();
}
},
),
_backupState.backupProgress == BackUpProgressEnum.inProgress

View File

@@ -65,8 +65,8 @@ class ThumbnailImage extends HookConsumerWidget {
} else {
AutoRouter.of(context).push(
VideoViewerRoute(
videoUrl: '${box.get(serverEndpointKey)}/asset/file?aid=${asset.deviceAssetId}&did=${asset.deviceId}',
),
videoUrl: '${box.get(serverEndpointKey)}/asset/file?aid=${asset.deviceAssetId}&did=${asset.deviceId}',
asset: asset),
);
}
}

View File

@@ -33,6 +33,10 @@ class HomePage extends HookConsumerWidget {
return null;
}, []);
void reloadAllAsset() {
ref.read(assetProvider.notifier).getAllAsset();
}
Widget _buildBody() {
if (assetGroupByDateTime.isNotEmpty) {
int? lastMonth;
@@ -86,7 +90,9 @@ class HomePage extends HookConsumerWidget {
child: null,
),
)
: const ImmichSliverAppBar(),
: ImmichSliverAppBar(
onPopBack: reloadAllAsset,
),
duration: const Duration(milliseconds: 350),
),
..._imageGridGroup

View File

@@ -1,7 +1,6 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/home/providers/asset.provider.dart';
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
@@ -33,9 +32,12 @@ class LoginForm extends HookConsumerWidget {
),
Text(
'IMMICH',
style: GoogleFonts.snowburstOne(
textStyle:
TextStyle(fontWeight: FontWeight.bold, fontSize: 48, color: Theme.of(context).primaryColor)),
style: TextStyle(
fontFamily: 'SnowburstOne',
fontWeight: FontWeight.bold,
fontSize: 48,
color: Theme.of(context).primaryColor,
),
),
EmailInput(controller: usernameController),
PasswordInput(controller: passwordController),
@@ -128,9 +130,10 @@ class LoginButton extends ConsumerWidget {
AutoRouter.of(context).pushNamed("/tab-controller-page");
} else {
ImmichToast.show(
context: context,
msg: "Error logging you in, check server url, email and password!",
toastType: ToastType.error);
context: context,
msg: "Error logging you in, check server url, email and password!",
toastType: ToastType.error,
);
}
},
child: const Text("Login"));

View File

@@ -0,0 +1,67 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:immich_mobile/constants/hive_box.dart';
import 'package:immich_mobile/utils/capitalize_first_letter.dart';
class ThumbnailWithInfo extends StatelessWidget {
const ThumbnailWithInfo({Key? key, required this.textInfo, required this.imageUrl, required this.onTap})
: super(key: key);
final String textInfo;
final String imageUrl;
final Function onTap;
@override
Widget build(BuildContext context) {
var box = Hive.box(userInfoBox);
return GestureDetector(
onTap: () {
onTap();
},
child: Padding(
padding: const EdgeInsets.only(right: 8.0),
child: SizedBox(
width: MediaQuery.of(context).size.width / 2,
child: Stack(
alignment: Alignment.bottomCenter,
children: [
Container(
foregroundDecoration: BoxDecoration(
borderRadius: BorderRadius.circular(10),
color: Colors.black26,
),
child: ClipRRect(
borderRadius: BorderRadius.circular(10),
child: CachedNetworkImage(
width: 250,
height: 250,
fit: BoxFit.cover,
imageUrl: imageUrl,
httpHeaders: {"Authorization": "Bearer ${box.get(accessTokenKey)}"},
),
),
),
Positioned(
bottom: 8,
left: 10,
child: SizedBox(
width: MediaQuery.of(context).size.width / 3,
child: Text(
textInfo.capitalizeFirstLetter(),
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
),
),
],
),
),
),
);
}
}

View File

@@ -1,5 +1,4 @@
import 'package:auto_route/auto_route.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hive_flutter/hive_flutter.dart';
@@ -10,6 +9,7 @@ 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/ui/search_bar.dart';
import 'package:immich_mobile/modules/search/ui/search_suggestion_list.dart';
import 'package:immich_mobile/modules/search/ui/thumbnail_with_info.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/utils/capitalize_first_letter.dart';
@@ -40,12 +40,12 @@ class SearchPage extends HookConsumerWidget {
_buildPlaces() {
return curatedLocation.when(
loading: () => const CircularProgressIndicator(),
loading: () => const SizedBox(width: 60, height: 60, child: CircularProgressIndicator.adaptive()),
error: (err, stack) => Text('Error: $err'),
data: (curatedLocations) {
return curatedLocations.isNotEmpty
? SizedBox(
height: MediaQuery.of(context).size.width / 3,
height: MediaQuery.of(context).size.width / 2,
child: ListView.builder(
padding: const EdgeInsets.only(left: 16),
scrollDirection: Axis.horizontal,
@@ -66,7 +66,7 @@ class SearchPage extends HookConsumerWidget {
),
)
: SizedBox(
height: MediaQuery.of(context).size.width / 3,
height: MediaQuery.of(context).size.width / 2,
child: ListView.builder(
padding: const EdgeInsets.only(left: 16),
scrollDirection: Axis.horizontal,
@@ -87,12 +87,12 @@ class SearchPage extends HookConsumerWidget {
_buildThings() {
return curatedObjects.when(
loading: () => const CircularProgressIndicator(),
loading: () => const SizedBox(width: 60, height: 60, child: CircularProgressIndicator.adaptive()),
error: (err, stack) => Text('Error: $err'),
data: (objects) {
return objects.isNotEmpty
? SizedBox(
height: MediaQuery.of(context).size.width / 3,
height: MediaQuery.of(context).size.width / 2,
child: ListView.builder(
padding: const EdgeInsets.only(left: 16),
scrollDirection: Axis.horizontal,
@@ -114,7 +114,7 @@ class SearchPage extends HookConsumerWidget {
),
)
: SizedBox(
height: MediaQuery.of(context).size.width / 3,
height: MediaQuery.of(context).size.width / 2,
child: ListView.builder(
padding: const EdgeInsets.only(left: 16),
scrollDirection: Axis.horizontal,
@@ -172,66 +172,3 @@ class SearchPage extends HookConsumerWidget {
);
}
}
class ThumbnailWithInfo extends StatelessWidget {
const ThumbnailWithInfo({Key? key, required this.textInfo, required this.imageUrl, required this.onTap})
: super(key: key);
final String textInfo;
final String imageUrl;
final Function onTap;
@override
Widget build(BuildContext context) {
var box = Hive.box(userInfoBox);
return GestureDetector(
onTap: () {
onTap();
},
child: Padding(
padding: const EdgeInsets.only(right: 8.0),
child: SizedBox(
width: MediaQuery.of(context).size.width / 3,
height: MediaQuery.of(context).size.width / 3,
child: Stack(
alignment: Alignment.bottomCenter,
children: [
Container(
foregroundDecoration: BoxDecoration(
borderRadius: BorderRadius.circular(10),
color: Colors.black26,
),
child: ClipRRect(
borderRadius: BorderRadius.circular(10),
child: CachedNetworkImage(
width: 150,
height: 150,
fit: BoxFit.cover,
imageUrl: imageUrl,
httpHeaders: {"Authorization": "Bearer ${box.get(accessTokenKey)}"},
),
),
),
Positioned(
bottom: 8,
left: 10,
child: SizedBox(
width: MediaQuery.of(context).size.width / 3,
child: Text(
textInfo.capitalizeFirstLetter(),
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 12,
),
),
),
),
],
),
),
),
);
}
}

View File

@@ -1,6 +1,7 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:flutter_spinkit/flutter_spinkit.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/home/ui/daily_title_text.dart';
import 'package:immich_mobile/modules/home/ui/draggable_scrollbar.dart';
@@ -107,7 +108,10 @@ class SearchResultPage extends HookConsumerWidget {
}
if (searchResultPageState.isLoading) {
return const CircularProgressIndicator.adaptive();
return Center(
child: SpinKitDancingSquare(
color: Theme.of(context).primaryColor,
));
}
if (searchResultPageState.isSuccess) {

View File

@@ -9,7 +9,7 @@ import 'package:immich_mobile/shared/models/immich_asset.model.dart';
import 'package:immich_mobile/shared/views/backup_controller_page.dart';
import 'package:immich_mobile/modules/asset_viewer/views/image_viewer_page.dart';
import 'package:immich_mobile/shared/views/tab_controller_page.dart';
import 'package:immich_mobile/shared/views/video_viewer_page.dart';
import 'package:immich_mobile/modules/asset_viewer/views/video_viewer_page.dart';
part 'router.gr.dart';

View File

@@ -44,7 +44,8 @@ class _$AppRouter extends RootStackRouter {
final args = routeData.argsAs<VideoViewerRouteArgs>();
return MaterialPageX<dynamic>(
routeData: routeData,
child: VideoViewerPage(key: args.key, videoUrl: args.videoUrl));
child: VideoViewerPage(
key: args.key, videoUrl: args.videoUrl, asset: args.asset));
},
BackupControllerRoute.name: (routeData) {
return MaterialPageX<dynamic>(
@@ -163,24 +164,29 @@ class ImageViewerRouteArgs {
/// generated route for
/// [VideoViewerPage]
class VideoViewerRoute extends PageRouteInfo<VideoViewerRouteArgs> {
VideoViewerRoute({Key? key, required String videoUrl})
VideoViewerRoute(
{Key? key, required String videoUrl, required ImmichAsset asset})
: super(VideoViewerRoute.name,
path: '/video-viewer-page',
args: VideoViewerRouteArgs(key: key, videoUrl: videoUrl));
args: VideoViewerRouteArgs(
key: key, videoUrl: videoUrl, asset: asset));
static const String name = 'VideoViewerRoute';
}
class VideoViewerRouteArgs {
const VideoViewerRouteArgs({this.key, required this.videoUrl});
const VideoViewerRouteArgs(
{this.key, required this.videoUrl, required this.asset});
final Key? key;
final String videoUrl;
final ImmichAsset asset;
@override
String toString() {
return 'VideoViewerRouteArgs{key: $key, videoUrl: $videoUrl}';
return 'VideoViewerRouteArgs{key: $key, videoUrl: $videoUrl, asset: $asset}';
}
}

View File

@@ -10,6 +10,8 @@ class ImmichAsset {
final String modifiedAt;
final bool isFavorite;
final String? duration;
final String originalPath;
final String resizePath;
ImmichAsset({
required this.id,
@@ -21,6 +23,8 @@ class ImmichAsset {
required this.modifiedAt,
required this.isFavorite,
this.duration,
required this.originalPath,
required this.resizePath,
});
ImmichAsset copyWith({
@@ -33,6 +37,8 @@ class ImmichAsset {
String? modifiedAt,
bool? isFavorite,
String? duration,
String? originalPath,
String? resizePath,
}) {
return ImmichAsset(
id: id ?? this.id,
@@ -44,6 +50,8 @@ class ImmichAsset {
modifiedAt: modifiedAt ?? this.modifiedAt,
isFavorite: isFavorite ?? this.isFavorite,
duration: duration ?? this.duration,
originalPath: originalPath ?? this.originalPath,
resizePath: resizePath ?? this.resizePath,
);
}
@@ -58,6 +66,8 @@ class ImmichAsset {
'modifiedAt': modifiedAt,
'isFavorite': isFavorite,
'duration': duration,
'originalPath': originalPath,
'resizePath': resizePath,
};
}
@@ -72,6 +82,8 @@ class ImmichAsset {
modifiedAt: map['modifiedAt'] ?? '',
isFavorite: map['isFavorite'] ?? false,
duration: map['duration'],
originalPath: map['originalPath'] ?? '',
resizePath: map['resizePath'] ?? '',
);
}
@@ -81,7 +93,7 @@ class ImmichAsset {
@override
String toString() {
return 'ImmichAsset(id: $id, deviceAssetId: $deviceAssetId, userId: $userId, deviceId: $deviceId, type: $type, createdAt: $createdAt, modifiedAt: $modifiedAt, isFavorite: $isFavorite, duration: $duration)';
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
@@ -97,7 +109,9 @@ class ImmichAsset {
other.createdAt == createdAt &&
other.modifiedAt == modifiedAt &&
other.isFavorite == isFavorite &&
other.duration == duration;
other.duration == duration &&
other.originalPath == originalPath &&
other.resizePath == resizePath;
}
@override
@@ -110,6 +124,8 @@ class ImmichAsset {
createdAt.hashCode ^
modifiedAt.hashCode ^
isFavorite.hashCode ^
duration.hashCode;
duration.hashCode ^
originalPath.hashCode ^
resizePath.hashCode;
}
}

View File

@@ -106,6 +106,20 @@ class WebsocketNotifier extends StateNotifier<WebscoketState> {
}
}
}
stopListenToEvent(String eventName) {
debugPrint("[Websocket] Stop listening to event $eventName");
state.socket?.off(eventName);
}
listenUploadEvent() {
debugPrint("[Websocket] Start listening to event on_upload_success");
state.socket?.on('on_upload_success', (data) {
var jsonString = jsonDecode(data.toString());
ImmichAsset newAsset = ImmichAsset.fromMap(jsonString);
ref.watch(assetProvider.notifier).onNewAssetUploaded(newAsset);
});
}
}
final websocketProvider = StateNotifierProvider<WebsocketNotifier, WebscoketState>((ref) {

View File

@@ -73,7 +73,7 @@ class BackupService {
});
// Build thumbnail multipart data
var thumbnailData = await entity.thumbDataWithSize(1280, 720);
var thumbnailData = await entity.thumbnailDataWithSize(const ThumbnailSize(720, 1280));
if (thumbnailData != null) {
thumbnailUploadData = MultipartFile.fromBytes(
List.from(thumbnailData),

View File

@@ -1,3 +1,4 @@
import 'dart:async';
import 'dart:convert';
import 'package:dio/dio.dart';
@@ -25,16 +26,36 @@ class NetworkService {
}
}
Future<dynamic> getRequest({required String url}) async {
Future<dynamic> getRequest({required String url, bool isByteResponse = false, bool isStreamReponse = false}) async {
try {
var dio = Dio();
dio.interceptors.add(AuthenticatedRequestInterceptor());
var savedEndpoint = Hive.box(userInfoBox).get(serverEndpointKey);
Response res = await dio.get('$savedEndpoint/$url');
if (res.statusCode == 200) {
return res;
if (isByteResponse) {
Response<List<int>> res = await dio.get<List<int>>(
'$savedEndpoint/$url',
options: Options(responseType: ResponseType.bytes),
);
if (res.statusCode == 200) {
return res;
}
} else if (isStreamReponse) {
Response<ResponseBody> res = await dio.get<ResponseBody>(
'$savedEndpoint/$url',
options: Options(responseType: ResponseType.stream),
);
if (res.statusCode == 200) {
return res;
}
} else {
Response res = await dio.get('$savedEndpoint/$url');
if (res.statusCode == 200) {
return res;
}
}
} on DioError catch (e) {
debugPrint("DioError: ${e.response}");

View File

@@ -8,12 +8,24 @@ class ImmichToast {
required BuildContext context,
required String msg,
ToastType toastType = ToastType.info,
ToastGravity gravity = ToastGravity.TOP,
}) {
FToast fToast;
fToast = FToast();
fToast.init(context);
_getColor(ToastType type, BuildContext context) {
switch (type) {
case ToastType.info:
return Theme.of(context).primaryColor;
case ToastType.success:
return const Color.fromARGB(255, 78, 140, 124);
case ToastType.error:
return const Color.fromARGB(255, 220, 48, 85);
}
}
fToast.showToast(
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 12.0),
@@ -36,8 +48,8 @@ class ImmichToast {
: Container(),
(toastType == ToastType.success)
? const Icon(
Icons.check,
color: Color.fromARGB(255, 104, 248, 140),
Icons.check_circle_rounded,
color: Color.fromARGB(255, 78, 140, 124),
)
: Container(),
(toastType == ToastType.error)
@@ -53,7 +65,7 @@ class ImmichToast {
child: Text(
msg,
style: TextStyle(
color: Theme.of(context).primaryColor,
color: _getColor(toastType, context),
fontWeight: FontWeight.bold,
fontSize: 15,
),
@@ -62,7 +74,7 @@ class ImmichToast {
],
),
),
gravity: ToastGravity.TOP,
gravity: gravity,
toastDuration: const Duration(seconds: 2),
);
}

View File

@@ -6,6 +6,7 @@ import 'package:immich_mobile/modules/login/models/authentication_state.model.da
import 'package:immich_mobile/shared/models/backup_state.model.dart';
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
import 'package:immich_mobile/shared/providers/backup.provider.dart';
import 'package:immich_mobile/shared/providers/websocket.provider.dart';
import 'package:percent_indicator/linear_percent_indicator.dart';
class BackupControllerPage extends HookConsumerWidget {
@@ -23,6 +24,7 @@ class BackupControllerPage extends HookConsumerWidget {
ref.read(backupProvider.notifier).getBackupInfo();
}
ref.watch(websocketProvider.notifier).stopListenToEvent('on_upload_success');
return null;
}, []);
@@ -107,6 +109,7 @@ class BackupControllerPage extends HookConsumerWidget {
),
leading: IconButton(
onPressed: () {
ref.watch(websocketProvider.notifier).listenUploadEvent();
AutoRouter.of(context).pop(true);
},
icon: const Icon(Icons.arrow_back_ios_rounded)),

View File

@@ -328,6 +328,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.0-dev.0"
flutter_spinkit:
dependency: "direct main"
description:
name: flutter_spinkit
url: "https://pub.dartlang.org"
source: hosted
version: "5.1.0"
flutter_test:
dependency: "direct dev"
description: flutter
@@ -366,13 +373,6 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.2"
google_fonts:
dependency: "direct main"
description:
name: google_fonts
url: "https://pub.dartlang.org"
source: hosted
version: "2.2.0"
graphs:
dependency: transitive
description:
@@ -680,7 +680,7 @@ packages:
name: photo_manager
url: "https://pub.dartlang.org"
source: hosted
version: "1.3.10"
version: "2.0.6"
photo_view:
dependency: "direct main"
description:

View File

@@ -1,8 +1,8 @@
name: immich_mobile
description: A new Flutter project.
description: Immich - selfhosted backup media file on mobile phone
publish_to: "none"
version: 1.4.0+6
version: 1.5.1+9
environment:
sdk: ">=2.15.1 <3.0.0"
@@ -11,14 +11,14 @@ dependencies:
flutter:
sdk: flutter
cupertino_icons: ^1.0.2
photo_manager: ^1.3.10
photo_manager: ^2.0.6
flutter_hooks: ^0.18.0
hooks_riverpod: ^2.0.0-dev.0
hive:
hive_flutter:
dio: ^4.0.4
cached_network_image: ^3.2.0
google_fonts: ^2.2.0
# google_fonts: ^2.2.0
percent_indicator: ^3.4.0
intl: ^0.17.0
auto_route: ^3.2.2
@@ -33,10 +33,10 @@ dependencies:
badges: ^2.0.2
photo_view: ^0.13.0
socket_io_client: ^2.0.0-beta.4-nullsafety.0
# mapbox_gl: ^0.15.0
flutter_map: ^0.14.0
flutter_udid: ^2.0.0
package_info_plus: ^1.4.0
flutter_spinkit: ^5.1.0
dev_dependencies:
flutter_test:
@@ -50,6 +50,15 @@ flutter:
uses-material-design: true
assets:
- assets/
fonts:
- family: WorkSans
fonts:
- asset: fonts/WorkSans.ttf
- asset: fonts/WorkSans-Italic.ttf
style: italic
- family: SnowburstOne
fonts:
- asset: fonts/SnowburstOne.ttf
flutter_icons:
image_path_android: "assets/immich-logo-no-outline.png"

View File

@@ -1,6 +1,6 @@
{
"name": "immich",
"version": "1.3.2",
"version": "1.5.1",
"description": "",
"author": "",
"private": true,

View File

@@ -76,6 +76,15 @@ export class AssetController {
return 'ok';
}
@Get('/download')
async downloadFile(
@GetAuthUser() authUser: AuthUserDto,
@Response({ passthrough: true }) res: Res,
@Query(ValidationPipe) query: ServeFileDto,
) {
return this.assetService.downloadFile(authUser, query, res);
}
@Get('/file')
async serveFile(
@Headers() headers,
@@ -106,18 +115,8 @@ export class AssetController {
return this.assetService.searchAsset(authUser, searchAssetDto);
}
@Get('/new')
async getNewAssets(@GetAuthUser() authUser: AuthUserDto, @Query(ValidationPipe) query: GetNewAssetQueryDto) {
return await this.assetService.getNewAssets(authUser, query.latestDate);
}
@Get('/all')
async getAllAssets(@GetAuthUser() authUser: AuthUserDto, @Query(ValidationPipe) query: GetAllAssetQueryDto) {
return await this.assetService.getAllAssets(authUser, query);
}
@Get('/')
async getAllAssetsNoPagination(@GetAuthUser() authUser: AuthUserDto) {
async getAllAssets(@GetAuthUser() authUser: AuthUserDto) {
return await this.assetService.getAllAssetsNoPagination(authUser);
}
@@ -128,7 +127,7 @@ export class AssetController {
@Get('/assetById/:assetId')
async getAssetById(@GetAuthUser() authUser: AuthUserDto, @Param('assetId') assetId) {
return this.assetService.getAssetById(authUser, assetId);
return await this.assetService.getAssetById(authUser, assetId);
}
@Delete('/')

View File

@@ -13,6 +13,7 @@ import { Response as Res } from 'express';
import { promisify } from 'util';
import { DeleteAssetDto } from './dto/delete-asset.dto';
import { SearchAssetDto } from './dto/search-asset.dto';
import path from 'path';
const fileInfo = promisify(stat);
@@ -75,42 +76,6 @@ export class AssetService {
}
}
public async getAllAssets(authUser: AuthUserDto, query: GetAllAssetQueryDto): Promise<GetAllAssetReponseDto> {
try {
const assets = await this.assetRepository
.createQueryBuilder('a')
.where('a."userId" = :userId', { userId: authUser.id })
.andWhere('a."createdAt" < :lastQueryCreatedAt', {
lastQueryCreatedAt: query.nextPageKey || new Date().toISOString(),
})
.orderBy('a."createdAt"::date', 'DESC')
.take(5000)
.getMany();
if (assets.length > 0) {
const data = _.groupBy(assets, (a) => new Date(a.createdAt).toISOString().slice(0, 10));
const formattedData = [];
Object.keys(data).forEach((v) => formattedData.push({ date: v, assets: data[v] }));
const response = new GetAllAssetReponseDto();
response.count = assets.length;
response.data = formattedData;
response.nextPageKey = assets[assets.length - 1].createdAt;
return response;
} else {
const response = new GetAllAssetReponseDto();
response.count = 0;
response.data = [];
response.nextPageKey = 'null';
return response;
}
} catch (e) {
Logger.error(e, 'getAllAssets');
}
}
public async findOne(authUser: AuthUserDto, deviceId: string, assetId: string): Promise<AssetEntity> {
const rows = await this.assetRepository.query(
'SELECT * FROM assets a WHERE a."deviceAssetId" = $1 AND a."userId" = $2 AND a."deviceId" = $3',
@@ -124,18 +89,6 @@ export class AssetService {
return rows[0] as AssetEntity;
}
public async getNewAssets(authUser: AuthUserDto, latestDate: string) {
return await this.assetRepository.find({
where: {
userId: authUser.id,
createdAt: MoreThan(latestDate),
},
order: {
createdAt: 'ASC', // ASC order to add existed asset the latest group first before creating a new date group.
},
});
}
public async getAssetById(authUser: AuthUserDto, assetId: string) {
return await this.assetRepository.findOne({
where: {
@@ -146,10 +99,26 @@ export class AssetService {
});
}
public async downloadFile(authUser: AuthUserDto, query: ServeFileDto, res: Res) {
let file = null;
const asset = await this.findOne(authUser, query.did, query.aid);
if (query.isThumb === 'false' || !query.isThumb) {
file = createReadStream(asset.originalPath);
} else {
file = createReadStream(asset.resizePath);
}
return new StreamableFile(file);
}
public async serveFile(authUser: AuthUserDto, query: ServeFileDto, res: Res, headers: any) {
let file = null;
const asset = await this.findOne(authUser, query.did, query.aid);
if (!asset) {
throw new BadRequestException('Asset does not exist');
}
// Handle Sending Images
if (asset.type == AssetType.IMAGE || query.isThumb == 'true') {
res.set({

View File

@@ -3,7 +3,7 @@
export const serverVersion = {
major: 1,
minor: 4,
patch: 0,
build: 0,
minor: 5,
patch: 1,
build: 9,
};