Merge branch 'immich-app:main' into feat/samsung-raw-and-fujifilm-raf
This commit is contained in:
@@ -3,6 +3,10 @@ name: Build Mobile
|
|||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
workflow_call:
|
workflow_call:
|
||||||
|
inputs:
|
||||||
|
ref:
|
||||||
|
required: false
|
||||||
|
type: string
|
||||||
pull_request:
|
pull_request:
|
||||||
push:
|
push:
|
||||||
branches: [main]
|
branches: [main]
|
||||||
@@ -13,7 +17,17 @@ jobs:
|
|||||||
runs-on: macos-12
|
runs-on: macos-12
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
|
- name: Determine ref
|
||||||
|
id: get-ref
|
||||||
|
run: |
|
||||||
|
input_ref="${{ inputs.ref }}"
|
||||||
|
github_ref="${{ github.sha }}"
|
||||||
|
ref="${input_ref:-$github_ref}"
|
||||||
|
echo "ref=$ref" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
ref: ${{ steps.get-ref.outputs.ref }}
|
||||||
|
|
||||||
- uses: actions/setup-java@v3
|
- uses: actions/setup-java@v3
|
||||||
with:
|
with:
|
||||||
|
|||||||
@@ -98,3 +98,4 @@ jobs:
|
|||||||
cache-from: type=registry,ref=ghcr.io/${{ github.repository_owner }}/immich-build-cache:${{matrix.image}}
|
cache-from: type=registry,ref=ghcr.io/${{ github.repository_owner }}/immich-build-cache:${{matrix.image}}
|
||||||
cache-to: ${{ steps.cache-target.outputs.cache-to }}
|
cache-to: ${{ steps.cache-target.outputs.cache-to }}
|
||||||
tags: ${{ steps.metadata.outputs.tags }}
|
tags: ${{ steps.metadata.outputs.tags }}
|
||||||
|
labels: ${{ steps.metadata.outputs.labels }}
|
||||||
|
|||||||
@@ -18,13 +18,11 @@ on:
|
|||||||
type: boolean
|
type: boolean
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build_mobile:
|
bump_version:
|
||||||
uses: ./.github/workflows/build-mobile.yml
|
|
||||||
secrets: inherit
|
|
||||||
|
|
||||||
tag_release:
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: build_mobile
|
|
||||||
|
outputs:
|
||||||
|
ref: ${{ steps.push-tag.outputs.commit_long_sha }}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
@@ -36,6 +34,7 @@ jobs:
|
|||||||
run: misc/release/pump-version.sh -s "${{ inputs.serverBump }}" -m "${{ inputs.mobileBump }}"
|
run: misc/release/pump-version.sh -s "${{ inputs.serverBump }}" -m "${{ inputs.mobileBump }}"
|
||||||
|
|
||||||
- name: Commit and tag
|
- name: Commit and tag
|
||||||
|
id: push-tag
|
||||||
uses: EndBug/add-and-commit@v9
|
uses: EndBug/add-and-commit@v9
|
||||||
with:
|
with:
|
||||||
author_name: Immich Release Bot
|
author_name: Immich Release Bot
|
||||||
@@ -44,6 +43,18 @@ jobs:
|
|||||||
tag: ${{ env.IMMICH_VERSION }}
|
tag: ${{ env.IMMICH_VERSION }}
|
||||||
push: true
|
push: true
|
||||||
|
|
||||||
|
build_mobile:
|
||||||
|
uses: ./.github/workflows/build-mobile.yml
|
||||||
|
needs: bump_version
|
||||||
|
secrets: inherit
|
||||||
|
with:
|
||||||
|
ref: ${{ needs.bump_version.outputs.ref }}
|
||||||
|
|
||||||
|
prepare_release:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: build_mobile
|
||||||
|
|
||||||
|
steps:
|
||||||
- name: Download APK
|
- name: Download APK
|
||||||
uses: actions/download-artifact@v3
|
uses: actions/download-artifact@v3
|
||||||
with:
|
with:
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
FROM node:16-bullseye-slim as builder
|
FROM node:16-bullseye-slim as builder
|
||||||
|
|
||||||
ARG DEBIAN_FRONTEND=noninteractive
|
ARG DEBIAN_FRONTEND=noninteractive
|
||||||
@@ -6,7 +5,7 @@ ARG DEBIAN_FRONTEND=noninteractive
|
|||||||
WORKDIR /usr/src/app
|
WORKDIR /usr/src/app
|
||||||
|
|
||||||
RUN apt-get update
|
RUN apt-get update
|
||||||
RUN apt-get install gcc g++ make cmake python3 python3-pip ffmpeg -y
|
RUN apt-get install gcc g++ make cmake python3 python3-pip -y
|
||||||
|
|
||||||
COPY package.json package-lock.json ./
|
COPY package.json package-lock.json ./
|
||||||
|
|
||||||
@@ -15,24 +14,17 @@ RUN npm rebuild @tensorflow/tfjs-node --build-from-source
|
|||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
|
|
||||||
FROM builder as prod
|
FROM builder as prod
|
||||||
|
|
||||||
RUN npm run build
|
RUN npm run build
|
||||||
|
|
||||||
RUN npm prune --omit=dev
|
RUN npm prune --omit=dev
|
||||||
|
|
||||||
|
|
||||||
FROM node:16-bullseye-slim
|
FROM node:16-bullseye-slim
|
||||||
|
|
||||||
ARG DEBIAN_FRONTEND=noninteractive
|
ARG DEBIAN_FRONTEND=noninteractive
|
||||||
|
|
||||||
WORKDIR /usr/src/app
|
WORKDIR /usr/src/app
|
||||||
|
|
||||||
RUN apt-get update \
|
|
||||||
&& apt-get install -y ffmpeg \
|
|
||||||
&& rm -rf /var/cache/apt/lists
|
|
||||||
|
|
||||||
COPY --from=prod /usr/src/app/node_modules ./node_modules
|
COPY --from=prod /usr/src/app/node_modules ./node_modules
|
||||||
COPY --from=prod /usr/src/app/dist ./dist
|
COPY --from=prod /usr/src/app/dist ./dist
|
||||||
|
|
||||||
|
|||||||
Generated
+331
-234
File diff suppressed because it is too large
Load Diff
@@ -23,19 +23,9 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nestjs/common": "^8.0.0",
|
"@nestjs/common": "^8.0.0",
|
||||||
"@nestjs/core": "^8.0.0",
|
"@nestjs/core": "^8.0.0",
|
||||||
"@nestjs/mapped-types": "^1.0.1",
|
|
||||||
"@nestjs/platform-express": "^8.0.0",
|
|
||||||
"@tensorflow-models/coco-ssd": "^2.2.2",
|
"@tensorflow-models/coco-ssd": "^2.2.2",
|
||||||
"@tensorflow-models/mobilenet": "^2.1.0",
|
"@tensorflow-models/mobilenet": "^2.1.0",
|
||||||
"@tensorflow/tfjs": "^3.19.0",
|
"@tensorflow/tfjs-node": "^3.19.0"
|
||||||
"@tensorflow/tfjs-converter": "^3.19.0",
|
|
||||||
"@tensorflow/tfjs-core": "^3.19.0",
|
|
||||||
"@tensorflow/tfjs-node": "^3.19.0",
|
|
||||||
"@tensorflow/tfjs-node-gpu": "^3.19.0",
|
|
||||||
"@trpc/server": "^9.20.3",
|
|
||||||
"reflect-metadata": "^0.1.13",
|
|
||||||
"rimraf": "^3.0.2",
|
|
||||||
"rxjs": "^7.2.0"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@nestjs/cli": "^8.2.4",
|
"@nestjs/cli": "^8.2.4",
|
||||||
@@ -52,6 +42,7 @@
|
|||||||
"eslint-plugin-prettier": "^4.0.0",
|
"eslint-plugin-prettier": "^4.0.0",
|
||||||
"jest": "^27.2.5",
|
"jest": "^27.2.5",
|
||||||
"prettier": "^2.3.2",
|
"prettier": "^2.3.2",
|
||||||
|
"rimraf": "^3.0.2",
|
||||||
"source-map-support": "^0.5.20",
|
"source-map-support": "^0.5.20",
|
||||||
"supertest": "^6.1.3",
|
"supertest": "^6.1.3",
|
||||||
"ts-jest": "^27.0.3",
|
"ts-jest": "^27.0.3",
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ if [ "$CURRENT_SERVER" != "$NEXT_SERVER" ]; then
|
|||||||
|
|
||||||
sed -i "s/^ \"version\": \"$CURRENT_SERVER\",$/ \"version\": \"$NEXT_SERVER\",/" server/package.json
|
sed -i "s/^ \"version\": \"$CURRENT_SERVER\",$/ \"version\": \"$NEXT_SERVER\",/" server/package.json
|
||||||
sed -i "s/^ \"version\": \"$CURRENT_SERVER\",$/ \"version\": \"$NEXT_SERVER\",/" server/package-lock.json
|
sed -i "s/^ \"version\": \"$CURRENT_SERVER\",$/ \"version\": \"$NEXT_SERVER\",/" server/package-lock.json
|
||||||
|
sed -i "s/\"version\": \"$CURRENT_SERVER\",$/\"version\": \"$NEXT_SERVER\",/" server/immich-openapi-specs.json
|
||||||
sed -i "s/\"android\.injected\.version\.name\" => \"$CURRENT_SERVER\",/\"android\.injected\.version\.name\" => \"$NEXT_SERVER\",/" mobile/android/fastlane/Fastfile
|
sed -i "s/\"android\.injected\.version\.name\" => \"$CURRENT_SERVER\",/\"android\.injected\.version\.name\" => \"$NEXT_SERVER\",/" mobile/android/fastlane/Fastfile
|
||||||
sed -i "s/version_number: \"$CURRENT_SERVER\"$/version_number: \"$NEXT_SERVER\"/" mobile/ios/fastlane/Fastfile
|
sed -i "s/version_number: \"$CURRENT_SERVER\"$/version_number: \"$NEXT_SERVER\"/" mobile/ios/fastlane/Fastfile
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -35,8 +35,8 @@ platform :android do
|
|||||||
task: 'bundle',
|
task: 'bundle',
|
||||||
build_type: 'Release',
|
build_type: 'Release',
|
||||||
properties: {
|
properties: {
|
||||||
"android.injected.version.code" => 67,
|
"android.injected.version.code" => 68,
|
||||||
"android.injected.version.name" => "1.44.0",
|
"android.injected.version.name" => "1.45.0",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
upload_to_play_store(skip_upload_apk: true, skip_upload_images: true, skip_upload_screenshots: true, aab: '../build/app/outputs/bundle/release/app-release.aab')
|
upload_to_play_store(skip_upload_apk: true, skip_upload_images: true, skip_upload_screenshots: true, aab: '../build/app/outputs/bundle/release/app-release.aab')
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
* reused HTTP client for uploading assets
|
||||||
|
* top padding missing from drag handle in add to album bottom sheet
|
||||||
|
* Improve the gallery to improve scale, double tap, and swipe gesture detection
|
||||||
|
* Uses clamping scroll physics on android
|
||||||
|
* Fix back button closing the app from multi selection in Android
|
||||||
|
* add and delete operation asset in album viewer doesn't update count in album list
|
||||||
|
* Update share_file to latest version and migrate to using cross platform shareXFile
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
fixes back button while multiselecting showing the last selected image
|
||||||
|
fixes dark mode color for invite to album app bar
|
||||||
|
fixed local asset thumbnail size and eliminated fade in
|
||||||
|
Fullscreen image effects
|
||||||
@@ -5,17 +5,17 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000213">
|
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000307">
|
||||||
|
|
||||||
</testcase>
|
</testcase>
|
||||||
|
|
||||||
|
|
||||||
<testcase classname="fastlane.lanes" name="1: bundleRelease" time="61.218233">
|
<testcase classname="fastlane.lanes" name="1: bundleRelease" time="36.955598">
|
||||||
|
|
||||||
</testcase>
|
</testcase>
|
||||||
|
|
||||||
|
|
||||||
<testcase classname="fastlane.lanes" name="2: upload_to_play_store" time="41.974053">
|
<testcase classname="fastlane.lanes" name="2: upload_to_play_store" time="75.445063">
|
||||||
|
|
||||||
</testcase>
|
</testcase>
|
||||||
|
|
||||||
|
|||||||
@@ -360,7 +360,7 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = Runner/RunnerProfile.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Runner/RunnerProfile.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 82;
|
CURRENT_PROJECT_VERSION = 84;
|
||||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
@@ -495,7 +495,7 @@
|
|||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 82;
|
CURRENT_PROJECT_VERSION = 84;
|
||||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
@@ -522,7 +522,7 @@
|
|||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 82;
|
CURRENT_PROJECT_VERSION = 84;
|
||||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
|
|||||||
@@ -17,11 +17,11 @@
|
|||||||
<key>CFBundlePackageType</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>APPL</string>
|
<string>APPL</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>1.43.0</string>
|
<string>1.45.0</string>
|
||||||
<key>CFBundleSignature</key>
|
<key>CFBundleSignature</key>
|
||||||
<string>????</string>
|
<string>????</string>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>82</string>
|
<string>84</string>
|
||||||
<key>LSRequiresIPhoneOS</key>
|
<key>LSRequiresIPhoneOS</key>
|
||||||
<true />
|
<true />
|
||||||
<key>MGLMapboxMetricsEnabledSettingShownInApp</key>
|
<key>MGLMapboxMetricsEnabledSettingShownInApp</key>
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ platform :ios do
|
|||||||
desc "iOS Beta"
|
desc "iOS Beta"
|
||||||
lane :beta do
|
lane :beta do
|
||||||
increment_version_number(
|
increment_version_number(
|
||||||
version_number: "1.44.0"
|
version_number: "1.45.0"
|
||||||
)
|
)
|
||||||
increment_build_number(
|
increment_build_number(
|
||||||
build_number: latest_testflight_build_number + 1,
|
build_number: latest_testflight_build_number + 1,
|
||||||
|
|||||||
@@ -5,32 +5,32 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000396">
|
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000222">
|
||||||
|
|
||||||
</testcase>
|
</testcase>
|
||||||
|
|
||||||
|
|
||||||
<testcase classname="fastlane.lanes" name="1: increment_version_number" time="2.478301">
|
<testcase classname="fastlane.lanes" name="1: increment_version_number" time="2.33276">
|
||||||
|
|
||||||
</testcase>
|
</testcase>
|
||||||
|
|
||||||
|
|
||||||
<testcase classname="fastlane.lanes" name="2: latest_testflight_build_number" time="3.846552">
|
<testcase classname="fastlane.lanes" name="2: latest_testflight_build_number" time="5.370222">
|
||||||
|
|
||||||
</testcase>
|
</testcase>
|
||||||
|
|
||||||
|
|
||||||
<testcase classname="fastlane.lanes" name="3: increment_build_number" time="2.367554">
|
<testcase classname="fastlane.lanes" name="3: increment_build_number" time="1.977555">
|
||||||
|
|
||||||
</testcase>
|
</testcase>
|
||||||
|
|
||||||
|
|
||||||
<testcase classname="fastlane.lanes" name="4: build_app" time="75.618447">
|
<testcase classname="fastlane.lanes" name="4: build_app" time="94.172587">
|
||||||
|
|
||||||
</testcase>
|
</testcase>
|
||||||
|
|
||||||
|
|
||||||
<testcase classname="fastlane.lanes" name="5: upload_to_testflight" time="47.502114">
|
<testcase classname="fastlane.lanes" name="5: upload_to_testflight" time="82.579096">
|
||||||
|
|
||||||
</testcase>
|
</testcase>
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ const String deviceIdKey = 'immichBoxDeviceIdKey'; // Key 2
|
|||||||
const String isLoggedInKey = 'immichIsLoggedInKey'; // Key 3
|
const String isLoggedInKey = 'immichIsLoggedInKey'; // Key 3
|
||||||
const String serverEndpointKey = 'immichBoxServerEndpoint'; // Key 4
|
const String serverEndpointKey = 'immichBoxServerEndpoint'; // Key 4
|
||||||
const String assetEtagKey = 'immichAssetEtagKey'; // Key 5
|
const String assetEtagKey = 'immichAssetEtagKey'; // Key 5
|
||||||
|
const String userIdKey = 'immichUserIdKey'; // Key 6
|
||||||
|
|
||||||
// Login Info
|
// Login Info
|
||||||
const String hiveLoginInfoBox = "immichLoginInfoBox"; // Box
|
const String hiveLoginInfoBox = "immichLoginInfoBox"; // Box
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import 'dart:io';
|
|||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_displaymode/flutter_displaymode.dart';
|
import 'package:flutter_displaymode/flutter_displaymode.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';
|
||||||
@@ -155,6 +156,8 @@ class ImmichAppState extends ConsumerState<ImmichApp>
|
|||||||
var router = ref.watch(appRouterProvider);
|
var router = ref.watch(appRouterProvider);
|
||||||
ref.watch(releaseInfoProvider.notifier).checkGithubReleaseInfo();
|
ref.watch(releaseInfoProvider.notifier).checkGithubReleaseInfo();
|
||||||
|
|
||||||
|
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
|
||||||
|
|
||||||
return MaterialApp(
|
return MaterialApp(
|
||||||
localizationsDelegates: context.localizationDelegates,
|
localizationsDelegates: context.localizationDelegates,
|
||||||
supportedLocales: context.supportedLocales,
|
supportedLocales: context.supportedLocales,
|
||||||
|
|||||||
@@ -85,9 +85,11 @@ class AlbumViewerThumbnail extends HookConsumerWidget {
|
|||||||
right: 10,
|
right: 10,
|
||||||
bottom: 5,
|
bottom: 5,
|
||||||
child: Icon(
|
child: Icon(
|
||||||
(deviceId != asset.deviceId)
|
asset.isRemote
|
||||||
|
? (deviceId == asset.deviceId
|
||||||
? Icons.cloud_done_outlined
|
? Icons.cloud_done_outlined
|
||||||
: Icons.photo_library_rounded,
|
: Icons.cloud_outlined)
|
||||||
|
: Icons.cloud_off_outlined,
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
size: 18,
|
size: 18,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -121,7 +121,7 @@ class SelectionThumbnailImage extends HookConsumerWidget {
|
|||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
asset.duration.substring(0, 7),
|
asset.duration.toString().substring(0, 7),
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
fontSize: 10,
|
fontSize: 10,
|
||||||
|
|||||||
@@ -118,7 +118,6 @@ class SelectAdditionalUserForSharingPage extends HookConsumerWidget {
|
|||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: const Text(
|
title: const Text(
|
||||||
'share_invite',
|
'share_invite',
|
||||||
style: TextStyle(color: Colors.black),
|
|
||||||
).tr(),
|
).tr(),
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
centerTitle: false,
|
centerTitle: false,
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import 'package:immich_mobile/shared/models/asset.dart';
|
|||||||
import 'package:immich_mobile/shared/services/share.service.dart';
|
import 'package:immich_mobile/shared/services/share.service.dart';
|
||||||
import 'package:immich_mobile/shared/ui/immich_toast.dart';
|
import 'package:immich_mobile/shared/ui/immich_toast.dart';
|
||||||
import 'package:immich_mobile/shared/ui/share_dialog.dart';
|
import 'package:immich_mobile/shared/ui/share_dialog.dart';
|
||||||
import 'package:openapi/api.dart';
|
|
||||||
|
|
||||||
class ImageViewerStateNotifier extends StateNotifier<ImageViewerPageState> {
|
class ImageViewerStateNotifier extends StateNotifier<ImageViewerPageState> {
|
||||||
final ImageViewerService _imageViewerService;
|
final ImageViewerService _imageViewerService;
|
||||||
@@ -20,7 +19,7 @@ class ImageViewerStateNotifier extends StateNotifier<ImageViewerPageState> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
void downloadAsset(AssetResponseDto asset, BuildContext context) async {
|
void downloadAsset(Asset 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);
|
||||||
|
|||||||
@@ -2,10 +2,9 @@ import 'dart:io';
|
|||||||
|
|
||||||
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/shared/models/asset.dart';
|
||||||
import 'package:immich_mobile/shared/providers/api.provider.dart';
|
import 'package:immich_mobile/shared/providers/api.provider.dart';
|
||||||
import 'package:immich_mobile/shared/services/api.service.dart';
|
import 'package:immich_mobile/shared/services/api.service.dart';
|
||||||
import 'package:openapi/api.dart';
|
|
||||||
import 'package:path/path.dart' as p;
|
|
||||||
|
|
||||||
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';
|
||||||
@@ -18,22 +17,16 @@ class ImageViewerService {
|
|||||||
|
|
||||||
ImageViewerService(this._apiService);
|
ImageViewerService(this._apiService);
|
||||||
|
|
||||||
Future<bool> downloadAssetToDevice(AssetResponseDto asset) async {
|
Future<bool> downloadAssetToDevice(Asset asset) async {
|
||||||
try {
|
try {
|
||||||
String fileName = p.basename(asset.originalPath);
|
|
||||||
|
|
||||||
// Download LivePhotos image and motion part
|
// Download LivePhotos image and motion part
|
||||||
if (asset.type == AssetTypeEnum.IMAGE && asset.livePhotoVideoId != null) {
|
if (asset.isImage && asset.livePhotoVideoId != null) {
|
||||||
var imageResponse = await _apiService.assetApi.downloadFileWithHttpInfo(
|
var imageResponse = await _apiService.assetApi.downloadFileWithHttpInfo(
|
||||||
asset.id,
|
asset.remoteId!,
|
||||||
isThumb: false,
|
|
||||||
isWeb: false,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
var motionReponse = await _apiService.assetApi.downloadFileWithHttpInfo(
|
var motionReponse = await _apiService.assetApi.downloadFileWithHttpInfo(
|
||||||
asset.livePhotoVideoId!,
|
asset.livePhotoVideoId!,
|
||||||
isThumb: false,
|
|
||||||
isWeb: false,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
final AssetEntity? entity;
|
final AssetEntity? entity;
|
||||||
@@ -47,30 +40,28 @@ class ImageViewerService {
|
|||||||
entity = await PhotoManager.editor.darwin.saveLivePhoto(
|
entity = await PhotoManager.editor.darwin.saveLivePhoto(
|
||||||
imageFile: imageFile,
|
imageFile: imageFile,
|
||||||
videoFile: videoFile,
|
videoFile: videoFile,
|
||||||
title: p.basename(asset.originalPath),
|
title: asset.fileName,
|
||||||
);
|
);
|
||||||
|
|
||||||
return entity != null;
|
return entity != null;
|
||||||
} else {
|
} else {
|
||||||
var res = await _apiService.assetApi.downloadFileWithHttpInfo(
|
var res = await _apiService.assetApi
|
||||||
asset.id,
|
.downloadFileWithHttpInfo(asset.remoteId!);
|
||||||
isThumb: false,
|
|
||||||
isWeb: false,
|
|
||||||
);
|
|
||||||
|
|
||||||
final AssetEntity? entity;
|
final AssetEntity? entity;
|
||||||
|
|
||||||
if (asset.type == AssetTypeEnum.IMAGE) {
|
if (asset.isImage) {
|
||||||
entity = await PhotoManager.editor.saveImage(
|
entity = await PhotoManager.editor.saveImage(
|
||||||
res.bodyBytes,
|
res.bodyBytes,
|
||||||
title: p.basename(asset.originalPath),
|
title: asset.fileName,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
final tempDir = await getTemporaryDirectory();
|
final tempDir = await getTemporaryDirectory();
|
||||||
File tempFile = await File('${tempDir.path}/$fileName').create();
|
File tempFile =
|
||||||
|
await File('${tempDir.path}/${asset.fileName}').create();
|
||||||
tempFile.writeAsBytesSync(res.bodyBytes);
|
tempFile.writeAsBytesSync(res.bodyBytes);
|
||||||
entity =
|
entity = await PhotoManager.editor
|
||||||
await PhotoManager.editor.saveVideo(tempFile, title: fileName);
|
.saveVideo(tempFile, title: asset.fileName);
|
||||||
}
|
}
|
||||||
return entity != null;
|
return entity != null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,9 +3,8 @@ 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/asset.dart';
|
import 'package:immich_mobile/shared/models/asset.dart';
|
||||||
|
import 'package:immich_mobile/shared/models/exif_info.dart';
|
||||||
import 'package:immich_mobile/shared/ui/drag_sheet.dart';
|
import 'package:immich_mobile/shared/ui/drag_sheet.dart';
|
||||||
import 'package:openapi/api.dart';
|
|
||||||
import 'package:path/path.dart' as p;
|
|
||||||
import 'package:latlong2/latlong.dart';
|
import 'package:latlong2/latlong.dart';
|
||||||
import 'package:immich_mobile/utils/bytes_units.dart';
|
import 'package:immich_mobile/utils/bytes_units.dart';
|
||||||
|
|
||||||
@@ -68,7 +67,7 @@ class ExifBottomSheet extends HookConsumerWidget {
|
|||||||
|
|
||||||
final textColor = Theme.of(context).primaryColor;
|
final textColor = Theme.of(context).primaryColor;
|
||||||
|
|
||||||
ExifResponseDto? exifInfo = assetDetail.remote?.exifInfo;
|
ExifInfo? exifInfo = assetDetail.exifInfo;
|
||||||
|
|
||||||
buildLocationText() {
|
buildLocationText() {
|
||||||
return Text(
|
return Text(
|
||||||
@@ -81,6 +80,17 @@ class ExifBottomSheet extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
buildSizeText(Asset a) {
|
||||||
|
String resolution = a.width != null && a.height != null
|
||||||
|
? "${a.height} x ${a.width} "
|
||||||
|
: "";
|
||||||
|
String fileSize = a.exifInfo?.fileSize != null
|
||||||
|
? formatBytes(a.exifInfo!.fileSize!)
|
||||||
|
: "";
|
||||||
|
String text = resolution + fileSize;
|
||||||
|
return text.isEmpty ? null : Text(text);
|
||||||
|
}
|
||||||
|
|
||||||
return SingleChildScrollView(
|
return SingleChildScrollView(
|
||||||
child: Card(
|
child: Card(
|
||||||
shape: const RoundedRectangleBorder(
|
shape: const RoundedRectangleBorder(
|
||||||
@@ -101,10 +111,9 @@ class ExifBottomSheet extends HookConsumerWidget {
|
|||||||
child: CustomDraggingHandle(),
|
child: CustomDraggingHandle(),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
if (exifInfo?.dateTimeOriginal != null)
|
|
||||||
Text(
|
Text(
|
||||||
DateFormat('date_format'.tr()).format(
|
DateFormat('date_format'.tr()).format(
|
||||||
exifInfo!.dateTimeOriginal!.toLocal(),
|
assetDetail.createdAt.toLocal(),
|
||||||
),
|
),
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
@@ -113,7 +122,7 @@ class ExifBottomSheet extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
|
|
||||||
// Location
|
// Location
|
||||||
if (assetDetail.latitude != null)
|
if (assetDetail.latitude != null && assetDetail.longitude != null)
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(top: 32.0),
|
padding: const EdgeInsets.only(top: 32.0),
|
||||||
child: Column(
|
child: Column(
|
||||||
@@ -126,22 +135,19 @@ class ExifBottomSheet extends HookConsumerWidget {
|
|||||||
"exif_bottom_sheet_location",
|
"exif_bottom_sheet_location",
|
||||||
style: TextStyle(fontSize: 11, color: textColor),
|
style: TextStyle(fontSize: 11, color: textColor),
|
||||||
).tr(),
|
).tr(),
|
||||||
if (assetDetail.latitude != null &&
|
|
||||||
assetDetail.longitude != null)
|
|
||||||
buildMap(),
|
buildMap(),
|
||||||
if (exifInfo != null &&
|
if (exifInfo != null &&
|
||||||
exifInfo.city != null &&
|
exifInfo.city != null &&
|
||||||
exifInfo.state != null)
|
exifInfo.state != null)
|
||||||
buildLocationText(),
|
buildLocationText(),
|
||||||
Text(
|
Text(
|
||||||
"${assetDetail.latitude?.toStringAsFixed(4)}, ${assetDetail.longitude?.toStringAsFixed(4)}",
|
"${assetDetail.latitude!.toStringAsFixed(4)}, ${assetDetail.longitude!.toStringAsFixed(4)}",
|
||||||
style: const TextStyle(fontSize: 12),
|
style: const TextStyle(fontSize: 12),
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
// Detail
|
// Detail
|
||||||
if (exifInfo != null)
|
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(top: 32.0),
|
padding: const EdgeInsets.only(top: 32.0),
|
||||||
child: Column(
|
child: Column(
|
||||||
@@ -163,25 +169,21 @@ class ExifBottomSheet extends HookConsumerWidget {
|
|||||||
dense: true,
|
dense: true,
|
||||||
leading: const Icon(Icons.image),
|
leading: const Icon(Icons.image),
|
||||||
title: Text(
|
title: Text(
|
||||||
"${exifInfo.imageName!}${p.extension(assetDetail.remote!.originalPath)}",
|
assetDetail.fileName,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
color: textColor,
|
color: textColor,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
subtitle: exifInfo.exifImageHeight != null
|
subtitle: buildSizeText(assetDetail),
|
||||||
? Text(
|
|
||||||
"${exifInfo.exifImageHeight} x ${exifInfo.exifImageWidth} ${formatBytes(exifInfo.fileSizeInByte ?? 0)} ",
|
|
||||||
)
|
|
||||||
: null,
|
|
||||||
),
|
),
|
||||||
if (exifInfo.make != null)
|
if (exifInfo?.make != null)
|
||||||
ListTile(
|
ListTile(
|
||||||
contentPadding: const EdgeInsets.all(0),
|
contentPadding: const EdgeInsets.all(0),
|
||||||
dense: true,
|
dense: true,
|
||||||
leading: const Icon(Icons.camera),
|
leading: const Icon(Icons.camera),
|
||||||
title: Text(
|
title: Text(
|
||||||
"${exifInfo.make} ${exifInfo.model}",
|
"${exifInfo!.make} ${exifInfo.model}",
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: textColor,
|
color: textColor,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ 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/asset.dart';
|
import 'package:immich_mobile/shared/models/asset.dart';
|
||||||
|
|
||||||
class TopControlAppBar extends HookConsumerWidget with PreferredSizeWidget {
|
class TopControlAppBar extends HookConsumerWidget {
|
||||||
const TopControlAppBar({
|
const TopControlAppBar({
|
||||||
Key? key,
|
Key? key,
|
||||||
required this.asset,
|
required this.asset,
|
||||||
@@ -31,7 +31,6 @@ class TopControlAppBar extends HookConsumerWidget with PreferredSizeWidget {
|
|||||||
|
|
||||||
return AppBar(
|
return AppBar(
|
||||||
foregroundColor: Colors.grey[100],
|
foregroundColor: Colors.grey[100],
|
||||||
toolbarHeight: 60,
|
|
||||||
backgroundColor: Colors.transparent,
|
backgroundColor: Colors.transparent,
|
||||||
leading: IconButton(
|
leading: IconButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
@@ -44,7 +43,7 @@ class TopControlAppBar extends HookConsumerWidget with PreferredSizeWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
if (asset.remote?.livePhotoVideoId != null)
|
if (asset.livePhotoVideoId != null)
|
||||||
IconButton(
|
IconButton(
|
||||||
iconSize: iconSize,
|
iconSize: iconSize,
|
||||||
splashRadius: iconSize,
|
splashRadius: iconSize,
|
||||||
@@ -105,7 +104,6 @@ class TopControlAppBar extends HookConsumerWidget with PreferredSizeWidget {
|
|||||||
color: Colors.grey[200],
|
color: Colors.grey[200],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (asset.isRemote)
|
|
||||||
IconButton(
|
IconButton(
|
||||||
iconSize: iconSize,
|
iconSize: iconSize,
|
||||||
splashRadius: iconSize,
|
splashRadius: iconSize,
|
||||||
@@ -120,7 +118,4 @@ class TopControlAppBar extends HookConsumerWidget with PreferredSizeWidget {
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
|
||||||
Size get preferredSize => const Size.fromHeight(kToolbarHeight);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import 'package:immich_mobile/modules/asset_viewer/providers/image_viewer_page_s
|
|||||||
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/asset_viewer/views/video_viewer_page.dart';
|
import 'package:immich_mobile/modules/asset_viewer/views/video_viewer_page.dart';
|
||||||
import 'package:immich_mobile/modules/home/services/asset.service.dart';
|
import 'package:immich_mobile/shared/services/asset.service.dart';
|
||||||
import 'package:immich_mobile/modules/home/ui/delete_diaglog.dart';
|
import 'package:immich_mobile/modules/home/ui/delete_diaglog.dart';
|
||||||
import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart';
|
import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart';
|
||||||
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
|
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
|
||||||
@@ -48,6 +48,7 @@ class GalleryViewerPage extends HookConsumerWidget {
|
|||||||
final isLoadPreview = useState(AppSettingsEnum.loadPreview.defaultValue);
|
final isLoadPreview = useState(AppSettingsEnum.loadPreview.defaultValue);
|
||||||
final isLoadOriginal = useState(AppSettingsEnum.loadOriginal.defaultValue);
|
final isLoadOriginal = useState(AppSettingsEnum.loadOriginal.defaultValue);
|
||||||
final isZoomed = useState<bool>(false);
|
final isZoomed = useState<bool>(false);
|
||||||
|
final showAppBar = useState<bool>(true);
|
||||||
final indexOfAsset = useState(assetList.indexOf(asset));
|
final indexOfAsset = useState(assetList.indexOf(asset));
|
||||||
final isPlayingMotionVideo = useState(false);
|
final isPlayingMotionVideo = useState(false);
|
||||||
late Offset localPosition;
|
late Offset localPosition;
|
||||||
@@ -79,41 +80,46 @@ class GalleryViewerPage extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Thumbnail image of a remote asset. Required asset.remote != null
|
/// Thumbnail image of a remote asset. Required asset.isRemote
|
||||||
ImageProvider remoteThumbnailImageProvider(Asset asset, api.ThumbnailFormat type) {
|
ImageProvider remoteThumbnailImageProvider(
|
||||||
|
Asset asset,
|
||||||
|
api.ThumbnailFormat type,
|
||||||
|
) {
|
||||||
return CachedNetworkImageProvider(
|
return CachedNetworkImageProvider(
|
||||||
getThumbnailUrl(
|
getThumbnailUrl(
|
||||||
asset.remote!,
|
asset,
|
||||||
type: type,
|
type: type,
|
||||||
),
|
),
|
||||||
cacheKey: getThumbnailCacheKey(
|
cacheKey: getThumbnailCacheKey(
|
||||||
asset.remote!,
|
asset,
|
||||||
type: type,
|
type: type,
|
||||||
),
|
),
|
||||||
headers: {"Authorization": authToken},
|
headers: {"Authorization": authToken},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Original (large) image of a remote asset. Required asset.remote != null
|
/// Original (large) image of a remote asset. Required asset.isRemote
|
||||||
ImageProvider originalImageProvider(Asset asset) {
|
ImageProvider originalImageProvider(Asset asset) {
|
||||||
return CachedNetworkImageProvider(
|
return CachedNetworkImageProvider(
|
||||||
getImageUrl(asset.remote!),
|
getImageUrl(asset),
|
||||||
cacheKey: getImageCacheKey(asset.remote!),
|
cacheKey: getImageCacheKey(asset),
|
||||||
headers: {"Authorization": authToken},
|
headers: {"Authorization": authToken},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Thumbnail image of a local asset. Required asset.local != null
|
/// Thumbnail image of a local asset. Required asset.isLocal
|
||||||
ImageProvider localThumbnailImageProvider(Asset asset) {
|
ImageProvider localThumbnailImageProvider(Asset asset) {
|
||||||
return AssetEntityImageProvider(
|
return AssetEntityImageProvider(
|
||||||
asset.local!,
|
asset.local!,
|
||||||
isOriginal: false,
|
isOriginal: false,
|
||||||
thumbnailSize: const ThumbnailSize.square(250),
|
thumbnailSize: ThumbnailSize(
|
||||||
|
MediaQuery.of(context).size.width.floor(),
|
||||||
|
MediaQuery.of(context).size.height.floor(),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Original (large) image of a local asset. Required asset.local != null
|
/// Original (large) image of a local asset. Required asset.isLocal
|
||||||
ImageProvider localImageProvider(Asset asset) {
|
ImageProvider localImageProvider(Asset asset) {
|
||||||
return AssetEntityImageProvider(asset.local!);
|
return AssetEntityImageProvider(asset.local!);
|
||||||
}
|
}
|
||||||
@@ -150,13 +156,11 @@ class GalleryViewerPage extends HookConsumerWidget {
|
|||||||
context,
|
context,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void showInfo() {
|
void showInfo() {
|
||||||
if (assetList[indexOfAsset.value].isRemote) {
|
|
||||||
showModalBottomSheet(
|
showModalBottomSheet(
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(15.0),
|
borderRadius: BorderRadius.circular(15.0),
|
||||||
@@ -170,7 +174,6 @@ class GalleryViewerPage extends HookConsumerWidget {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
void handleDelete(Asset deleteAsset) {
|
void handleDelete(Asset deleteAsset) {
|
||||||
showDialog(
|
showDialog(
|
||||||
@@ -224,9 +227,13 @@ class GalleryViewerPage extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return Scaffold(
|
buildAppBar() {
|
||||||
backgroundColor: Colors.black,
|
return AnimatedOpacity(
|
||||||
appBar: TopControlAppBar(
|
duration: const Duration(milliseconds: 100),
|
||||||
|
opacity: (showAppBar.value || !isZoomed.value) ? 1.0 : 0.0,
|
||||||
|
child: Container(
|
||||||
|
color: Colors.black.withOpacity(0.4),
|
||||||
|
child: TopControlAppBar(
|
||||||
isPlayingMotionVideo: isPlayingMotionVideo.value,
|
isPlayingMotionVideo: isPlayingMotionVideo.value,
|
||||||
asset: assetList[indexOfAsset.value],
|
asset: assetList[indexOfAsset.value],
|
||||||
onMoreInfoPressed: () {
|
onMoreInfoPressed: () {
|
||||||
@@ -236,7 +243,7 @@ class GalleryViewerPage extends HookConsumerWidget {
|
|||||||
? null
|
? null
|
||||||
: () {
|
: () {
|
||||||
ref.watch(imageViewerStateProvider.notifier).downloadAsset(
|
ref.watch(imageViewerStateProvider.notifier).downloadAsset(
|
||||||
assetList[indexOfAsset.value].remote!,
|
assetList[indexOfAsset.value],
|
||||||
context,
|
context,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -248,12 +255,24 @@ class GalleryViewerPage extends HookConsumerWidget {
|
|||||||
onToggleMotionVideo: (() {
|
onToggleMotionVideo: (() {
|
||||||
isPlayingMotionVideo.value = !isPlayingMotionVideo.value;
|
isPlayingMotionVideo.value = !isPlayingMotionVideo.value;
|
||||||
}),
|
}),
|
||||||
onDeletePressed: () => handleDelete((assetList[indexOfAsset.value])),
|
onDeletePressed: () =>
|
||||||
onAddToAlbumPressed: () => addToAlbum(assetList[indexOfAsset.value]),
|
handleDelete((assetList[indexOfAsset.value])),
|
||||||
|
onAddToAlbumPressed: () =>
|
||||||
|
addToAlbum(assetList[indexOfAsset.value]),
|
||||||
),
|
),
|
||||||
body: SafeArea(
|
),
|
||||||
child: PhotoViewGallery.builder(
|
);
|
||||||
scaleStateChangedCallback: (state) => isZoomed.value = state != PhotoViewScaleState.initial,
|
}
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
backgroundColor: Colors.black,
|
||||||
|
body: Stack(
|
||||||
|
children: [
|
||||||
|
PhotoViewGallery.builder(
|
||||||
|
scaleStateChangedCallback: (state) {
|
||||||
|
isZoomed.value = state != PhotoViewScaleState.initial;
|
||||||
|
showAppBar.value = !isZoomed.value;
|
||||||
|
},
|
||||||
pageController: controller,
|
pageController: controller,
|
||||||
scrollPhysics: isZoomed.value
|
scrollPhysics: isZoomed.value
|
||||||
? const NeverScrollableScrollPhysics() // Don't allow paging while scrolled in
|
? const NeverScrollableScrollPhysics() // Don't allow paging while scrolled in
|
||||||
@@ -275,24 +294,35 @@ class GalleryViewerPage extends HookConsumerWidget {
|
|||||||
indexOfAsset.value = value;
|
indexOfAsset.value = value;
|
||||||
HapticFeedback.selectionClick();
|
HapticFeedback.selectionClick();
|
||||||
},
|
},
|
||||||
loadingBuilder: isLoadPreview.value ? (context, event) {
|
loadingBuilder: isLoadPreview.value
|
||||||
|
? (context, event) {
|
||||||
final asset = assetList[indexOfAsset.value];
|
final asset = assetList[indexOfAsset.value];
|
||||||
if (!asset.isLocal) {
|
if (!asset.isLocal) {
|
||||||
// Use the WEBP Thumbnail as a placeholder for the JPEG thumbnail to acheive
|
// Use the WEBP Thumbnail as a placeholder for the JPEG thumbnail to acheive
|
||||||
// Three-Stage Loading (WEBP -> JPEG -> Original)
|
// Three-Stage Loading (WEBP -> JPEG -> Original)
|
||||||
final webPThumbnail = CachedNetworkImage(
|
final webPThumbnail = CachedNetworkImage(
|
||||||
imageUrl: getThumbnailUrl(asset.remote!, type: api.ThumbnailFormat.WEBP),
|
imageUrl: getThumbnailUrl(asset),
|
||||||
cacheKey: getThumbnailCacheKey(asset.remote!, type: api.ThumbnailFormat.WEBP),
|
cacheKey: getThumbnailCacheKey(asset),
|
||||||
httpHeaders: { 'Authorization': authToken },
|
httpHeaders: {'Authorization': authToken},
|
||||||
progressIndicatorBuilder: (_, __, ___) => const Center(child: ImmichLoadingIndicator(),),
|
progressIndicatorBuilder: (_, __, ___) => const Center(
|
||||||
|
child: ImmichLoadingIndicator(),
|
||||||
|
),
|
||||||
|
fadeInDuration: const Duration(milliseconds: 0),
|
||||||
fit: BoxFit.contain,
|
fit: BoxFit.contain,
|
||||||
);
|
);
|
||||||
|
|
||||||
return CachedNetworkImage(
|
return CachedNetworkImage(
|
||||||
imageUrl: getThumbnailUrl(asset.remote!, type: api.ThumbnailFormat.JPEG),
|
imageUrl: getThumbnailUrl(
|
||||||
cacheKey: getThumbnailCacheKey(asset.remote!, type: api.ThumbnailFormat.JPEG),
|
asset,
|
||||||
httpHeaders: { 'Authorization': authToken },
|
type: api.ThumbnailFormat.JPEG,
|
||||||
|
),
|
||||||
|
cacheKey: getThumbnailCacheKey(
|
||||||
|
asset,
|
||||||
|
type: api.ThumbnailFormat.JPEG,
|
||||||
|
),
|
||||||
|
httpHeaders: {'Authorization': authToken},
|
||||||
fit: BoxFit.contain,
|
fit: BoxFit.contain,
|
||||||
|
fadeInDuration: const Duration(milliseconds: 0),
|
||||||
placeholder: (_, __) => webPThumbnail,
|
placeholder: (_, __) => webPThumbnail,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
@@ -301,7 +331,8 @@ class GalleryViewerPage extends HookConsumerWidget {
|
|||||||
fit: BoxFit.contain,
|
fit: BoxFit.contain,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} : null,
|
}
|
||||||
|
: null,
|
||||||
builder: (context, index) {
|
builder: (context, index) {
|
||||||
getAssetExif();
|
getAssetExif();
|
||||||
if (assetList[index].isImage && !isPlayingMotionVideo.value) {
|
if (assetList[index].isImage && !isPlayingMotionVideo.value) {
|
||||||
@@ -320,17 +351,28 @@ class GalleryViewerPage extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return PhotoViewGalleryPageOptions(
|
return PhotoViewGalleryPageOptions(
|
||||||
onDragStart: (_, details, __) => localPosition = details.localPosition,
|
onDragStart: (_, details, __) =>
|
||||||
|
localPosition = details.localPosition,
|
||||||
onDragUpdate: (_, details, __) => handleSwipeUpDown(details),
|
onDragUpdate: (_, details, __) => handleSwipeUpDown(details),
|
||||||
|
onTapDown: (_, __, ___) =>
|
||||||
|
showAppBar.value = !showAppBar.value,
|
||||||
imageProvider: provider,
|
imageProvider: provider,
|
||||||
heroAttributes: PhotoViewHeroAttributes(tag: assetList[index].id),
|
heroAttributes:
|
||||||
|
PhotoViewHeroAttributes(tag: assetList[index].id),
|
||||||
minScale: PhotoViewComputedScale.contained,
|
minScale: PhotoViewComputedScale.contained,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return PhotoViewGalleryPageOptions.customChild(
|
return PhotoViewGalleryPageOptions.customChild(
|
||||||
onDragStart: (_, details, __) => localPosition = details.localPosition,
|
onDragStart: (_, details, __) =>
|
||||||
|
localPosition = details.localPosition,
|
||||||
onDragUpdate: (_, details, __) => handleSwipeUpDown(details),
|
onDragUpdate: (_, details, __) => handleSwipeUpDown(details),
|
||||||
heroAttributes: PhotoViewHeroAttributes(tag: assetList[index].id),
|
onTapDown: (_, __, ___) =>
|
||||||
|
showAppBar.value = !showAppBar.value,
|
||||||
|
heroAttributes:
|
||||||
|
PhotoViewHeroAttributes(tag: assetList[index].id),
|
||||||
|
maxScale: 1.0,
|
||||||
|
minScale: 1.0,
|
||||||
|
child: SafeArea(
|
||||||
child: VideoViewerPage(
|
child: VideoViewerPage(
|
||||||
asset: assetList[index],
|
asset: assetList[index],
|
||||||
isMotionVideo: isPlayingMotionVideo.value,
|
isMotionVideo: isPlayingMotionVideo.value,
|
||||||
@@ -340,12 +382,19 @@ class GalleryViewerPage extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
Positioned(
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
child: buildAppBar(),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -53,8 +53,8 @@ class VideoViewerPage extends HookConsumerWidget {
|
|||||||
final box = Hive.box(userInfoBox);
|
final box = Hive.box(userInfoBox);
|
||||||
final String jwtToken = box.get(accessTokenKey);
|
final String jwtToken = box.get(accessTokenKey);
|
||||||
final String videoUrl = isMotionVideo
|
final String videoUrl = isMotionVideo
|
||||||
? '${box.get(serverEndpointKey)}/asset/file/${asset.remote?.livePhotoVideoId!}'
|
? '${box.get(serverEndpointKey)}/asset/file/${asset.livePhotoVideoId}'
|
||||||
: '${box.get(serverEndpointKey)}/asset/file/${asset.id}';
|
: '${box.get(serverEndpointKey)}/asset/file/${asset.remoteId}';
|
||||||
|
|
||||||
return Stack(
|
return Stack(
|
||||||
children: [
|
children: [
|
||||||
|
|||||||
@@ -75,6 +75,9 @@ class BackupService {
|
|||||||
final filter = FilterOptionGroup(
|
final filter = FilterOptionGroup(
|
||||||
containsPathModified: true,
|
containsPathModified: true,
|
||||||
orders: [const OrderOption(type: OrderOptionType.updateDate)],
|
orders: [const OrderOption(type: OrderOptionType.updateDate)],
|
||||||
|
// title is needed to create Assets
|
||||||
|
imageOption: const FilterOption(needTitle: true),
|
||||||
|
videoOption: const FilterOption(needTitle: true),
|
||||||
);
|
);
|
||||||
final now = DateTime.now();
|
final now = DateTime.now();
|
||||||
final List<AssetPathEntity?> selectedAlbums =
|
final List<AssetPathEntity?> selectedAlbums =
|
||||||
|
|||||||
@@ -1,76 +0,0 @@
|
|||||||
import 'package:flutter/foundation.dart';
|
|
||||||
import 'package:openapi/api.dart';
|
|
||||||
|
|
||||||
class ImmichAssetGroupByDate {
|
|
||||||
final String date;
|
|
||||||
List<AssetResponseDto> assets;
|
|
||||||
ImmichAssetGroupByDate({
|
|
||||||
required this.date,
|
|
||||||
required this.assets,
|
|
||||||
});
|
|
||||||
|
|
||||||
ImmichAssetGroupByDate copyWith({
|
|
||||||
String? date,
|
|
||||||
List<AssetResponseDto>? assets,
|
|
||||||
}) {
|
|
||||||
return ImmichAssetGroupByDate(
|
|
||||||
date: date ?? this.date,
|
|
||||||
assets: assets ?? this.assets,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
String toString() => 'ImmichAssetGroupByDate(date: $date, assets: $assets)';
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool operator ==(Object other) {
|
|
||||||
if (identical(this, other)) return true;
|
|
||||||
|
|
||||||
return other is ImmichAssetGroupByDate &&
|
|
||||||
other.date == date &&
|
|
||||||
listEquals(other.assets, assets);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
int get hashCode => date.hashCode ^ assets.hashCode;
|
|
||||||
}
|
|
||||||
|
|
||||||
class GetAllAssetResponse {
|
|
||||||
final int count;
|
|
||||||
final List<ImmichAssetGroupByDate> data;
|
|
||||||
final String nextPageKey;
|
|
||||||
GetAllAssetResponse({
|
|
||||||
required this.count,
|
|
||||||
required this.data,
|
|
||||||
required this.nextPageKey,
|
|
||||||
});
|
|
||||||
|
|
||||||
GetAllAssetResponse copyWith({
|
|
||||||
int? count,
|
|
||||||
List<ImmichAssetGroupByDate>? data,
|
|
||||||
String? nextPageKey,
|
|
||||||
}) {
|
|
||||||
return GetAllAssetResponse(
|
|
||||||
count: count ?? this.count,
|
|
||||||
data: data ?? this.data,
|
|
||||||
nextPageKey: nextPageKey ?? this.nextPageKey,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
String toString() =>
|
|
||||||
'GetAllAssetResponse(count: $count, data: $data, nextPageKey: $nextPageKey)';
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool operator ==(Object other) {
|
|
||||||
if (identical(this, other)) return true;
|
|
||||||
|
|
||||||
return other is GetAllAssetResponse &&
|
|
||||||
other.count == count &&
|
|
||||||
listEquals(other.data, data) &&
|
|
||||||
other.nextPageKey == nextPageKey;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
int get hashCode => count.hashCode ^ data.hashCode ^ nextPageKey.hashCode;
|
|
||||||
}
|
|
||||||
@@ -24,7 +24,6 @@ class ImmichAssetGridState extends State<ImmichAssetGrid> {
|
|||||||
bool _scrolling = false;
|
bool _scrolling = false;
|
||||||
final Set<String> _selectedAssets = HashSet();
|
final Set<String> _selectedAssets = HashSet();
|
||||||
|
|
||||||
|
|
||||||
Set<Asset> _getSelectedAssets() {
|
Set<Asset> _getSelectedAssets() {
|
||||||
return _selectedAssets
|
return _selectedAssets
|
||||||
.map((e) => widget.allAssets.firstWhereOrNull((a) => a.id == e))
|
.map((e) => widget.allAssets.firstWhereOrNull((a) => a.id == e))
|
||||||
@@ -103,7 +102,7 @@ class ImmichAssetGridState extends State<ImmichAssetGrid> {
|
|||||||
return Row(
|
return Row(
|
||||||
key: Key("asset-row-${row.assets.first.id}"),
|
key: Key("asset-row-${row.assets.first.id}"),
|
||||||
children: row.assets.map((Asset asset) {
|
children: row.assets.map((Asset asset) {
|
||||||
bool last = asset == row.assets.last;
|
bool last = asset.id == row.assets.last.id;
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
key: Key("asset-${asset.id}"),
|
key: Key("asset-${asset.id}"),
|
||||||
@@ -224,13 +223,25 @@ class ImmichAssetGridState extends State<ImmichAssetGrid> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<bool> onWillPop() async {
|
||||||
|
if (widget.selectionActive && _selectedAssets.isNotEmpty) {
|
||||||
|
_deselectAll();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Stack(
|
return WillPopScope(
|
||||||
|
onWillPop: onWillPop,
|
||||||
|
child: Stack(
|
||||||
children: [
|
children: [
|
||||||
_buildAssetGrid(),
|
_buildAssetGrid(),
|
||||||
if (widget.selectionActive) _buildMultiSelectIndicator(),
|
if (widget.selectionActive) _buildMultiSelectIndicator(),
|
||||||
],
|
],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -200,18 +200,7 @@ class HomePage extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> onWillPop() async {
|
return SafeArea(
|
||||||
if (multiselectEnabled.state) {
|
|
||||||
selectionEnabledHook.value = false;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return WillPopScope(
|
|
||||||
onWillPop: onWillPop,
|
|
||||||
child: SafeArea(
|
|
||||||
bottom: !multiselectEnabled.state,
|
bottom: !multiselectEnabled.state,
|
||||||
top: true,
|
top: true,
|
||||||
child: Stack(
|
child: Stack(
|
||||||
@@ -240,7 +229,6 @@ class HomePage extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ 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/album/services/album_cache.service.dart';
|
import 'package:immich_mobile/modules/album/services/album_cache.service.dart';
|
||||||
import 'package:immich_mobile/modules/home/services/asset_cache.service.dart';
|
import 'package:immich_mobile/shared/services/asset_cache.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/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/backup/services/backup.service.dart';
|
import 'package:immich_mobile/modules/backup/services/backup.service.dart';
|
||||||
@@ -166,6 +166,7 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
|
|||||||
var deviceInfo = await _deviceInfoService.getDeviceInfo();
|
var deviceInfo = await _deviceInfoService.getDeviceInfo();
|
||||||
userInfoHiveBox.put(deviceIdKey, deviceInfo["deviceId"]);
|
userInfoHiveBox.put(deviceIdKey, deviceInfo["deviceId"]);
|
||||||
userInfoHiveBox.put(accessTokenKey, accessToken);
|
userInfoHiveBox.put(accessTokenKey, accessToken);
|
||||||
|
userInfoHiveBox.put(userIdKey, userResponseDto.id);
|
||||||
|
|
||||||
state = state.copyWith(
|
state = state.copyWith(
|
||||||
isAuthenticated: true,
|
isAuthenticated: true,
|
||||||
|
|||||||
@@ -45,9 +45,11 @@ 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<Asset>.from(
|
searchResult: List.from(
|
||||||
map['searchResult']
|
map['searchResult']
|
||||||
?.map((x) => Asset.remote(AssetResponseDto.fromJson(x))),
|
.map(AssetResponseDto.fromJson)
|
||||||
|
.where((e) => e != null)
|
||||||
|
.map(Asset.remote),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,9 +30,7 @@ class SearchResultPageNotifier extends StateNotifier<SearchResultPageState> {
|
|||||||
isSuccess: false,
|
isSuccess: false,
|
||||||
);
|
);
|
||||||
|
|
||||||
List<Asset>? assets = (await _searchService.searchAsset(searchTerm))
|
List<Asset>? assets = await _searchService.searchAsset(searchTerm);
|
||||||
?.map((e) => Asset.remote(e))
|
|
||||||
.toList();
|
|
||||||
|
|
||||||
if (assets != null) {
|
if (assets != null) {
|
||||||
state = state.copyWith(
|
state = state.copyWith(
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
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/asset.dart';
|
||||||
import 'package:immich_mobile/shared/providers/api.provider.dart';
|
import 'package:immich_mobile/shared/providers/api.provider.dart';
|
||||||
import 'package:immich_mobile/shared/services/api.service.dart';
|
import 'package:immich_mobile/shared/services/api.service.dart';
|
||||||
import 'package:openapi/api.dart';
|
import 'package:openapi/api.dart';
|
||||||
@@ -24,10 +25,14 @@ class SearchService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<AssetResponseDto>?> searchAsset(String searchTerm) async {
|
Future<List<Asset>?> searchAsset(String searchTerm) async {
|
||||||
try {
|
try {
|
||||||
return await _apiService.assetApi
|
final List<AssetResponseDto>? results = await _apiService.assetApi
|
||||||
.searchAsset(SearchAssetDto(searchTerm: searchTerm));
|
.searchAsset(SearchAssetDto(searchTerm: searchTerm));
|
||||||
|
if (results == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return results.map((e) => Asset.remote(e)).toList();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint("[ERROR] [searchAsset] ${e.toString()}");
|
debugPrint("[ERROR] [searchAsset] ${e.toString()}");
|
||||||
return null;
|
return null;
|
||||||
@@ -50,7 +55,7 @@ class SearchService {
|
|||||||
return await _apiService.assetApi.getCuratedObjects();
|
return await _apiService.assetApi.getCuratedObjects();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint("Error [getCuratedObjects] ${e.toString()}");
|
debugPrint("Error [getCuratedObjects] ${e.toString()}");
|
||||||
throw [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,63 +1,128 @@
|
|||||||
import 'package:hive/hive.dart';
|
import 'package:hive/hive.dart';
|
||||||
import 'package:immich_mobile/constants/hive_box.dart';
|
import 'package:immich_mobile/constants/hive_box.dart';
|
||||||
|
import 'package:immich_mobile/shared/models/exif_info.dart';
|
||||||
import 'package:openapi/api.dart';
|
import 'package:openapi/api.dart';
|
||||||
import 'package:photo_manager/photo_manager.dart';
|
import 'package:photo_manager/photo_manager.dart';
|
||||||
|
import 'package:immich_mobile/utils/builtin_extensions.dart';
|
||||||
|
import 'package:path/path.dart' as p;
|
||||||
|
|
||||||
/// Asset (online or local)
|
/// Asset (online or local)
|
||||||
class Asset {
|
class Asset {
|
||||||
Asset.remote(this.remote) {
|
Asset.remote(AssetResponseDto remote)
|
||||||
local = null;
|
: remoteId = remote.id,
|
||||||
}
|
createdAt = DateTime.parse(remote.createdAt),
|
||||||
|
modifiedAt = DateTime.parse(remote.modifiedAt),
|
||||||
|
durationInSeconds = remote.duration.toDuration().inSeconds,
|
||||||
|
fileName = p.basename(remote.originalPath),
|
||||||
|
height = remote.exifInfo?.exifImageHeight?.toInt(),
|
||||||
|
width = remote.exifInfo?.exifImageWidth?.toInt(),
|
||||||
|
livePhotoVideoId = remote.livePhotoVideoId,
|
||||||
|
deviceAssetId = remote.deviceAssetId,
|
||||||
|
deviceId = remote.deviceId,
|
||||||
|
ownerId = remote.ownerId,
|
||||||
|
latitude = remote.exifInfo?.latitude?.toDouble(),
|
||||||
|
longitude = remote.exifInfo?.longitude?.toDouble(),
|
||||||
|
exifInfo =
|
||||||
|
remote.exifInfo != null ? ExifInfo.fromDto(remote.exifInfo!) : null;
|
||||||
|
|
||||||
Asset.local(this.local) {
|
Asset.local(AssetEntity local, String owner)
|
||||||
remote = null;
|
: localId = local.id,
|
||||||
}
|
latitude = local.latitude,
|
||||||
|
longitude = local.longitude,
|
||||||
late final AssetResponseDto? remote;
|
durationInSeconds = local.duration,
|
||||||
late final AssetEntity? local;
|
height = local.height,
|
||||||
|
width = local.width,
|
||||||
bool get isRemote => remote != null;
|
fileName = local.title!,
|
||||||
bool get isLocal => local != null;
|
deviceAssetId = local.id,
|
||||||
|
deviceId = Hive.box(userInfoBox).get(deviceIdKey),
|
||||||
String get deviceId =>
|
ownerId = owner,
|
||||||
isRemote ? remote!.deviceId : Hive.box(userInfoBox).get(deviceIdKey);
|
modifiedAt = local.modifiedDateTime.toUtc(),
|
||||||
|
createdAt = local.createDateTime.toUtc() {
|
||||||
String get deviceAssetId => isRemote ? remote!.deviceAssetId : local!.id;
|
if (createdAt.year == 1970) {
|
||||||
|
createdAt = modifiedAt;
|
||||||
String get id => isLocal ? local!.id : remote!.id;
|
|
||||||
|
|
||||||
double? get latitude =>
|
|
||||||
isLocal ? local!.latitude : remote!.exifInfo?.latitude?.toDouble();
|
|
||||||
|
|
||||||
double? get longitude =>
|
|
||||||
isLocal ? local!.longitude : remote!.exifInfo?.longitude?.toDouble();
|
|
||||||
|
|
||||||
DateTime get createdAt {
|
|
||||||
if (isLocal) {
|
|
||||||
if (local!.createDateTime.year == 1970) {
|
|
||||||
return local!.modifiedDateTime;
|
|
||||||
}
|
|
||||||
return local!.createDateTime;
|
|
||||||
} else {
|
|
||||||
return DateTime.parse(remote!.createdAt);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool get isImage => isLocal
|
Asset({
|
||||||
? local!.type == AssetType.image
|
this.localId,
|
||||||
: remote!.type == AssetTypeEnum.IMAGE;
|
this.remoteId,
|
||||||
|
required this.deviceAssetId,
|
||||||
|
required this.deviceId,
|
||||||
|
required this.ownerId,
|
||||||
|
required this.createdAt,
|
||||||
|
required this.modifiedAt,
|
||||||
|
this.latitude,
|
||||||
|
this.longitude,
|
||||||
|
required this.durationInSeconds,
|
||||||
|
this.width,
|
||||||
|
this.height,
|
||||||
|
required this.fileName,
|
||||||
|
this.livePhotoVideoId,
|
||||||
|
this.exifInfo,
|
||||||
|
});
|
||||||
|
|
||||||
String get duration => isRemote
|
AssetEntity? _local;
|
||||||
? remote!.duration
|
|
||||||
: Duration(seconds: local!.duration).toString();
|
|
||||||
|
|
||||||
/// use only for tests
|
AssetEntity? get local {
|
||||||
set createdAt(DateTime val) {
|
if (isLocal && _local == null) {
|
||||||
if (isRemote) {
|
_local = AssetEntity(
|
||||||
remote!.createdAt = val.toIso8601String();
|
id: localId!.toString(),
|
||||||
|
typeInt: isImage ? 1 : 2,
|
||||||
|
width: width!,
|
||||||
|
height: height!,
|
||||||
|
duration: durationInSeconds,
|
||||||
|
createDateSecond: createdAt.millisecondsSinceEpoch ~/ 1000,
|
||||||
|
latitude: latitude,
|
||||||
|
longitude: longitude,
|
||||||
|
modifiedDateSecond: modifiedAt.millisecondsSinceEpoch ~/ 1000,
|
||||||
|
title: fileName,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
return _local;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String? localId;
|
||||||
|
|
||||||
|
String? remoteId;
|
||||||
|
|
||||||
|
String deviceAssetId;
|
||||||
|
|
||||||
|
String deviceId;
|
||||||
|
|
||||||
|
String ownerId;
|
||||||
|
|
||||||
|
DateTime createdAt;
|
||||||
|
|
||||||
|
DateTime modifiedAt;
|
||||||
|
|
||||||
|
double? latitude;
|
||||||
|
|
||||||
|
double? longitude;
|
||||||
|
|
||||||
|
int durationInSeconds;
|
||||||
|
|
||||||
|
int? width;
|
||||||
|
|
||||||
|
int? height;
|
||||||
|
|
||||||
|
String fileName;
|
||||||
|
|
||||||
|
String? livePhotoVideoId;
|
||||||
|
|
||||||
|
ExifInfo? exifInfo;
|
||||||
|
|
||||||
|
String get id => isLocal ? localId.toString() : remoteId!;
|
||||||
|
|
||||||
|
String get name => p.withoutExtension(fileName);
|
||||||
|
|
||||||
|
bool get isRemote => remoteId != null;
|
||||||
|
|
||||||
|
bool get isLocal => localId != null;
|
||||||
|
|
||||||
|
bool get isImage => durationInSeconds == 0;
|
||||||
|
|
||||||
|
Duration get duration => Duration(seconds: durationInSeconds);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(other) {
|
bool operator ==(other) {
|
||||||
if (other is! Asset) return false;
|
if (other is! Asset) return false;
|
||||||
@@ -67,12 +132,26 @@ class Asset {
|
|||||||
@override
|
@override
|
||||||
int get hashCode => id.hashCode;
|
int get hashCode => id.hashCode;
|
||||||
|
|
||||||
|
// methods below are only required for caching as JSON
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
final json = <String, dynamic>{};
|
final json = <String, dynamic>{};
|
||||||
if (isLocal) {
|
json["localId"] = localId;
|
||||||
json["local"] = _assetEntityToJson(local!);
|
json["remoteId"] = remoteId;
|
||||||
} else {
|
json["deviceAssetId"] = deviceAssetId;
|
||||||
json["remote"] = remote!.toJson();
|
json["deviceId"] = deviceId;
|
||||||
|
json["ownerId"] = ownerId;
|
||||||
|
json["createdAt"] = createdAt.millisecondsSinceEpoch;
|
||||||
|
json["modifiedAt"] = modifiedAt.millisecondsSinceEpoch;
|
||||||
|
json["latitude"] = latitude;
|
||||||
|
json["longitude"] = longitude;
|
||||||
|
json["durationInSeconds"] = durationInSeconds;
|
||||||
|
json["width"] = width;
|
||||||
|
json["height"] = height;
|
||||||
|
json["fileName"] = fileName;
|
||||||
|
json["livePhotoVideoId"] = livePhotoVideoId;
|
||||||
|
if (exifInfo != null) {
|
||||||
|
json["exifInfo"] = exifInfo!.toJson();
|
||||||
}
|
}
|
||||||
return json;
|
return json;
|
||||||
}
|
}
|
||||||
@@ -80,55 +159,28 @@ class Asset {
|
|||||||
static Asset? fromJson(dynamic value) {
|
static Asset? fromJson(dynamic value) {
|
||||||
if (value is Map) {
|
if (value is Map) {
|
||||||
final json = value.cast<String, dynamic>();
|
final json = value.cast<String, dynamic>();
|
||||||
final l = json["local"];
|
return Asset(
|
||||||
if (l != null) {
|
localId: json["localId"],
|
||||||
return Asset.local(_assetEntityFromJson(l));
|
remoteId: json["remoteId"],
|
||||||
} else {
|
deviceAssetId: json["deviceAssetId"],
|
||||||
return Asset.remote(AssetResponseDto.fromJson(json["remote"]));
|
deviceId: json["deviceId"],
|
||||||
}
|
ownerId: json["ownerId"],
|
||||||
}
|
createdAt:
|
||||||
return null;
|
DateTime.fromMillisecondsSinceEpoch(json["createdAt"], isUtc: true),
|
||||||
}
|
modifiedAt: DateTime.fromMillisecondsSinceEpoch(
|
||||||
}
|
json["modifiedAt"],
|
||||||
|
isUtc: true,
|
||||||
Map<String, dynamic> _assetEntityToJson(AssetEntity a) {
|
),
|
||||||
final json = <String, dynamic>{};
|
|
||||||
json["id"] = a.id;
|
|
||||||
json["typeInt"] = a.typeInt;
|
|
||||||
json["width"] = a.width;
|
|
||||||
json["height"] = a.height;
|
|
||||||
json["duration"] = a.duration;
|
|
||||||
json["orientation"] = a.orientation;
|
|
||||||
json["isFavorite"] = a.isFavorite;
|
|
||||||
json["title"] = a.title;
|
|
||||||
json["createDateSecond"] = a.createDateSecond;
|
|
||||||
json["modifiedDateSecond"] = a.modifiedDateSecond;
|
|
||||||
json["latitude"] = a.latitude;
|
|
||||||
json["longitude"] = a.longitude;
|
|
||||||
json["mimeType"] = a.mimeType;
|
|
||||||
json["subtype"] = a.subtype;
|
|
||||||
return json;
|
|
||||||
}
|
|
||||||
|
|
||||||
AssetEntity? _assetEntityFromJson(dynamic value) {
|
|
||||||
if (value is Map) {
|
|
||||||
final json = value.cast<String, dynamic>();
|
|
||||||
return AssetEntity(
|
|
||||||
id: json["id"],
|
|
||||||
typeInt: json["typeInt"],
|
|
||||||
width: json["width"],
|
|
||||||
height: json["height"],
|
|
||||||
duration: json["duration"],
|
|
||||||
orientation: json["orientation"],
|
|
||||||
isFavorite: json["isFavorite"],
|
|
||||||
title: json["title"],
|
|
||||||
createDateSecond: json["createDateSecond"],
|
|
||||||
modifiedDateSecond: json["modifiedDateSecond"],
|
|
||||||
latitude: json["latitude"],
|
latitude: json["latitude"],
|
||||||
longitude: json["longitude"],
|
longitude: json["longitude"],
|
||||||
mimeType: json["mimeType"],
|
durationInSeconds: json["durationInSeconds"],
|
||||||
subtype: json["subtype"],
|
width: json["width"],
|
||||||
|
height: json["height"],
|
||||||
|
fileName: json["fileName"],
|
||||||
|
livePhotoVideoId: json["livePhotoVideoId"],
|
||||||
|
exifInfo: ExifInfo.fromJson(json["exifInfo"]),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,86 @@
|
|||||||
|
import 'package:openapi/api.dart';
|
||||||
|
import 'package:immich_mobile/utils/builtin_extensions.dart';
|
||||||
|
|
||||||
|
class ExifInfo {
|
||||||
|
int? fileSize;
|
||||||
|
String? make;
|
||||||
|
String? model;
|
||||||
|
String? orientation;
|
||||||
|
String? lensModel;
|
||||||
|
double? fNumber;
|
||||||
|
double? focalLength;
|
||||||
|
int? iso;
|
||||||
|
double? exposureTime;
|
||||||
|
String? city;
|
||||||
|
String? state;
|
||||||
|
String? country;
|
||||||
|
|
||||||
|
ExifInfo.fromDto(ExifResponseDto dto)
|
||||||
|
: fileSize = dto.fileSizeInByte,
|
||||||
|
make = dto.make,
|
||||||
|
model = dto.model,
|
||||||
|
orientation = dto.orientation,
|
||||||
|
lensModel = dto.lensModel,
|
||||||
|
fNumber = dto.fNumber?.toDouble(),
|
||||||
|
focalLength = dto.focalLength?.toDouble(),
|
||||||
|
iso = dto.iso?.toInt(),
|
||||||
|
exposureTime = dto.exposureTime?.toDouble(),
|
||||||
|
city = dto.city,
|
||||||
|
state = dto.state,
|
||||||
|
country = dto.country;
|
||||||
|
|
||||||
|
// stuff below is only required for caching as JSON
|
||||||
|
|
||||||
|
ExifInfo(
|
||||||
|
this.fileSize,
|
||||||
|
this.make,
|
||||||
|
this.model,
|
||||||
|
this.orientation,
|
||||||
|
this.lensModel,
|
||||||
|
this.fNumber,
|
||||||
|
this.focalLength,
|
||||||
|
this.iso,
|
||||||
|
this.exposureTime,
|
||||||
|
this.city,
|
||||||
|
this.state,
|
||||||
|
this.country,
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
final json = <String, dynamic>{};
|
||||||
|
json["fileSize"] = fileSize;
|
||||||
|
json["make"] = make;
|
||||||
|
json["model"] = model;
|
||||||
|
json["orientation"] = orientation;
|
||||||
|
json["lensModel"] = lensModel;
|
||||||
|
json["fNumber"] = fNumber;
|
||||||
|
json["focalLength"] = focalLength;
|
||||||
|
json["iso"] = iso;
|
||||||
|
json["exposureTime"] = exposureTime;
|
||||||
|
json["city"] = city;
|
||||||
|
json["state"] = state;
|
||||||
|
json["country"] = country;
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
|
||||||
|
static ExifInfo? fromJson(dynamic value) {
|
||||||
|
if (value is Map) {
|
||||||
|
final json = value.cast<String, dynamic>();
|
||||||
|
return ExifInfo(
|
||||||
|
json["fileSize"],
|
||||||
|
json["make"],
|
||||||
|
json["model"],
|
||||||
|
json["orientation"],
|
||||||
|
json["lensModel"],
|
||||||
|
json["fNumber"],
|
||||||
|
json["focalLength"],
|
||||||
|
json["iso"],
|
||||||
|
json["exposureTime"],
|
||||||
|
json["city"],
|
||||||
|
json["state"],
|
||||||
|
json["country"],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,8 +4,8 @@ import 'package:flutter/foundation.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/home/services/asset.service.dart';
|
import 'package:immich_mobile/shared/services/asset.service.dart';
|
||||||
import 'package:immich_mobile/modules/home/services/asset_cache.service.dart';
|
import 'package:immich_mobile/shared/services/asset_cache.service.dart';
|
||||||
import 'package:immich_mobile/modules/home/ui/asset_grid/asset_grid_data_structure.dart';
|
import 'package:immich_mobile/modules/home/ui/asset_grid/asset_grid_data_structure.dart';
|
||||||
import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart';
|
import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart';
|
||||||
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
|
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
|
||||||
@@ -36,7 +36,7 @@ class AssetsState {
|
|||||||
return AssetsState([...allAssets, ...toAdd]);
|
return AssetsState([...allAssets, ...toAdd]);
|
||||||
}
|
}
|
||||||
|
|
||||||
_groupByDate() async {
|
Future<Map<String, List<Asset>>> _groupByDate() async {
|
||||||
sortCompare(List<Asset> assets) {
|
sortCompare(List<Asset> assets) {
|
||||||
assets.sortByCompare<DateTime>(
|
assets.sortByCompare<DateTime>(
|
||||||
(e) => e.createdAt,
|
(e) => e.createdAt,
|
||||||
@@ -50,11 +50,11 @@ class AssetsState {
|
|||||||
return await compute(sortCompare, allAssets.toList());
|
return await compute(sortCompare, allAssets.toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
static fromAssetList(List<Asset> assets) {
|
static AssetsState fromAssetList(List<Asset> assets) {
|
||||||
return AssetsState(assets);
|
return AssetsState(assets);
|
||||||
}
|
}
|
||||||
|
|
||||||
static empty() {
|
static AssetsState empty() {
|
||||||
return AssetsState([]);
|
return AssetsState([]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -82,7 +82,10 @@ class AssetNotifier extends StateNotifier<AssetsState> {
|
|||||||
this._settingsService,
|
this._settingsService,
|
||||||
) : super(AssetsState.fromAssetList([]));
|
) : super(AssetsState.fromAssetList([]));
|
||||||
|
|
||||||
_updateAssetsState(List<Asset> newAssetList, {bool cache = true}) async {
|
Future<void> _updateAssetsState(
|
||||||
|
List<Asset> newAssetList, {
|
||||||
|
bool cache = true,
|
||||||
|
}) async {
|
||||||
if (cache) {
|
if (cache) {
|
||||||
_assetCacheService.put(newAssetList);
|
_assetCacheService.put(newAssetList);
|
||||||
}
|
}
|
||||||
@@ -101,20 +104,26 @@ class AssetNotifier extends StateNotifier<AssetsState> {
|
|||||||
final stopwatch = Stopwatch();
|
final stopwatch = Stopwatch();
|
||||||
try {
|
try {
|
||||||
_getAllAssetInProgress = true;
|
_getAllAssetInProgress = true;
|
||||||
final bool isCacheValid = await _assetCacheService.isValid();
|
bool isCacheValid = await _assetCacheService.isValid();
|
||||||
stopwatch.start();
|
stopwatch.start();
|
||||||
final Box box = Hive.box(userInfoBox);
|
final Box box = Hive.box(userInfoBox);
|
||||||
|
if (isCacheValid && state.allAssets.isEmpty) {
|
||||||
|
final List<Asset>? cachedData = await _assetCacheService.get();
|
||||||
|
if (cachedData == null) {
|
||||||
|
isCacheValid = false;
|
||||||
|
log.warning("Cached asset data is invalid, fetching new data");
|
||||||
|
} else {
|
||||||
|
await _updateAssetsState(cachedData, cache: false);
|
||||||
|
log.info(
|
||||||
|
"Reading assets ${state.allAssets.length} from cache: ${stopwatch.elapsedMilliseconds}ms",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
stopwatch.reset();
|
||||||
|
}
|
||||||
final localTask = _assetService.getLocalAssets(urgent: !isCacheValid);
|
final localTask = _assetService.getLocalAssets(urgent: !isCacheValid);
|
||||||
final remoteTask = _assetService.getRemoteAssets(
|
final remoteTask = _assetService.getRemoteAssets(
|
||||||
etag: isCacheValid ? box.get(assetEtagKey) : null,
|
etag: isCacheValid ? box.get(assetEtagKey) : null,
|
||||||
);
|
);
|
||||||
if (isCacheValid && state.allAssets.isEmpty) {
|
|
||||||
await _updateAssetsState(await _assetCacheService.get(), cache: false);
|
|
||||||
log.info(
|
|
||||||
"Reading assets ${state.allAssets.length} from cache: ${stopwatch.elapsedMilliseconds}ms",
|
|
||||||
);
|
|
||||||
stopwatch.reset();
|
|
||||||
}
|
|
||||||
|
|
||||||
int remoteBegin = state.allAssets.indexWhere((a) => a.isRemote);
|
int remoteBegin = state.allAssets.indexWhere((a) => a.isRemote);
|
||||||
remoteBegin = remoteBegin == -1 ? state.allAssets.length : remoteBegin;
|
remoteBegin = remoteBegin == -1 ? state.allAssets.length : remoteBegin;
|
||||||
@@ -184,7 +193,7 @@ class AssetNotifier extends StateNotifier<AssetsState> {
|
|||||||
_updateAssetsState([]);
|
_updateAssetsState([]);
|
||||||
}
|
}
|
||||||
|
|
||||||
onNewAssetUploaded(AssetResponseDto newAsset) {
|
void onNewAssetUploaded(Asset newAsset) {
|
||||||
final int i = state.allAssets.indexWhere(
|
final int i = state.allAssets.indexWhere(
|
||||||
(a) =>
|
(a) =>
|
||||||
a.isRemote ||
|
a.isRemote ||
|
||||||
@@ -192,13 +201,13 @@ class AssetNotifier extends StateNotifier<AssetsState> {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (i == -1 || state.allAssets[i].deviceAssetId != newAsset.deviceAssetId) {
|
if (i == -1 || state.allAssets[i].deviceAssetId != newAsset.deviceAssetId) {
|
||||||
_updateAssetsState([...state.allAssets, Asset.remote(newAsset)]);
|
_updateAssetsState([...state.allAssets, newAsset]);
|
||||||
} else {
|
} else {
|
||||||
// order is important to keep all local-only assets at the beginning!
|
// order is important to keep all local-only assets at the beginning!
|
||||||
_updateAssetsState([
|
_updateAssetsState([
|
||||||
...state.allAssets.slice(0, i),
|
...state.allAssets.slice(0, i),
|
||||||
...state.allAssets.slice(i + 1),
|
...state.allAssets.slice(i + 1),
|
||||||
Asset.remote(newAsset),
|
newAsset,
|
||||||
]);
|
]);
|
||||||
// TODO here is a place to unify local/remote assets by replacing the
|
// TODO here is a place to unify local/remote assets by replacing the
|
||||||
// local-only asset in the state with a local&remote asset
|
// local-only asset in the state with a local&remote asset
|
||||||
@@ -230,7 +239,7 @@ class AssetNotifier extends StateNotifier<AssetsState> {
|
|||||||
// Delete asset from device
|
// Delete asset from device
|
||||||
for (final Asset asset in assetsToDelete) {
|
for (final Asset asset in assetsToDelete) {
|
||||||
if (asset.isLocal) {
|
if (asset.isLocal) {
|
||||||
local.add(asset.id);
|
local.add(asset.localId!);
|
||||||
} else if (asset.deviceId == deviceId) {
|
} else if (asset.deviceId == deviceId) {
|
||||||
// Delete asset on device if it is still present
|
// Delete asset on device if it is still present
|
||||||
var localAsset = await AssetEntity.fromId(asset.deviceAssetId);
|
var localAsset = await AssetEntity.fromId(asset.deviceAssetId);
|
||||||
@@ -252,8 +261,7 @@ class AssetNotifier extends StateNotifier<AssetsState> {
|
|||||||
Future<Iterable<String>> _deleteRemoteAssets(
|
Future<Iterable<String>> _deleteRemoteAssets(
|
||||||
Set<Asset> assetsToDelete,
|
Set<Asset> assetsToDelete,
|
||||||
) async {
|
) async {
|
||||||
final Iterable<AssetResponseDto> remote =
|
final Iterable<Asset> remote = assetsToDelete.where((e) => e.isRemote);
|
||||||
assetsToDelete.where((e) => e.isRemote).map((e) => e.remote!);
|
|
||||||
final List<DeleteAssetResponseDto> deleteAssetResult =
|
final List<DeleteAssetResponseDto> deleteAssetResult =
|
||||||
await _assetService.deleteAssets(remote) ?? [];
|
await _assetService.deleteAssets(remote) ?? [];
|
||||||
return deleteAssetResult
|
return deleteAssetResult
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ 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/asset.dart';
|
||||||
import 'package:immich_mobile/shared/providers/asset.provider.dart';
|
import 'package:immich_mobile/shared/providers/asset.provider.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
import 'package:openapi/api.dart';
|
import 'package:openapi/api.dart';
|
||||||
@@ -91,14 +92,7 @@ class WebsocketNotifier extends StateNotifier<WebsocketState> {
|
|||||||
state = WebsocketState(isConnected: false, socket: null);
|
state = WebsocketState(isConnected: false, socket: null);
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on('on_upload_success', (data) {
|
socket.on('on_upload_success', _handleOnUploadSuccess);
|
||||||
var jsonString = jsonDecode(data.toString());
|
|
||||||
AssetResponseDto? newAsset = AssetResponseDto.fromJson(jsonString);
|
|
||||||
|
|
||||||
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()}");
|
||||||
}
|
}
|
||||||
@@ -122,14 +116,16 @@ class WebsocketNotifier extends StateNotifier<WebsocketState> {
|
|||||||
|
|
||||||
listenUploadEvent() {
|
listenUploadEvent() {
|
||||||
debugPrint("Start listening to event on_upload_success");
|
debugPrint("Start listening to event on_upload_success");
|
||||||
state.socket?.on('on_upload_success', (data) {
|
state.socket?.on('on_upload_success', _handleOnUploadSuccess);
|
||||||
var jsonString = jsonDecode(data.toString());
|
}
|
||||||
AssetResponseDto? newAsset = AssetResponseDto.fromJson(jsonString);
|
|
||||||
|
|
||||||
if (newAsset != null) {
|
_handleOnUploadSuccess(dynamic data) {
|
||||||
|
final jsonString = jsonDecode(data.toString());
|
||||||
|
final dto = AssetResponseDto.fromJson(jsonString);
|
||||||
|
if (dto != null) {
|
||||||
|
final newAsset = Asset.remote(dto);
|
||||||
ref.watch(assetProvider.notifier).onNewAssetUploaded(newAsset);
|
ref.watch(assetProvider.notifier).onNewAssetUploaded(newAsset);
|
||||||
}
|
}
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+9
-5
@@ -62,10 +62,11 @@ class AssetService {
|
|||||||
}
|
}
|
||||||
final box = await Hive.openBox<HiveBackupAlbums>(hiveBackupInfoBox);
|
final box = await Hive.openBox<HiveBackupAlbums>(hiveBackupInfoBox);
|
||||||
final HiveBackupAlbums? backupAlbumInfo = box.get(backupInfoKey);
|
final HiveBackupAlbums? backupAlbumInfo = box.get(backupInfoKey);
|
||||||
|
final String userId = Hive.box(userInfoBox).get(userIdKey);
|
||||||
if (backupAlbumInfo != null) {
|
if (backupAlbumInfo != null) {
|
||||||
return (await _backupService
|
return (await _backupService
|
||||||
.buildUploadCandidates(backupAlbumInfo.deepCopy()))
|
.buildUploadCandidates(backupAlbumInfo.deepCopy()))
|
||||||
.map(Asset.local)
|
.map((e) => Asset.local(e, userId))
|
||||||
.toList(growable: false);
|
.toList(growable: false);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -76,21 +77,24 @@ class AssetService {
|
|||||||
|
|
||||||
Future<Asset?> getAssetById(String assetId) async {
|
Future<Asset?> getAssetById(String assetId) async {
|
||||||
try {
|
try {
|
||||||
return Asset.remote(await _apiService.assetApi.getAssetById(assetId));
|
final dto = await _apiService.assetApi.getAssetById(assetId);
|
||||||
|
if (dto != null) {
|
||||||
|
return Asset.remote(dto);
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint("Error [getAssetById] ${e.toString()}");
|
debugPrint("Error [getAssetById] ${e.toString()}");
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<DeleteAssetResponseDto>?> deleteAssets(
|
Future<List<DeleteAssetResponseDto>?> deleteAssets(
|
||||||
Iterable<AssetResponseDto> deleteAssets,
|
Iterable<Asset> deleteAssets,
|
||||||
) async {
|
) async {
|
||||||
try {
|
try {
|
||||||
final List<String> payload = [];
|
final List<String> payload = [];
|
||||||
|
|
||||||
for (final asset in deleteAssets) {
|
for (final asset in deleteAssets) {
|
||||||
payload.add(asset.id);
|
payload.add(asset.remoteId!);
|
||||||
}
|
}
|
||||||
|
|
||||||
return await _apiService.assetApi
|
return await _apiService.assetApi
|
||||||
+3
-5
@@ -23,17 +23,15 @@ class AssetCacheService extends JsonCache<List<Asset>> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<List<Asset>> get() async {
|
Future<List<Asset>?> get() async {
|
||||||
try {
|
try {
|
||||||
final mapList = await readRawData() as List<dynamic>;
|
final mapList = await readRawData() as List<dynamic>;
|
||||||
|
|
||||||
final responseData = await compute(_computeEncode, mapList);
|
final responseData = await compute(_computeEncode, mapList);
|
||||||
|
|
||||||
return responseData;
|
return responseData;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint(e.toString());
|
debugPrint(e.toString());
|
||||||
|
await invalidate();
|
||||||
return [];
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -60,5 +60,5 @@ abstract class JsonCache<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void put(T data);
|
void put(T data);
|
||||||
Future<T> get();
|
Future<T?> get();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ 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/asset.dart';
|
import 'package:immich_mobile/shared/models/asset.dart';
|
||||||
import 'package:immich_mobile/shared/providers/api.provider.dart';
|
import 'package:immich_mobile/shared/providers/api.provider.dart';
|
||||||
import 'package:path/path.dart';
|
|
||||||
import 'package:path_provider/path_provider.dart';
|
import 'package:path_provider/path_provider.dart';
|
||||||
import 'package:share_plus/share_plus.dart';
|
import 'package:share_plus/share_plus.dart';
|
||||||
import 'api.service.dart';
|
import 'api.service.dart';
|
||||||
@@ -25,13 +24,10 @@ class ShareService {
|
|||||||
final downloadedXFiles = assets.map<Future<XFile>>((asset) async {
|
final downloadedXFiles = assets.map<Future<XFile>>((asset) async {
|
||||||
if (asset.isRemote) {
|
if (asset.isRemote) {
|
||||||
final tempDir = await getTemporaryDirectory();
|
final tempDir = await getTemporaryDirectory();
|
||||||
final fileName = basename(asset.remote!.originalPath);
|
final fileName = asset.fileName;
|
||||||
final tempFile = await File('${tempDir.path}/$fileName').create();
|
final tempFile = await File('${tempDir.path}/$fileName').create();
|
||||||
final res = await _apiService.assetApi.downloadFileWithHttpInfo(
|
final res = await _apiService.assetApi
|
||||||
asset.remote!.id,
|
.downloadFileWithHttpInfo(asset.remoteId!);
|
||||||
isThumb: false,
|
|
||||||
isWeb: false,
|
|
||||||
);
|
|
||||||
tempFile.writeAsBytesSync(res.bodyBytes);
|
tempFile.writeAsBytesSync(res.bodyBytes);
|
||||||
return XFile(tempFile.path);
|
return XFile(tempFile.path);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import 'package:cached_network_image/cached_network_image.dart';
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.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/shared/models/asset.dart';
|
import 'package:immich_mobile/shared/models/asset.dart';
|
||||||
@@ -15,13 +16,28 @@ class ImmichImage extends StatelessWidget {
|
|||||||
this.useGrayBoxPlaceholder = false,
|
this.useGrayBoxPlaceholder = false,
|
||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
final Asset asset;
|
final Asset? asset;
|
||||||
final bool useGrayBoxPlaceholder;
|
final bool useGrayBoxPlaceholder;
|
||||||
final double width;
|
final double width;
|
||||||
final double height;
|
final double height;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
if (this.asset == null) {
|
||||||
|
return Container(
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
color: Colors.grey,
|
||||||
|
),
|
||||||
|
child: SizedBox(
|
||||||
|
width: width,
|
||||||
|
height: height,
|
||||||
|
child: const Center(
|
||||||
|
child: Icon(Icons.no_photography),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
final Asset asset = this.asset!;
|
||||||
if (asset.isLocal) {
|
if (asset.isLocal) {
|
||||||
return Image(
|
return Image(
|
||||||
image: AssetEntityImageProvider(
|
image: AssetEntityImageProvider(
|
||||||
@@ -49,7 +65,16 @@ class ImmichImage extends StatelessWidget {
|
|||||||
));
|
));
|
||||||
},
|
},
|
||||||
errorBuilder: (context, error, stackTrace) {
|
errorBuilder: (context, error, stackTrace) {
|
||||||
debugPrint("Error getting thumb for assetId=${asset.id}: $error");
|
if (error is PlatformException &&
|
||||||
|
error.code == "The asset not found!") {
|
||||||
|
debugPrint(
|
||||||
|
"Asset ${asset.localId} does not exist anymore on device!",
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
debugPrint(
|
||||||
|
"Error getting thumb for assetId=${asset.localId}: $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,
|
||||||
@@ -57,12 +82,12 @@ class ImmichImage extends StatelessWidget {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
final String token = Hive.box(userInfoBox).get(accessTokenKey);
|
final String? token = Hive.box(userInfoBox).get(accessTokenKey);
|
||||||
final String thumbnailRequestUrl = getThumbnailUrl(asset.remote!);
|
final String thumbnailRequestUrl = getThumbnailUrl(asset);
|
||||||
return CachedNetworkImage(
|
return CachedNetworkImage(
|
||||||
imageUrl: thumbnailRequestUrl,
|
imageUrl: thumbnailRequestUrl,
|
||||||
httpHeaders: {"Authorization": "Bearer $token"},
|
httpHeaders: {"Authorization": "Bearer $token"},
|
||||||
cacheKey: getThumbnailCacheKey(asset.remote!),
|
cacheKey: getThumbnailCacheKey(asset),
|
||||||
width: width,
|
width: width,
|
||||||
height: height,
|
height: height,
|
||||||
// keeping memCacheWidth, memCacheHeight, maxWidthDiskCache and
|
// keeping memCacheWidth, memCacheHeight, maxWidthDiskCache and
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
extension DurationExtension on String {
|
||||||
|
Duration toDuration() {
|
||||||
|
final parts =
|
||||||
|
split(':').map((e) => double.parse(e).toInt()).toList(growable: false);
|
||||||
|
return Duration(hours: parts[0], minutes: parts[1], seconds: parts[2]);
|
||||||
|
}
|
||||||
|
|
||||||
|
double? toDouble() {
|
||||||
|
return double.tryParse(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,17 +1,18 @@
|
|||||||
import 'package:hive/hive.dart';
|
import 'package:hive/hive.dart';
|
||||||
|
import 'package:immich_mobile/shared/models/asset.dart';
|
||||||
import 'package:openapi/api.dart';
|
import 'package:openapi/api.dart';
|
||||||
|
|
||||||
import '../constants/hive_box.dart';
|
import '../constants/hive_box.dart';
|
||||||
|
|
||||||
String getThumbnailUrl(
|
String getThumbnailUrl(
|
||||||
final AssetResponseDto asset, {
|
final Asset asset, {
|
||||||
ThumbnailFormat type = ThumbnailFormat.WEBP,
|
ThumbnailFormat type = ThumbnailFormat.WEBP,
|
||||||
}) {
|
}) {
|
||||||
return _getThumbnailUrl(asset.id, type: type);
|
return _getThumbnailUrl(asset.remoteId!, type: type);
|
||||||
}
|
}
|
||||||
|
|
||||||
String getThumbnailCacheKey(
|
String getThumbnailCacheKey(
|
||||||
final AssetResponseDto asset, {
|
final Asset asset, {
|
||||||
ThumbnailFormat type = ThumbnailFormat.WEBP,
|
ThumbnailFormat type = ThumbnailFormat.WEBP,
|
||||||
}) {
|
}) {
|
||||||
return _getThumbnailCacheKey(asset.id, type);
|
return _getThumbnailCacheKey(asset.id, type);
|
||||||
@@ -45,12 +46,12 @@ String getAlbumThumbNailCacheKey(
|
|||||||
return _getThumbnailCacheKey(album.albumThumbnailAssetId!, type);
|
return _getThumbnailCacheKey(album.albumThumbnailAssetId!, type);
|
||||||
}
|
}
|
||||||
|
|
||||||
String getImageUrl(final AssetResponseDto asset) {
|
String getImageUrl(final Asset asset) {
|
||||||
final box = Hive.box(userInfoBox);
|
final box = Hive.box(userInfoBox);
|
||||||
return '${box.get(serverEndpointKey)}/asset/file/${asset.id}?isThumb=false';
|
return '${box.get(serverEndpointKey)}/asset/file/${asset.remoteId}?isThumb=false';
|
||||||
}
|
}
|
||||||
|
|
||||||
String getImageCacheKey(final AssetResponseDto asset) {
|
String getImageCacheKey(final Asset asset) {
|
||||||
return '${asset.id}_fullStage';
|
return '${asset.id}_fullStage';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Generated
+1
-1
@@ -3,7 +3,7 @@ Immich API
|
|||||||
|
|
||||||
This Dart package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project:
|
This Dart package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project:
|
||||||
|
|
||||||
- API version: 1.43.0
|
- API version: 1.43.1
|
||||||
- Build package: org.openapitools.codegen.languages.DartClientCodegen
|
- Build package: org.openapitools.codegen.languages.DartClientCodegen
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|||||||
Generated
+2
-6
@@ -230,7 +230,7 @@ Name | Type | Description | Notes
|
|||||||
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
|
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
|
||||||
|
|
||||||
# **downloadFile**
|
# **downloadFile**
|
||||||
> Object downloadFile(assetId, isThumb, isWeb)
|
> Object downloadFile(assetId)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -248,11 +248,9 @@ import 'package:openapi/api.dart';
|
|||||||
|
|
||||||
final api_instance = AssetApi();
|
final api_instance = AssetApi();
|
||||||
final assetId = assetId_example; // String |
|
final assetId = assetId_example; // String |
|
||||||
final isThumb = true; // bool |
|
|
||||||
final isWeb = true; // bool |
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final result = api_instance.downloadFile(assetId, isThumb, isWeb);
|
final result = api_instance.downloadFile(assetId);
|
||||||
print(result);
|
print(result);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('Exception when calling AssetApi->downloadFile: $e\n');
|
print('Exception when calling AssetApi->downloadFile: $e\n');
|
||||||
@@ -264,8 +262,6 @@ try {
|
|||||||
Name | Type | Description | Notes
|
Name | Type | Description | Notes
|
||||||
------------- | ------------- | ------------- | -------------
|
------------- | ------------- | ------------- | -------------
|
||||||
**assetId** | **String**| |
|
**assetId** | **String**| |
|
||||||
**isThumb** | **bool**| | [optional]
|
|
||||||
**isWeb** | **bool**| | [optional]
|
|
||||||
|
|
||||||
### Return type
|
### Return type
|
||||||
|
|
||||||
|
|||||||
Generated
+3
-18
@@ -234,11 +234,7 @@ class AssetApi {
|
|||||||
/// Parameters:
|
/// Parameters:
|
||||||
///
|
///
|
||||||
/// * [String] assetId (required):
|
/// * [String] assetId (required):
|
||||||
///
|
Future<Response> downloadFileWithHttpInfo(String assetId,) async {
|
||||||
/// * [bool] isThumb:
|
|
||||||
///
|
|
||||||
/// * [bool] isWeb:
|
|
||||||
Future<Response> downloadFileWithHttpInfo(String assetId, { bool? isThumb, bool? isWeb, }) async {
|
|
||||||
// ignore: prefer_const_declarations
|
// ignore: prefer_const_declarations
|
||||||
final path = r'/asset/download/{assetId}'
|
final path = r'/asset/download/{assetId}'
|
||||||
.replaceAll('{assetId}', assetId);
|
.replaceAll('{assetId}', assetId);
|
||||||
@@ -250,13 +246,6 @@ class AssetApi {
|
|||||||
final headerParams = <String, String>{};
|
final headerParams = <String, String>{};
|
||||||
final formParams = <String, String>{};
|
final formParams = <String, String>{};
|
||||||
|
|
||||||
if (isThumb != null) {
|
|
||||||
queryParams.addAll(_queryParams('', 'isThumb', isThumb));
|
|
||||||
}
|
|
||||||
if (isWeb != null) {
|
|
||||||
queryParams.addAll(_queryParams('', 'isWeb', isWeb));
|
|
||||||
}
|
|
||||||
|
|
||||||
const contentTypes = <String>[];
|
const contentTypes = <String>[];
|
||||||
|
|
||||||
|
|
||||||
@@ -276,12 +265,8 @@ class AssetApi {
|
|||||||
/// Parameters:
|
/// Parameters:
|
||||||
///
|
///
|
||||||
/// * [String] assetId (required):
|
/// * [String] assetId (required):
|
||||||
///
|
Future<Object?> downloadFile(String assetId,) async {
|
||||||
/// * [bool] isThumb:
|
final response = await downloadFileWithHttpInfo(assetId,);
|
||||||
///
|
|
||||||
/// * [bool] isWeb:
|
|
||||||
Future<Object?> downloadFile(String assetId, { bool? isThumb, bool? isWeb, }) async {
|
|
||||||
final response = await downloadFileWithHttpInfo(assetId, isThumb: isThumb, isWeb: isWeb, );
|
|
||||||
if (response.statusCode >= HttpStatus.badRequest) {
|
if (response.statusCode >= HttpStatus.badRequest) {
|
||||||
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||||
}
|
}
|
||||||
|
|||||||
+14
-26
@@ -43,9 +43,7 @@ class AlbumResponseDto {
|
|||||||
List<AssetResponseDto> assets;
|
List<AssetResponseDto> assets;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) =>
|
bool operator ==(Object other) => identical(this, other) || other is AlbumResponseDto &&
|
||||||
identical(this, other) ||
|
|
||||||
other is AlbumResponseDto &&
|
|
||||||
other.assetCount == assetCount &&
|
other.assetCount == assetCount &&
|
||||||
other.id == id &&
|
other.id == id &&
|
||||||
other.ownerId == ownerId &&
|
other.ownerId == ownerId &&
|
||||||
@@ -70,8 +68,7 @@ class AlbumResponseDto {
|
|||||||
(assets.hashCode);
|
(assets.hashCode);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() =>
|
String toString() => 'AlbumResponseDto[assetCount=$assetCount, id=$id, ownerId=$ownerId, albumName=$albumName, createdAt=$createdAt, albumThumbnailAssetId=$albumThumbnailAssetId, shared=$shared, sharedUsers=$sharedUsers, assets=$assets]';
|
||||||
'AlbumResponseDto[assetCount=$assetCount, id=$id, ownerId=$ownerId, albumName=$albumName, createdAt=$createdAt, albumThumbnailAssetId=$albumThumbnailAssetId, shared=$shared, sharedUsers=$sharedUsers, assets=$assets]';
|
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
final json = <String, dynamic>{};
|
final json = <String, dynamic>{};
|
||||||
@@ -101,13 +98,13 @@ class AlbumResponseDto {
|
|||||||
// Ensure that the map contains the required keys.
|
// Ensure that the map contains the required keys.
|
||||||
// Note 1: the values aren't checked for validity beyond being non-null.
|
// Note 1: the values aren't checked for validity beyond being non-null.
|
||||||
// Note 2: this code is stripped in release mode!
|
// Note 2: this code is stripped in release mode!
|
||||||
// assert(() {
|
assert(() {
|
||||||
// requiredKeys.forEach((key) {
|
requiredKeys.forEach((key) {
|
||||||
// assert(json.containsKey(key), 'Required key "AlbumResponseDto[$key]" is missing from JSON.');
|
assert(json.containsKey(key), 'Required key "AlbumResponseDto[$key]" is missing from JSON.');
|
||||||
// assert(json[key] != null, 'Required key "AlbumResponseDto[$key]" has a null value in JSON.');
|
assert(json[key] != null, 'Required key "AlbumResponseDto[$key]" has a null value in JSON.');
|
||||||
// });
|
});
|
||||||
// return true;
|
return true;
|
||||||
// }());
|
}());
|
||||||
|
|
||||||
return AlbumResponseDto(
|
return AlbumResponseDto(
|
||||||
assetCount: mapValueOfType<int>(json, r'assetCount')!,
|
assetCount: mapValueOfType<int>(json, r'assetCount')!,
|
||||||
@@ -115,8 +112,7 @@ class AlbumResponseDto {
|
|||||||
ownerId: mapValueOfType<String>(json, r'ownerId')!,
|
ownerId: mapValueOfType<String>(json, r'ownerId')!,
|
||||||
albumName: mapValueOfType<String>(json, r'albumName')!,
|
albumName: mapValueOfType<String>(json, r'albumName')!,
|
||||||
createdAt: mapValueOfType<String>(json, r'createdAt')!,
|
createdAt: mapValueOfType<String>(json, r'createdAt')!,
|
||||||
albumThumbnailAssetId:
|
albumThumbnailAssetId: mapValueOfType<String>(json, r'albumThumbnailAssetId'),
|
||||||
mapValueOfType<String>(json, r'albumThumbnailAssetId'),
|
|
||||||
shared: mapValueOfType<bool>(json, r'shared')!,
|
shared: mapValueOfType<bool>(json, r'shared')!,
|
||||||
sharedUsers: UserResponseDto.listFromJson(json[r'sharedUsers'])!,
|
sharedUsers: UserResponseDto.listFromJson(json[r'sharedUsers'])!,
|
||||||
assets: AssetResponseDto.listFromJson(json[r'assets'])!,
|
assets: AssetResponseDto.listFromJson(json[r'assets'])!,
|
||||||
@@ -125,10 +121,7 @@ class AlbumResponseDto {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
static List<AlbumResponseDto>? listFromJson(
|
static List<AlbumResponseDto>? listFromJson(dynamic json, {bool growable = false,}) {
|
||||||
dynamic json, {
|
|
||||||
bool growable = false,
|
|
||||||
}) {
|
|
||||||
final result = <AlbumResponseDto>[];
|
final result = <AlbumResponseDto>[];
|
||||||
if (json is List && json.isNotEmpty) {
|
if (json is List && json.isNotEmpty) {
|
||||||
for (final row in json) {
|
for (final row in json) {
|
||||||
@@ -156,18 +149,12 @@ class AlbumResponseDto {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// maps a json object with a list of AlbumResponseDto-objects as value to a dart map
|
// maps a json object with a list of AlbumResponseDto-objects as value to a dart map
|
||||||
static Map<String, List<AlbumResponseDto>> mapListFromJson(
|
static Map<String, List<AlbumResponseDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||||
dynamic json, {
|
|
||||||
bool growable = false,
|
|
||||||
}) {
|
|
||||||
final map = <String, List<AlbumResponseDto>>{};
|
final map = <String, List<AlbumResponseDto>>{};
|
||||||
if (json is Map && json.isNotEmpty) {
|
if (json is Map && json.isNotEmpty) {
|
||||||
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||||
for (final entry in json.entries) {
|
for (final entry in json.entries) {
|
||||||
final value = AlbumResponseDto.listFromJson(
|
final value = AlbumResponseDto.listFromJson(entry.value, growable: growable,);
|
||||||
entry.value,
|
|
||||||
growable: growable,
|
|
||||||
);
|
|
||||||
if (value != null) {
|
if (value != null) {
|
||||||
map[entry.key] = value;
|
map[entry.key] = value;
|
||||||
}
|
}
|
||||||
@@ -189,3 +176,4 @@ class AlbumResponseDto {
|
|||||||
'assets',
|
'assets',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+13
-24
@@ -82,9 +82,7 @@ class AssetResponseDto {
|
|||||||
List<TagResponseDto> tags;
|
List<TagResponseDto> tags;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) =>
|
bool operator ==(Object other) => identical(this, other) || other is AssetResponseDto &&
|
||||||
identical(this, other) ||
|
|
||||||
other is AssetResponseDto &&
|
|
||||||
other.type == type &&
|
other.type == type &&
|
||||||
other.id == id &&
|
other.id == id &&
|
||||||
other.deviceAssetId == deviceAssetId &&
|
other.deviceAssetId == deviceAssetId &&
|
||||||
@@ -127,8 +125,7 @@ class AssetResponseDto {
|
|||||||
(tags.hashCode);
|
(tags.hashCode);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() =>
|
String toString() => 'AssetResponseDto[type=$type, id=$id, deviceAssetId=$deviceAssetId, ownerId=$ownerId, deviceId=$deviceId, originalPath=$originalPath, resizePath=$resizePath, createdAt=$createdAt, modifiedAt=$modifiedAt, isFavorite=$isFavorite, mimeType=$mimeType, duration=$duration, webpPath=$webpPath, encodedVideoPath=$encodedVideoPath, exifInfo=$exifInfo, smartInfo=$smartInfo, livePhotoVideoId=$livePhotoVideoId, tags=$tags]';
|
||||||
'AssetResponseDto[type=$type, id=$id, deviceAssetId=$deviceAssetId, ownerId=$ownerId, deviceId=$deviceId, originalPath=$originalPath, resizePath=$resizePath, createdAt=$createdAt, modifiedAt=$modifiedAt, isFavorite=$isFavorite, mimeType=$mimeType, duration=$duration, webpPath=$webpPath, encodedVideoPath=$encodedVideoPath, exifInfo=$exifInfo, smartInfo=$smartInfo, livePhotoVideoId=$livePhotoVideoId, tags=$tags]';
|
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
final json = <String, dynamic>{};
|
final json = <String, dynamic>{};
|
||||||
@@ -191,13 +188,13 @@ class AssetResponseDto {
|
|||||||
// Ensure that the map contains the required keys.
|
// Ensure that the map contains the required keys.
|
||||||
// Note 1: the values aren't checked for validity beyond being non-null.
|
// Note 1: the values aren't checked for validity beyond being non-null.
|
||||||
// Note 2: this code is stripped in release mode!
|
// Note 2: this code is stripped in release mode!
|
||||||
// assert(() {
|
assert(() {
|
||||||
// requiredKeys.forEach((key) {
|
requiredKeys.forEach((key) {
|
||||||
// assert(json.containsKey(key), 'Required key "AssetResponseDto[$key]" is missing from JSON.');
|
assert(json.containsKey(key), 'Required key "AssetResponseDto[$key]" is missing from JSON.');
|
||||||
// assert(json[key] != null, 'Required key "AssetResponseDto[$key]" has a null value in JSON.');
|
assert(json[key] != null, 'Required key "AssetResponseDto[$key]" has a null value in JSON.');
|
||||||
// });
|
});
|
||||||
// return true;
|
return true;
|
||||||
// }());
|
}());
|
||||||
|
|
||||||
return AssetResponseDto(
|
return AssetResponseDto(
|
||||||
type: AssetTypeEnum.fromJson(json[r'type'])!,
|
type: AssetTypeEnum.fromJson(json[r'type'])!,
|
||||||
@@ -223,10 +220,7 @@ class AssetResponseDto {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
static List<AssetResponseDto>? listFromJson(
|
static List<AssetResponseDto>? listFromJson(dynamic json, {bool growable = false,}) {
|
||||||
dynamic json, {
|
|
||||||
bool growable = false,
|
|
||||||
}) {
|
|
||||||
final result = <AssetResponseDto>[];
|
final result = <AssetResponseDto>[];
|
||||||
if (json is List && json.isNotEmpty) {
|
if (json is List && json.isNotEmpty) {
|
||||||
for (final row in json) {
|
for (final row in json) {
|
||||||
@@ -254,18 +248,12 @@ class AssetResponseDto {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// maps a json object with a list of AssetResponseDto-objects as value to a dart map
|
// maps a json object with a list of AssetResponseDto-objects as value to a dart map
|
||||||
static Map<String, List<AssetResponseDto>> mapListFromJson(
|
static Map<String, List<AssetResponseDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||||
dynamic json, {
|
|
||||||
bool growable = false,
|
|
||||||
}) {
|
|
||||||
final map = <String, List<AssetResponseDto>>{};
|
final map = <String, List<AssetResponseDto>>{};
|
||||||
if (json is Map && json.isNotEmpty) {
|
if (json is Map && json.isNotEmpty) {
|
||||||
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||||
for (final entry in json.entries) {
|
for (final entry in json.entries) {
|
||||||
final value = AssetResponseDto.listFromJson(
|
final value = AssetResponseDto.listFromJson(entry.value, growable: growable,);
|
||||||
entry.value,
|
|
||||||
growable: growable,
|
|
||||||
);
|
|
||||||
if (value != null) {
|
if (value != null) {
|
||||||
map[entry.key] = value;
|
map[entry.key] = value;
|
||||||
}
|
}
|
||||||
@@ -292,3 +280,4 @@ class AssetResponseDto {
|
|||||||
'tags',
|
'tags',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Generated
+1
-1
@@ -47,7 +47,7 @@ void main() {
|
|||||||
|
|
||||||
//
|
//
|
||||||
//
|
//
|
||||||
//Future<Object> downloadFile(String assetId, { bool isThumb, bool isWeb }) async
|
//Future<Object> downloadFile(String assetId) async
|
||||||
test('test downloadFile', () async {
|
test('test downloadFile', () async {
|
||||||
// TODO
|
// TODO
|
||||||
});
|
});
|
||||||
|
|||||||
+1
-1
@@ -2,7 +2,7 @@ name: immich_mobile
|
|||||||
description: Immich - selfhosted backup media file on mobile phone
|
description: Immich - selfhosted backup media file on mobile phone
|
||||||
|
|
||||||
publish_to: "none"
|
publish_to: "none"
|
||||||
version: 1.43.0+66
|
version: 1.45.0+68
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ">=2.17.0 <3.0.0"
|
sdk: ">=2.17.0 <3.0.0"
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:immich_mobile/modules/home/ui/asset_grid/asset_grid_data_structure.dart';
|
import 'package:immich_mobile/modules/home/ui/asset_grid/asset_grid_data_structure.dart';
|
||||||
import 'package:immich_mobile/shared/models/asset.dart';
|
import 'package:immich_mobile/shared/models/asset.dart';
|
||||||
import 'package:openapi/api.dart';
|
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
final List<Asset> testAssets = [];
|
final List<Asset> testAssets = [];
|
||||||
@@ -13,24 +12,14 @@ void main() {
|
|||||||
DateTime date = DateTime(2022, month, day);
|
DateTime date = DateTime(2022, month, day);
|
||||||
|
|
||||||
testAssets.add(
|
testAssets.add(
|
||||||
Asset.remote(
|
Asset(
|
||||||
AssetResponseDto(
|
deviceAssetId: '$i',
|
||||||
type: AssetTypeEnum.IMAGE,
|
|
||||||
id: '$i',
|
|
||||||
deviceAssetId: '',
|
|
||||||
ownerId: '',
|
|
||||||
deviceId: '',
|
deviceId: '',
|
||||||
originalPath: '',
|
ownerId: '',
|
||||||
resizePath: '',
|
createdAt: date,
|
||||||
createdAt: date.toIso8601String(),
|
modifiedAt: date,
|
||||||
modifiedAt: date.toIso8601String(),
|
durationInSeconds: 0,
|
||||||
isFavorite: false,
|
fileName: '',
|
||||||
mimeType: 'image/jpeg',
|
|
||||||
duration: '',
|
|
||||||
webpPath: '',
|
|
||||||
encodedVideoPath: '',
|
|
||||||
livePhotoVideoId: '',
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -70,11 +59,20 @@ void main() {
|
|||||||
// Day 1
|
// Day 1
|
||||||
// 15 Assets => 5 Rows
|
// 15 Assets => 5 Rows
|
||||||
expect(renderList.elements.length, 18);
|
expect(renderList.elements.length, 18);
|
||||||
expect(renderList.elements[0].type, RenderAssetGridElementType.monthTitle);
|
expect(
|
||||||
|
renderList.elements[0].type,
|
||||||
|
RenderAssetGridElementType.monthTitle,
|
||||||
|
);
|
||||||
expect(renderList.elements[0].date.month, 1);
|
expect(renderList.elements[0].date.month, 1);
|
||||||
expect(renderList.elements[7].type, RenderAssetGridElementType.monthTitle);
|
expect(
|
||||||
|
renderList.elements[7].type,
|
||||||
|
RenderAssetGridElementType.monthTitle,
|
||||||
|
);
|
||||||
expect(renderList.elements[7].date.month, 2);
|
expect(renderList.elements[7].date.month, 2);
|
||||||
expect(renderList.elements[11].type, RenderAssetGridElementType.monthTitle);
|
expect(
|
||||||
|
renderList.elements[11].type,
|
||||||
|
RenderAssetGridElementType.monthTitle,
|
||||||
|
);
|
||||||
expect(renderList.elements[11].date.month, 10);
|
expect(renderList.elements[11].date.month, 10);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
+4
-4
@@ -2,7 +2,7 @@ FROM node:16-alpine3.14 as builder
|
|||||||
|
|
||||||
WORKDIR /usr/src/app
|
WORKDIR /usr/src/app
|
||||||
|
|
||||||
RUN apk add --update-cache build-base python3 libheif vips-dev ffmpeg exiftool perl
|
RUN apk add --update-cache build-base python3 libheif vips-dev ffmpeg perl
|
||||||
|
|
||||||
COPY package.json package-lock.json ./
|
COPY package.json package-lock.json ./
|
||||||
|
|
||||||
@@ -14,14 +14,14 @@ COPY . .
|
|||||||
FROM builder as prod
|
FROM builder as prod
|
||||||
|
|
||||||
RUN npm run build
|
RUN npm run build
|
||||||
RUN npm prune --omit=dev
|
RUN npm prune --omit=dev --omit=optional
|
||||||
|
|
||||||
|
|
||||||
FROM node:16-alpine3.14
|
FROM node:16-alpine3.14
|
||||||
|
|
||||||
WORKDIR /usr/src/app
|
WORKDIR /usr/src/app
|
||||||
|
|
||||||
RUN apk add --no-cache libheif vips ffmpeg exiftool perl
|
RUN apk add --no-cache libheif vips ffmpeg perl
|
||||||
|
|
||||||
COPY --from=prod /usr/src/app/node_modules ./node_modules
|
COPY --from=prod /usr/src/app/node_modules ./node_modules
|
||||||
COPY --from=prod /usr/src/app/dist ./dist
|
COPY --from=prod /usr/src/app/dist ./dist
|
||||||
@@ -32,7 +32,7 @@ COPY LICENSE /LICENSE
|
|||||||
COPY package.json package-lock.json ./
|
COPY package.json package-lock.json ./
|
||||||
COPY start-server.sh start-microservices.sh ./
|
COPY start-server.sh start-microservices.sh ./
|
||||||
|
|
||||||
RUN npm link
|
RUN npm link && npm cache clean --force
|
||||||
|
|
||||||
VOLUME /usr/src/app/upload
|
VOLUME /usr/src/app/upload
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
Put,
|
Put,
|
||||||
UploadedFiles,
|
UploadedFiles,
|
||||||
Patch,
|
Patch,
|
||||||
|
StreamableFile,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { Authenticated } from '../../decorators/authenticated.decorator';
|
import { Authenticated } from '../../decorators/authenticated.decorator';
|
||||||
import { AssetService } from './asset.service';
|
import { AssetService } from './asset.service';
|
||||||
@@ -28,7 +29,7 @@ import { CheckDuplicateAssetDto } from './dto/check-duplicate-asset.dto';
|
|||||||
import { ApiBearerAuth, ApiBody, ApiConsumes, ApiHeader, ApiTags } from '@nestjs/swagger';
|
import { ApiBearerAuth, ApiBody, ApiConsumes, ApiHeader, ApiTags } from '@nestjs/swagger';
|
||||||
import { CuratedObjectsResponseDto } from './response-dto/curated-objects-response.dto';
|
import { CuratedObjectsResponseDto } from './response-dto/curated-objects-response.dto';
|
||||||
import { CuratedLocationsResponseDto } from './response-dto/curated-locations-response.dto';
|
import { CuratedLocationsResponseDto } from './response-dto/curated-locations-response.dto';
|
||||||
import { AssetResponseDto } from '@app/domain';
|
import { AssetResponseDto, ImmichReadStream } from '@app/domain';
|
||||||
import { CheckDuplicateAssetResponseDto } from './response-dto/check-duplicate-asset-response.dto';
|
import { CheckDuplicateAssetResponseDto } from './response-dto/check-duplicate-asset-response.dto';
|
||||||
import { AssetFileUploadDto } from './dto/asset-file-upload.dto';
|
import { AssetFileUploadDto } from './dto/asset-file-upload.dto';
|
||||||
import { CreateAssetDto, mapToUploadFile } from './dto/create-asset.dto';
|
import { CreateAssetDto, mapToUploadFile } from './dto/create-asset.dto';
|
||||||
@@ -55,6 +56,10 @@ import { UpdateAssetsToSharedLinkDto } from './dto/add-assets-to-shared-link.dto
|
|||||||
import { AssetSearchDto } from './dto/asset-search.dto';
|
import { AssetSearchDto } from './dto/asset-search.dto';
|
||||||
import { assetUploadOption, ImmichFile } from '../../config/asset-upload.config';
|
import { assetUploadOption, ImmichFile } from '../../config/asset-upload.config';
|
||||||
|
|
||||||
|
function asStreamableFile({ stream, type, length }: ImmichReadStream) {
|
||||||
|
return new StreamableFile(stream, { type, length });
|
||||||
|
}
|
||||||
|
|
||||||
@ApiBearerAuth()
|
@ApiBearerAuth()
|
||||||
@ApiTags('Asset')
|
@ApiTags('Asset')
|
||||||
@Controller('asset')
|
@Controller('asset')
|
||||||
@@ -92,7 +97,7 @@ export class AssetController {
|
|||||||
|
|
||||||
const responseDto = await this.assetService.uploadFile(authUser, dto, file, livePhotoFile);
|
const responseDto = await this.assetService.uploadFile(authUser, dto, file, livePhotoFile);
|
||||||
if (responseDto.duplicate) {
|
if (responseDto.duplicate) {
|
||||||
res.send(200);
|
res.status(200);
|
||||||
}
|
}
|
||||||
|
|
||||||
return responseDto;
|
return responseDto;
|
||||||
@@ -103,12 +108,9 @@ export class AssetController {
|
|||||||
async downloadFile(
|
async downloadFile(
|
||||||
@GetAuthUser() authUser: AuthUserDto,
|
@GetAuthUser() authUser: AuthUserDto,
|
||||||
@Response({ passthrough: true }) res: Res,
|
@Response({ passthrough: true }) res: Res,
|
||||||
@Query(new ValidationPipe({ transform: true })) query: ServeFileDto,
|
|
||||||
@Param('assetId') assetId: string,
|
@Param('assetId') assetId: string,
|
||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
this.assetService.checkDownloadAccess(authUser);
|
return this.assetService.downloadFile(authUser, assetId).then(asStreamableFile);
|
||||||
await this.assetService.checkAssetsAccess(authUser, [assetId]);
|
|
||||||
return this.assetService.downloadFile(query, assetId, res);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Authenticated({ isShared: true })
|
@Authenticated({ isShared: true })
|
||||||
|
|||||||
@@ -9,12 +9,13 @@ import { AssetCountByUserIdResponseDto } from './response-dto/asset-count-by-use
|
|||||||
import { DownloadService } from '../../modules/download/download.service';
|
import { DownloadService } from '../../modules/download/download.service';
|
||||||
import { AlbumRepository, IAlbumRepository } from '../album/album-repository';
|
import { AlbumRepository, IAlbumRepository } from '../album/album-repository';
|
||||||
import { StorageService } from '@app/storage';
|
import { StorageService } from '@app/storage';
|
||||||
import { ICryptoRepository, IJobRepository, ISharedLinkRepository, JobName } from '@app/domain';
|
import { ICryptoRepository, IJobRepository, ISharedLinkRepository, IStorageRepository, JobName } from '@app/domain';
|
||||||
import {
|
import {
|
||||||
authStub,
|
authStub,
|
||||||
newCryptoRepositoryMock,
|
newCryptoRepositoryMock,
|
||||||
newJobRepositoryMock,
|
newJobRepositoryMock,
|
||||||
newSharedLinkRepositoryMock,
|
newSharedLinkRepositoryMock,
|
||||||
|
newStorageRepositoryMock,
|
||||||
sharedLinkResponseStub,
|
sharedLinkResponseStub,
|
||||||
sharedLinkStub,
|
sharedLinkStub,
|
||||||
} from '@app/domain/../test';
|
} from '@app/domain/../test';
|
||||||
@@ -110,6 +111,7 @@ describe('AssetService', () => {
|
|||||||
let sharedLinkRepositoryMock: jest.Mocked<ISharedLinkRepository>;
|
let sharedLinkRepositoryMock: jest.Mocked<ISharedLinkRepository>;
|
||||||
let cryptoMock: jest.Mocked<ICryptoRepository>;
|
let cryptoMock: jest.Mocked<ICryptoRepository>;
|
||||||
let jobMock: jest.Mocked<IJobRepository>;
|
let jobMock: jest.Mocked<IJobRepository>;
|
||||||
|
let storageMock: jest.Mocked<IStorageRepository>;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
assetRepositoryMock = {
|
assetRepositoryMock = {
|
||||||
@@ -154,6 +156,7 @@ describe('AssetService', () => {
|
|||||||
sharedLinkRepositoryMock = newSharedLinkRepositoryMock();
|
sharedLinkRepositoryMock = newSharedLinkRepositoryMock();
|
||||||
jobMock = newJobRepositoryMock();
|
jobMock = newJobRepositoryMock();
|
||||||
cryptoMock = newCryptoRepositoryMock();
|
cryptoMock = newCryptoRepositoryMock();
|
||||||
|
storageMock = newStorageRepositoryMock();
|
||||||
|
|
||||||
sut = new AssetService(
|
sut = new AssetService(
|
||||||
assetRepositoryMock,
|
assetRepositoryMock,
|
||||||
@@ -164,6 +167,7 @@ describe('AssetService', () => {
|
|||||||
sharedLinkRepositoryMock,
|
sharedLinkRepositoryMock,
|
||||||
jobMock,
|
jobMock,
|
||||||
cryptoMock,
|
cryptoMock,
|
||||||
|
storageMock,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -413,4 +417,15 @@ describe('AssetService', () => {
|
|||||||
expect(() => sut.checkDownloadAccess(authStub.readonlySharedLink)).toThrow(ForbiddenException);
|
expect(() => sut.checkDownloadAccess(authStub.readonlySharedLink)).toThrow(ForbiddenException);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('downloadFile', () => {
|
||||||
|
it('should download a single file', async () => {
|
||||||
|
assetRepositoryMock.countByIdAndUser.mockResolvedValue(1);
|
||||||
|
assetRepositoryMock.get.mockResolvedValue(_getAsset_1());
|
||||||
|
|
||||||
|
await sut.downloadFile(authStub.admin, 'id_1');
|
||||||
|
|
||||||
|
expect(storageMock.createReadStream).toHaveBeenCalledWith('fake_path/asset_1.jpeg', 'image/jpeg');
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import {
|
|||||||
StreamableFile,
|
StreamableFile,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
import { createHash } from 'node:crypto';
|
|
||||||
import { QueryFailedError, Repository } from 'typeorm';
|
import { QueryFailedError, Repository } from 'typeorm';
|
||||||
import { AuthUserDto } from '../../decorators/auth-user.decorator';
|
import { AuthUserDto } from '../../decorators/auth-user.decorator';
|
||||||
import { AssetEntity, AssetType, SharedLinkType } from '@app/infra';
|
import { AssetEntity, AssetType, SharedLinkType } from '@app/infra';
|
||||||
@@ -23,7 +22,14 @@ import { SearchAssetDto } from './dto/search-asset.dto';
|
|||||||
import fs from 'fs/promises';
|
import fs from 'fs/promises';
|
||||||
import { CheckDuplicateAssetDto } from './dto/check-duplicate-asset.dto';
|
import { CheckDuplicateAssetDto } from './dto/check-duplicate-asset.dto';
|
||||||
import { CuratedObjectsResponseDto } from './response-dto/curated-objects-response.dto';
|
import { CuratedObjectsResponseDto } from './response-dto/curated-objects-response.dto';
|
||||||
import { AssetResponseDto, JobName, mapAsset, mapAssetWithoutExif } from '@app/domain';
|
import {
|
||||||
|
AssetResponseDto,
|
||||||
|
ImmichReadStream,
|
||||||
|
IStorageRepository,
|
||||||
|
JobName,
|
||||||
|
mapAsset,
|
||||||
|
mapAssetWithoutExif,
|
||||||
|
} from '@app/domain';
|
||||||
import { CreateAssetDto, UploadFile } from './dto/create-asset.dto';
|
import { CreateAssetDto, UploadFile } from './dto/create-asset.dto';
|
||||||
import { DeleteAssetResponseDto, DeleteAssetStatusEnum } from './response-dto/delete-asset-response.dto';
|
import { DeleteAssetResponseDto, DeleteAssetStatusEnum } from './response-dto/delete-asset-response.dto';
|
||||||
import { GetAssetThumbnailDto, GetAssetThumbnailFormatEnum } from './dto/get-asset-thumbnail.dto';
|
import { GetAssetThumbnailDto, GetAssetThumbnailFormatEnum } from './dto/get-asset-thumbnail.dto';
|
||||||
@@ -73,6 +79,7 @@ export class AssetService {
|
|||||||
@Inject(ISharedLinkRepository) sharedLinkRepository: ISharedLinkRepository,
|
@Inject(ISharedLinkRepository) sharedLinkRepository: ISharedLinkRepository,
|
||||||
@Inject(IJobRepository) private jobRepository: IJobRepository,
|
@Inject(IJobRepository) private jobRepository: IJobRepository,
|
||||||
@Inject(ICryptoRepository) cryptoRepository: ICryptoRepository,
|
@Inject(ICryptoRepository) cryptoRepository: ICryptoRepository,
|
||||||
|
@Inject(IStorageRepository) private storage: IStorageRepository,
|
||||||
) {
|
) {
|
||||||
this.assetCore = new AssetCore(_assetRepository, jobRepository, storageService);
|
this.assetCore = new AssetCore(_assetRepository, jobRepository, storageService);
|
||||||
this.shareCore = new ShareCore(sharedLinkRepository, cryptoRepository);
|
this.shareCore = new ShareCore(sharedLinkRepository, cryptoRepository);
|
||||||
@@ -189,62 +196,21 @@ export class AssetService {
|
|||||||
return this.downloadService.downloadArchive(`immich-${now}`, assetToDownload);
|
return this.downloadService.downloadArchive(`immich-${now}`, assetToDownload);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async downloadFile(query: ServeFileDto, assetId: string, res: Res) {
|
public async downloadFile(authUser: AuthUserDto, assetId: string): Promise<ImmichReadStream> {
|
||||||
|
this.checkDownloadAccess(authUser);
|
||||||
|
await this.checkAssetsAccess(authUser, [assetId]);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let fileReadStream = null;
|
const asset = await this._assetRepository.get(assetId);
|
||||||
const asset = await this._assetRepository.getById(assetId);
|
if (asset && asset.originalPath && asset.mimeType) {
|
||||||
|
return this.storage.createReadStream(asset.originalPath, asset.mimeType);
|
||||||
// Download Video
|
|
||||||
if (asset.type === AssetType.VIDEO) {
|
|
||||||
const { size } = await fileInfo(asset.originalPath);
|
|
||||||
|
|
||||||
res.set({
|
|
||||||
'Content-Type': asset.mimeType,
|
|
||||||
'Content-Length': size,
|
|
||||||
});
|
|
||||||
|
|
||||||
await fs.access(asset.originalPath, constants.R_OK | constants.W_OK);
|
|
||||||
fileReadStream = createReadStream(asset.originalPath);
|
|
||||||
} else {
|
|
||||||
// Download Image
|
|
||||||
if (!query.isThumb) {
|
|
||||||
/**
|
|
||||||
* Download Image Original File
|
|
||||||
*/
|
|
||||||
const { size } = await fileInfo(asset.originalPath);
|
|
||||||
|
|
||||||
res.set({
|
|
||||||
'Content-Type': asset.mimeType,
|
|
||||||
'Content-Length': size,
|
|
||||||
});
|
|
||||||
|
|
||||||
await fs.access(asset.originalPath, constants.R_OK | constants.W_OK);
|
|
||||||
fileReadStream = createReadStream(asset.originalPath);
|
|
||||||
} else {
|
|
||||||
/**
|
|
||||||
* Download Image Resize File
|
|
||||||
*/
|
|
||||||
if (!asset.resizePath) {
|
|
||||||
throw new NotFoundException('resizePath not set');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const { size } = await fileInfo(asset.resizePath);
|
|
||||||
|
|
||||||
res.set({
|
|
||||||
'Content-Type': 'image/jpeg',
|
|
||||||
'Content-Length': size,
|
|
||||||
});
|
|
||||||
|
|
||||||
await fs.access(asset.resizePath, constants.R_OK | constants.W_OK);
|
|
||||||
fileReadStream = createReadStream(asset.resizePath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return new StreamableFile(fileReadStream);
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Logger.error(`Error download asset ${e}`, 'downloadFile');
|
Logger.error(`Error download asset ${e}`, 'downloadFile');
|
||||||
throw new InternalServerErrorException(`Failed to download asset ${e}`, 'DownloadFile');
|
throw new InternalServerErrorException(`Failed to download asset ${e}`, 'DownloadFile');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
throw new NotFoundException();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getAssetThumbnail(
|
public async getAssetThumbnail(
|
||||||
@@ -255,8 +221,7 @@ export class AssetService {
|
|||||||
) {
|
) {
|
||||||
let fileReadStream: ReadStream;
|
let fileReadStream: ReadStream;
|
||||||
|
|
||||||
const asset = await this.assetRepository.findOne({ where: { id: assetId } });
|
const asset = await this._assetRepository.get(assetId);
|
||||||
|
|
||||||
if (!asset) {
|
if (!asset) {
|
||||||
throw new NotFoundException('Asset not found');
|
throw new NotFoundException('Asset not found');
|
||||||
}
|
}
|
||||||
@@ -460,7 +425,7 @@ export class AssetService {
|
|||||||
try {
|
try {
|
||||||
await this._assetRepository.remove(asset);
|
await this._assetRepository.remove(asset);
|
||||||
|
|
||||||
result.push({ id: asset.id, status: DeleteAssetStatusEnum.SUCCESS });
|
result.push({ id, status: DeleteAssetStatusEnum.SUCCESS });
|
||||||
deleteQueue.push(asset as any);
|
deleteQueue.push(asset as any);
|
||||||
|
|
||||||
// TODO refactor this to use cascades
|
// TODO refactor this to use cascades
|
||||||
@@ -584,18 +549,6 @@ export class AssetService {
|
|||||||
return this._assetRepository.getAssetByChecksum(userId, checksum);
|
return this._assetRepository.getAssetByChecksum(userId, checksum);
|
||||||
}
|
}
|
||||||
|
|
||||||
calculateChecksum(filePath: string): Promise<Buffer> {
|
|
||||||
const fileReadStream = createReadStream(filePath);
|
|
||||||
const sha1Hash = createHash('sha1');
|
|
||||||
const deferred = new Promise<Buffer>((resolve, reject) => {
|
|
||||||
sha1Hash.once('error', (err) => reject(err));
|
|
||||||
sha1Hash.once('finish', () => resolve(sha1Hash.read()));
|
|
||||||
});
|
|
||||||
|
|
||||||
fileReadStream.pipe(sha1Hash);
|
|
||||||
return deferred;
|
|
||||||
}
|
|
||||||
|
|
||||||
getAssetCountByUserId(authUser: AuthUserDto): Promise<AssetCountByUserIdResponseDto> {
|
getAssetCountByUserId(authUser: AuthUserDto): Promise<AssetCountByUserIdResponseDto> {
|
||||||
return this._assetRepository.getAssetCountByUserId(authUser.id);
|
return this._assetRepository.getAssetCountByUserId(authUser.id);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,10 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { JobService } from './job.service';
|
import { JobService } from './job.service';
|
||||||
import { JobController } from './job.controller';
|
import { JobController } from './job.controller';
|
||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
|
||||||
import { ExifEntity } from '@app/infra';
|
|
||||||
import { TagModule } from '../tag/tag.module';
|
|
||||||
import { AssetModule } from '../asset/asset.module';
|
import { AssetModule } from '../asset/asset.module';
|
||||||
import { StorageModule } from '@app/storage';
|
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [TypeOrmModule.forFeature([ExifEntity]), TagModule, AssetModule, StorageModule],
|
imports: [AssetModule],
|
||||||
controllers: [JobController],
|
controllers: [JobController],
|
||||||
providers: [JobService],
|
providers: [JobService],
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { immichAppConfig } from '@app/common/config';
|
import { immichAppConfig } from '@app/common/config';
|
||||||
import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { AssetModule } from './api-v1/asset/asset.module';
|
import { AssetModule } from './api-v1/asset/asset.module';
|
||||||
import { ConfigModule } from '@nestjs/config';
|
import { ConfigModule } from '@nestjs/config';
|
||||||
import { ServerInfoModule } from './api-v1/server-info/server-info.module';
|
import { ServerInfoModule } from './api-v1/server-info/server-info.module';
|
||||||
@@ -61,12 +61,4 @@ import { AuthGuard } from './middlewares/auth.guard';
|
|||||||
],
|
],
|
||||||
providers: [{ provide: APP_GUARD, useExisting: AuthGuard }, AuthGuard],
|
providers: [{ provide: APP_GUARD, useExisting: AuthGuard }, AuthGuard],
|
||||||
})
|
})
|
||||||
export class AppModule implements NestModule {
|
export class AppModule {}
|
||||||
// TODO: check if consumer is needed or remove
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
configure(consumer: MiddlewareConsumer): void {
|
|
||||||
if (process.env.NODE_ENV == 'development') {
|
|
||||||
// consumer.apply(AppLoggerMiddleware).forRoutes('*');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,22 +0,0 @@
|
|||||||
import { Injectable, NestMiddleware, Logger } from '@nestjs/common';
|
|
||||||
|
|
||||||
import { Request, Response, NextFunction } from 'express';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class AppLoggerMiddleware implements NestMiddleware {
|
|
||||||
private logger = new Logger('HTTP');
|
|
||||||
|
|
||||||
use(request: Request, response: Response, next: NextFunction): void {
|
|
||||||
const { ip, method, baseUrl } = request;
|
|
||||||
const userAgent = request.get('user-agent') || '';
|
|
||||||
|
|
||||||
response.on('close', () => {
|
|
||||||
const { statusCode } = response;
|
|
||||||
const contentLength = response.get('content-length');
|
|
||||||
|
|
||||||
this.logger.log(`${method} ${baseUrl} ${statusCode} ${contentLength} - ${userAgent} ${ip}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
next();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1109,24 +1109,6 @@
|
|||||||
"operationId": "downloadFile",
|
"operationId": "downloadFile",
|
||||||
"description": "",
|
"description": "",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
|
||||||
"name": "isThumb",
|
|
||||||
"required": false,
|
|
||||||
"in": "query",
|
|
||||||
"schema": {
|
|
||||||
"title": "Is serve thumbnail (resize) file",
|
|
||||||
"type": "boolean"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "isWeb",
|
|
||||||
"required": false,
|
|
||||||
"in": "query",
|
|
||||||
"schema": {
|
|
||||||
"title": "Is request made from web",
|
|
||||||
"type": "boolean"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "assetId",
|
"name": "assetId",
|
||||||
"required": true,
|
"required": true,
|
||||||
@@ -2707,7 +2689,7 @@
|
|||||||
"info": {
|
"info": {
|
||||||
"title": "Immich",
|
"title": "Immich",
|
||||||
"description": "Immich API",
|
"description": "Immich API",
|
||||||
"version": "1.43.1",
|
"version": "1.45.0",
|
||||||
"contact": {}
|
"contact": {}
|
||||||
},
|
},
|
||||||
"tags": [],
|
"tags": [],
|
||||||
|
|||||||
@@ -6,11 +6,6 @@ import { ICryptoRepository } from '../crypto/crypto.repository';
|
|||||||
import { LoginResponseDto, mapLoginResponse } from './response-dto';
|
import { LoginResponseDto, mapLoginResponse } from './response-dto';
|
||||||
import { IUserTokenRepository, UserTokenCore } from '../user-token';
|
import { IUserTokenRepository, UserTokenCore } from '../user-token';
|
||||||
|
|
||||||
export type JwtValidationResult = {
|
|
||||||
status: boolean;
|
|
||||||
userId: string | null;
|
|
||||||
};
|
|
||||||
|
|
||||||
export class AuthCore {
|
export class AuthCore {
|
||||||
private userTokenCore: UserTokenCore;
|
private userTokenCore: UserTokenCore;
|
||||||
constructor(
|
constructor(
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
export * from './auth-user.dto';
|
export * from './auth-user.dto';
|
||||||
export * from './change-password.dto';
|
export * from './change-password.dto';
|
||||||
export * from './jwt-payload.dto';
|
|
||||||
export * from './login-credential.dto';
|
export * from './login-credential.dto';
|
||||||
export * from './sign-up.dto';
|
export * from './sign-up.dto';
|
||||||
|
|||||||
@@ -1,4 +0,0 @@
|
|||||||
export class JwtPayloadDto {
|
|
||||||
userId!: string;
|
|
||||||
email!: string;
|
|
||||||
}
|
|
||||||
@@ -8,6 +8,7 @@ export * from './domain.module';
|
|||||||
export * from './job';
|
export * from './job';
|
||||||
export * from './oauth';
|
export * from './oauth';
|
||||||
export * from './share';
|
export * from './share';
|
||||||
|
export * from './storage';
|
||||||
export * from './system-config';
|
export * from './system-config';
|
||||||
export * from './tag';
|
export * from './tag';
|
||||||
export * from './user';
|
export * from './user';
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from './storage.repository';
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { ReadStream } from 'fs';
|
||||||
|
|
||||||
|
export interface ImmichReadStream {
|
||||||
|
stream: ReadStream;
|
||||||
|
type: string;
|
||||||
|
length: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const IStorageRepository = 'IStorageRepository';
|
||||||
|
|
||||||
|
export interface IStorageRepository {
|
||||||
|
createReadStream(filepath: string, mimeType: string): Promise<ImmichReadStream>;
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ export * from './device-info.repository.mock';
|
|||||||
export * from './fixtures';
|
export * from './fixtures';
|
||||||
export * from './job.repository.mock';
|
export * from './job.repository.mock';
|
||||||
export * from './shared-link.repository.mock';
|
export * from './shared-link.repository.mock';
|
||||||
|
export * from './storage.repository.mock';
|
||||||
export * from './system-config.repository.mock';
|
export * from './system-config.repository.mock';
|
||||||
export * from './user-token.repository.mock';
|
export * from './user-token.repository.mock';
|
||||||
export * from './user.repository.mock';
|
export * from './user.repository.mock';
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import { IStorageRepository } from '../src';
|
||||||
|
|
||||||
|
export const newStorageRepositoryMock = (): jest.Mocked<IStorageRepository> => {
|
||||||
|
return {
|
||||||
|
createReadStream: jest.fn(),
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
IJobRepository,
|
IJobRepository,
|
||||||
IKeyRepository,
|
IKeyRepository,
|
||||||
ISharedLinkRepository,
|
ISharedLinkRepository,
|
||||||
|
IStorageRepository,
|
||||||
ISystemConfigRepository,
|
ISystemConfigRepository,
|
||||||
IUserRepository,
|
IUserRepository,
|
||||||
QueueName,
|
QueueName,
|
||||||
@@ -29,6 +30,7 @@ import {
|
|||||||
UserTokenEntity,
|
UserTokenEntity,
|
||||||
} from './db';
|
} from './db';
|
||||||
import { JobRepository } from './job';
|
import { JobRepository } from './job';
|
||||||
|
import { FilesystemProvider } from './storage';
|
||||||
|
|
||||||
const providers: Provider[] = [
|
const providers: Provider[] = [
|
||||||
{ provide: ICryptoRepository, useClass: CryptoRepository },
|
{ provide: ICryptoRepository, useClass: CryptoRepository },
|
||||||
@@ -36,6 +38,7 @@ const providers: Provider[] = [
|
|||||||
{ provide: IKeyRepository, useClass: APIKeyRepository },
|
{ provide: IKeyRepository, useClass: APIKeyRepository },
|
||||||
{ provide: IJobRepository, useClass: JobRepository },
|
{ provide: IJobRepository, useClass: JobRepository },
|
||||||
{ provide: ISharedLinkRepository, useClass: SharedLinkRepository },
|
{ provide: ISharedLinkRepository, useClass: SharedLinkRepository },
|
||||||
|
{ provide: IStorageRepository, useClass: FilesystemProvider },
|
||||||
{ provide: ISystemConfigRepository, useClass: SystemConfigRepository },
|
{ provide: ISystemConfigRepository, useClass: SystemConfigRepository },
|
||||||
{ provide: IUserRepository, useClass: UserRepository },
|
{ provide: IUserRepository, useClass: UserRepository },
|
||||||
{ provide: IUserTokenRepository, useClass: UserTokenRepository },
|
{ provide: IUserTokenRepository, useClass: UserTokenRepository },
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import { ImmichReadStream, IStorageRepository } from '@app/domain';
|
||||||
|
import { constants, createReadStream, stat } from 'fs';
|
||||||
|
import fs from 'fs/promises';
|
||||||
|
import { promisify } from 'util';
|
||||||
|
|
||||||
|
const fileInfo = promisify(stat);
|
||||||
|
|
||||||
|
export class FilesystemProvider implements IStorageRepository {
|
||||||
|
async createReadStream(filepath: string, mimeType: string): Promise<ImmichReadStream> {
|
||||||
|
const { size } = await fileInfo(filepath);
|
||||||
|
await fs.access(filepath, constants.R_OK | constants.W_OK);
|
||||||
|
return {
|
||||||
|
stream: createReadStream(filepath),
|
||||||
|
length: size,
|
||||||
|
type: mimeType,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from './filesystem.provider';
|
||||||
Generated
+731
-516
File diff suppressed because it is too large
Load Diff
+5
-9
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "immich",
|
"name": "immich",
|
||||||
"version": "1.44.0",
|
"version": "1.45.0",
|
||||||
"description": "",
|
"description": "",
|
||||||
"author": "",
|
"author": "",
|
||||||
"private": true,
|
"private": true,
|
||||||
@@ -42,7 +42,6 @@
|
|||||||
"@nestjs/common": "^9.2.1",
|
"@nestjs/common": "^9.2.1",
|
||||||
"@nestjs/config": "^2.2.0",
|
"@nestjs/config": "^2.2.0",
|
||||||
"@nestjs/core": "^9.2.1",
|
"@nestjs/core": "^9.2.1",
|
||||||
"@nestjs/mapped-types": "1.2.0",
|
|
||||||
"@nestjs/platform-express": "^9.2.1",
|
"@nestjs/platform-express": "^9.2.1",
|
||||||
"@nestjs/platform-socket.io": "^9.2.1",
|
"@nestjs/platform-socket.io": "^9.2.1",
|
||||||
"@nestjs/schedule": "^2.1.0",
|
"@nestjs/schedule": "^2.1.0",
|
||||||
@@ -58,15 +57,11 @@
|
|||||||
"class-validator": "^0.13.2",
|
"class-validator": "^0.13.2",
|
||||||
"cookie-parser": "^1.4.6",
|
"cookie-parser": "^1.4.6",
|
||||||
"diskusage": "^1.1.3",
|
"diskusage": "^1.1.3",
|
||||||
"dotenv": "^14.2.0",
|
|
||||||
"exiftool-vendored": "^19.0.0",
|
"exiftool-vendored": "^19.0.0",
|
||||||
"fdir": "^5.3.0",
|
"exiftool-vendored.pl": "^12.54.0",
|
||||||
"fluent-ffmpeg": "^2.1.2",
|
"fluent-ffmpeg": "^2.1.2",
|
||||||
"geo-tz": "^7.0.2",
|
|
||||||
"handlebars": "^4.7.7",
|
"handlebars": "^4.7.7",
|
||||||
"i18n-iso-countries": "^7.5.0",
|
"i18n-iso-countries": "^7.5.0",
|
||||||
"ioredis": "^5.2.4",
|
|
||||||
"jest-when": "^3.5.2",
|
|
||||||
"joi": "^17.5.0",
|
"joi": "^17.5.0",
|
||||||
"local-reverse-geocoder": "0.12.5",
|
"local-reverse-geocoder": "0.12.5",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
@@ -77,11 +72,9 @@
|
|||||||
"pg": "^8.8.0",
|
"pg": "^8.8.0",
|
||||||
"redis": "^4.5.1",
|
"redis": "^4.5.1",
|
||||||
"reflect-metadata": "^0.1.13",
|
"reflect-metadata": "^0.1.13",
|
||||||
"rimraf": "^3.0.2",
|
|
||||||
"rxjs": "^7.2.0",
|
"rxjs": "^7.2.0",
|
||||||
"sanitize-filename": "^1.6.3",
|
"sanitize-filename": "^1.6.3",
|
||||||
"sharp": "^0.28.0",
|
"sharp": "^0.28.0",
|
||||||
"systeminformation": "^5.11.0",
|
|
||||||
"typeorm": "^0.3.11"
|
"typeorm": "^0.3.11"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -107,11 +100,14 @@
|
|||||||
"@types/supertest": "^2.0.11",
|
"@types/supertest": "^2.0.11",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.48.1",
|
"@typescript-eslint/eslint-plugin": "^5.48.1",
|
||||||
"@typescript-eslint/parser": "^5.48.1",
|
"@typescript-eslint/parser": "^5.48.1",
|
||||||
|
"dotenv": "^14.2.0",
|
||||||
"eslint": "^8.31.0",
|
"eslint": "^8.31.0",
|
||||||
"eslint-config-prettier": "^8.3.0",
|
"eslint-config-prettier": "^8.3.0",
|
||||||
"eslint-plugin-prettier": "^4.0.0",
|
"eslint-plugin-prettier": "^4.0.0",
|
||||||
"jest": "^27.2.5",
|
"jest": "^27.2.5",
|
||||||
|
"jest-when": "^3.5.2",
|
||||||
"prettier": "^2.3.2",
|
"prettier": "^2.3.2",
|
||||||
|
"rimraf": "^3.0.2",
|
||||||
"source-map-support": "^0.5.20",
|
"source-map-support": "^0.5.20",
|
||||||
"supertest": "^6.1.3",
|
"supertest": "^6.1.3",
|
||||||
"ts-jest": "^27.0.3",
|
"ts-jest": "^27.0.3",
|
||||||
|
|||||||
+20
-9
@@ -1,29 +1,40 @@
|
|||||||
# Our Node base image
|
# Our Node base image
|
||||||
FROM node:16-alpine3.14 as base
|
FROM node:16-alpine3.14 as base
|
||||||
|
|
||||||
COPY LICENSE /licenses/LICENSE.txt
|
|
||||||
COPY LICENSE /LICENSE
|
|
||||||
|
|
||||||
WORKDIR /usr/src/app
|
WORKDIR /usr/src/app
|
||||||
|
EXPOSE 3000
|
||||||
|
RUN apk add --no-cache setpriv
|
||||||
|
|
||||||
|
FROM base as builder
|
||||||
|
|
||||||
RUN chown node:node /usr/src/app
|
RUN chown node:node /usr/src/app
|
||||||
|
|
||||||
RUN apk add --no-cache setpriv
|
|
||||||
|
|
||||||
COPY --chown=node:node package*.json ./
|
COPY --chown=node:node package*.json ./
|
||||||
|
|
||||||
RUN npm ci
|
RUN npm ci
|
||||||
|
|
||||||
COPY --chown=node:node . .
|
COPY --chown=node:node . .
|
||||||
|
|
||||||
RUN npm run build
|
|
||||||
|
|
||||||
EXPOSE 3000
|
EXPOSE 3000
|
||||||
|
|
||||||
FROM base AS dev
|
FROM builder AS dev
|
||||||
ENV CHOKIDAR_USEPOLLING=true
|
ENV CHOKIDAR_USEPOLLING=true
|
||||||
EXPOSE 24678
|
EXPOSE 24678
|
||||||
CMD ["npm", "run", "dev"]
|
CMD ["npm", "run", "dev"]
|
||||||
|
|
||||||
FROM base as prod
|
FROM builder AS prod
|
||||||
|
|
||||||
|
RUN npm run build
|
||||||
|
RUN npm prune --omit=dev
|
||||||
|
|
||||||
|
FROM base
|
||||||
|
|
||||||
ENV NODE_ENV=production
|
ENV NODE_ENV=production
|
||||||
|
|
||||||
|
WORKDIR /usr/src/app
|
||||||
|
|
||||||
|
COPY --from=prod /usr/src/app/node_modules ./node_modules
|
||||||
|
COPY --from=prod /usr/src/app/build ./build
|
||||||
|
|
||||||
|
COPY package.json package-lock.json ./
|
||||||
|
COPY entrypoint.sh ./
|
||||||
|
|||||||
Generated
+4
-3
@@ -14,7 +14,6 @@
|
|||||||
"exifr": "^7.1.3",
|
"exifr": "^7.1.3",
|
||||||
"handlebars": "^4.7.7",
|
"handlebars": "^4.7.7",
|
||||||
"leaflet": "^1.8.0",
|
"leaflet": "^1.8.0",
|
||||||
"lodash": "^4.17.21",
|
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
"luxon": "^3.1.1",
|
"luxon": "^3.1.1",
|
||||||
"socket.io-client": "^4.5.1",
|
"socket.io-client": "^4.5.1",
|
||||||
@@ -8999,7 +8998,8 @@
|
|||||||
"node_modules/lodash": {
|
"node_modules/lodash": {
|
||||||
"version": "4.17.21",
|
"version": "4.17.21",
|
||||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
|
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
|
||||||
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/lodash-es": {
|
"node_modules/lodash-es": {
|
||||||
"version": "4.17.21",
|
"version": "4.17.21",
|
||||||
@@ -17880,7 +17880,8 @@
|
|||||||
"lodash": {
|
"lodash": {
|
||||||
"version": "4.17.21",
|
"version": "4.17.21",
|
||||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
|
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
|
||||||
|
"dev": true
|
||||||
},
|
},
|
||||||
"lodash-es": {
|
"lodash-es": {
|
||||||
"version": "4.17.21",
|
"version": "4.17.21",
|
||||||
|
|||||||
@@ -66,7 +66,6 @@
|
|||||||
"exifr": "^7.1.3",
|
"exifr": "^7.1.3",
|
||||||
"handlebars": "^4.7.7",
|
"handlebars": "^4.7.7",
|
||||||
"leaflet": "^1.8.0",
|
"leaflet": "^1.8.0",
|
||||||
"lodash": "^4.17.21",
|
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
"luxon": "^3.1.1",
|
"luxon": "^3.1.1",
|
||||||
"socket.io-client": "^4.5.1",
|
"socket.io-client": "^4.5.1",
|
||||||
|
|||||||
Generated
+8
-24
@@ -4,7 +4,7 @@
|
|||||||
* Immich
|
* Immich
|
||||||
* Immich API
|
* Immich API
|
||||||
*
|
*
|
||||||
* The version of the OpenAPI document: 1.43.0
|
* The version of the OpenAPI document: 1.43.1
|
||||||
*
|
*
|
||||||
*
|
*
|
||||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||||
@@ -3729,12 +3729,10 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
|
|||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param {string} assetId
|
* @param {string} assetId
|
||||||
* @param {boolean} [isThumb]
|
|
||||||
* @param {boolean} [isWeb]
|
|
||||||
* @param {*} [options] Override http request option.
|
* @param {*} [options] Override http request option.
|
||||||
* @throws {RequiredError}
|
* @throws {RequiredError}
|
||||||
*/
|
*/
|
||||||
downloadFile: async (assetId: string, isThumb?: boolean, isWeb?: boolean, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
|
downloadFile: async (assetId: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||||
// verify required parameter 'assetId' is not null or undefined
|
// verify required parameter 'assetId' is not null or undefined
|
||||||
assertParamExists('downloadFile', 'assetId', assetId)
|
assertParamExists('downloadFile', 'assetId', assetId)
|
||||||
const localVarPath = `/asset/download/{assetId}`
|
const localVarPath = `/asset/download/{assetId}`
|
||||||
@@ -3754,14 +3752,6 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
|
|||||||
// http bearer authentication required
|
// http bearer authentication required
|
||||||
await setBearerAuthToObject(localVarHeaderParameter, configuration)
|
await setBearerAuthToObject(localVarHeaderParameter, configuration)
|
||||||
|
|
||||||
if (isThumb !== undefined) {
|
|
||||||
localVarQueryParameter['isThumb'] = isThumb;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isWeb !== undefined) {
|
|
||||||
localVarQueryParameter['isWeb'] = isWeb;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||||
@@ -4489,13 +4479,11 @@ export const AssetApiFp = function(configuration?: Configuration) {
|
|||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param {string} assetId
|
* @param {string} assetId
|
||||||
* @param {boolean} [isThumb]
|
|
||||||
* @param {boolean} [isWeb]
|
|
||||||
* @param {*} [options] Override http request option.
|
* @param {*} [options] Override http request option.
|
||||||
* @throws {RequiredError}
|
* @throws {RequiredError}
|
||||||
*/
|
*/
|
||||||
async downloadFile(assetId: string, isThumb?: boolean, isWeb?: boolean, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<object>> {
|
async downloadFile(assetId: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<object>> {
|
||||||
const localVarAxiosArgs = await localVarAxiosParamCreator.downloadFile(assetId, isThumb, isWeb, options);
|
const localVarAxiosArgs = await localVarAxiosParamCreator.downloadFile(assetId, options);
|
||||||
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
@@ -4719,13 +4707,11 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath
|
|||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param {string} assetId
|
* @param {string} assetId
|
||||||
* @param {boolean} [isThumb]
|
|
||||||
* @param {boolean} [isWeb]
|
|
||||||
* @param {*} [options] Override http request option.
|
* @param {*} [options] Override http request option.
|
||||||
* @throws {RequiredError}
|
* @throws {RequiredError}
|
||||||
*/
|
*/
|
||||||
downloadFile(assetId: string, isThumb?: boolean, isWeb?: boolean, options?: any): AxiosPromise<object> {
|
downloadFile(assetId: string, options?: any): AxiosPromise<object> {
|
||||||
return localVarFp.downloadFile(assetId, isThumb, isWeb, options).then((request) => request(axios, basePath));
|
return localVarFp.downloadFile(assetId, options).then((request) => request(axios, basePath));
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
@@ -4939,14 +4925,12 @@ export class AssetApi extends BaseAPI {
|
|||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param {string} assetId
|
* @param {string} assetId
|
||||||
* @param {boolean} [isThumb]
|
|
||||||
* @param {boolean} [isWeb]
|
|
||||||
* @param {*} [options] Override http request option.
|
* @param {*} [options] Override http request option.
|
||||||
* @throws {RequiredError}
|
* @throws {RequiredError}
|
||||||
* @memberof AssetApi
|
* @memberof AssetApi
|
||||||
*/
|
*/
|
||||||
public downloadFile(assetId: string, isThumb?: boolean, isWeb?: boolean, options?: AxiosRequestConfig) {
|
public downloadFile(assetId: string, options?: AxiosRequestConfig) {
|
||||||
return AssetApiFp(this.configuration).downloadFile(assetId, isThumb, isWeb, options).then((request) => request(this.axios, this.basePath));
|
return AssetApiFp(this.configuration).downloadFile(assetId, options).then((request) => request(this.axios, this.basePath));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Generated
+1
-1
@@ -4,7 +4,7 @@
|
|||||||
* Immich
|
* Immich
|
||||||
* Immich API
|
* Immich API
|
||||||
*
|
*
|
||||||
* The version of the OpenAPI document: 1.43.0
|
* The version of the OpenAPI document: 1.43.1
|
||||||
*
|
*
|
||||||
*
|
*
|
||||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||||
|
|||||||
Generated
+1
-1
@@ -4,7 +4,7 @@
|
|||||||
* Immich
|
* Immich
|
||||||
* Immich API
|
* Immich API
|
||||||
*
|
*
|
||||||
* The version of the OpenAPI document: 1.43.0
|
* The version of the OpenAPI document: 1.43.1
|
||||||
*
|
*
|
||||||
*
|
*
|
||||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||||
|
|||||||
Generated
+1
-1
@@ -4,7 +4,7 @@
|
|||||||
* Immich
|
* Immich
|
||||||
* Immich API
|
* Immich API
|
||||||
*
|
*
|
||||||
* The version of the OpenAPI document: 1.43.0
|
* The version of the OpenAPI document: 1.43.1
|
||||||
*
|
*
|
||||||
*
|
*
|
||||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||||
|
|||||||
Generated
+1
-1
@@ -4,7 +4,7 @@
|
|||||||
* Immich
|
* Immich
|
||||||
* Immich API
|
* Immich API
|
||||||
*
|
*
|
||||||
* The version of the OpenAPI document: 1.43.0
|
* The version of the OpenAPI document: 1.43.1
|
||||||
*
|
*
|
||||||
*
|
*
|
||||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||||
|
|||||||
@@ -136,10 +136,8 @@
|
|||||||
|
|
||||||
$downloadAssets[imageFileName] = 0;
|
$downloadAssets[imageFileName] = 0;
|
||||||
|
|
||||||
const { data, status } = await api.assetApi.downloadFile(assetId, false, false, {
|
const { data, status } = await api.assetApi.downloadFile(assetId, {
|
||||||
params: {
|
params: { key },
|
||||||
key
|
|
||||||
},
|
|
||||||
responseType: 'blob',
|
responseType: 'blob',
|
||||||
onDownloadProgress: (progressEvent) => {
|
onDownloadProgress: (progressEvent) => {
|
||||||
if (progressEvent.lengthComputable) {
|
if (progressEvent.lengthComputable) {
|
||||||
|
|||||||
Reference in New Issue
Block a user