Compare commits
18 Commits
v0.6-dev
...
v1.3.1-dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3e918ffd18 | ||
|
|
678ce23c16 | ||
|
|
31c18ff34c | ||
|
|
e407a4fa13 | ||
|
|
be72df70fe | ||
|
|
dbd79f4797 | ||
|
|
f790315d3f | ||
|
|
f1ab700334 | ||
|
|
afc29a67d2 | ||
|
|
ba816babee | ||
|
|
6e0ac79eae | ||
|
|
94514cfeea | ||
|
|
8c7080eaef | ||
|
|
348d395b21 | ||
|
|
2e7e97ea13 | ||
|
|
d71e7ebff1 | ||
|
|
9755936950 | ||
|
|
347052f82f |
3
.github/FUNDING.yml
vendored
Normal file
3
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# These are supported funding model platforms
|
||||||
|
|
||||||
|
github: alextran1502
|
||||||
35
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
35
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
---
|
||||||
|
name: Bug report
|
||||||
|
about: Create a report to help us improve
|
||||||
|
title: "[BUG]"
|
||||||
|
labels: bug
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Describe the bug**
|
||||||
|
A clear and concise description of what the bug is.
|
||||||
|
|
||||||
|
**To Reproduce**
|
||||||
|
Steps to reproduce the behavior:
|
||||||
|
1. Go to '...'
|
||||||
|
2. Click on '....'
|
||||||
|
3. Scroll down to '....'
|
||||||
|
4. See error
|
||||||
|
|
||||||
|
**Expected behavior**
|
||||||
|
A clear and concise description of what you expected to happen.
|
||||||
|
|
||||||
|
**Screenshots**
|
||||||
|
If applicable, add screenshots to help explain your problem.
|
||||||
|
|
||||||
|
**Desktop (please complete the following information):**
|
||||||
|
- OS: [e.g. iOS]
|
||||||
|
|
||||||
|
**Smartphone (please complete the following information):**
|
||||||
|
- Device: [e.g. iPhone6]
|
||||||
|
- OS: [e.g. iOS8.1]
|
||||||
|
- Version [e.g. 22]
|
||||||
|
|
||||||
|
**Additional context**
|
||||||
|
Add any other context about the problem here.
|
||||||
2
.github/workflows/build_push_server.yml
vendored
2
.github/workflows/build_push_server.yml
vendored
@@ -34,7 +34,7 @@ jobs:
|
|||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
# https://github.com/docker/build-push-action#multi-platform-image
|
# https://github.com/docker/build-push-action#multi-platform-image
|
||||||
- name: Build and push Immich
|
- name: Build and push Immich
|
||||||
uses: docker/build-push-action@v2.9.0
|
uses: docker/build-push-action@v2.10.0
|
||||||
with:
|
with:
|
||||||
context: ./server
|
context: ./server
|
||||||
file: ./server/Dockerfile
|
file: ./server/Dockerfile
|
||||||
|
|||||||
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
.DS_Store
|
||||||
13
PR_CHECKLIST.md
Normal file
13
PR_CHECKLIST.md
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
# Deployment checklist for iOS/Android/Server
|
||||||
|
|
||||||
|
[] Up version in [mobile/pubspec.yml](/mobile/pubspec.yaml)
|
||||||
|
|
||||||
|
[] Up version in [docker/docker-compose.yml](/docker/docker-compose.yml) for `immich_server` service
|
||||||
|
|
||||||
|
[] Up version in [docker/docker-compose.gpu.yml](/docker/docker-compose.gpu.yml) for `immich_server` service
|
||||||
|
|
||||||
|
[] Up version in [server/src/constants/server_version.constant.ts](/server/src/constants/server_version.constant.ts)
|
||||||
|
|
||||||
|
[] Up version in iOS Fastlane [/mobile/ios/fastlane/Fastfile](/mobile/ios/fastlane/Fastfile)
|
||||||
|
|
||||||
|
All of the version should be the same.
|
||||||
26
README.md
26
README.md
@@ -1,5 +1,24 @@
|
|||||||
<p align="center">
|
<p align="center">
|
||||||
<img src="design/immich-logo.svg" width="150" title="hover text">
|
<a href="https://opensource.org/licenses/MIT"><img src="https://img.shields.io/badge/license-MIT-green.svg?color=3F51B5&style=for-the-badge&label=License&logoColor=000000&labelColor=ececec" alt="License: MIT"></a>
|
||||||
|
<a href="https://github.com/alextran1502/immich"><img src="https://img.shields.io/github/stars/alextran1502/immich.svg?style=for-the-badge&logo=github&color=3F51B5&label=Stars&logoColor=000000&labelColor=ececec" alt="Star on Github"></a>
|
||||||
|
<a href="https://immichci.little-home.net/viewType.html?buildTypeId=Immich_BuildAndroidAndGetArtifact&guest=1">
|
||||||
|
<img src="https://img.shields.io/teamcity/http/immichci.little-home.net/s/Immich_BuildAndroidAndGetArtifact.svg?style=for-the-badge&label=Android&logo=teamcity&logoColor=000000&labelColor=ececec" alt="Android Build"/>
|
||||||
|
</a>
|
||||||
|
<a href="https://immichci.little-home.net/viewType.html?buildTypeId=Immich_BuildAndPublishIOSToTestFlight&guest=1">
|
||||||
|
<img src="https://img.shields.io/teamcity/http/immichci.little-home.net/s/Immich_BuildAndPublishIOSToTestFlight.svg?style=for-the-badge&label=iOS&logo=teamcity&logoColor=000000&labelColor=ececec" alt="iOS Build"/>
|
||||||
|
</a>
|
||||||
|
<a href="https://actions-badge.atrox.dev/alextran1502/immich/goto?ref=main">
|
||||||
|
<img alt="Build Status" src="https://img.shields.io/endpoint.svg?url=https%3A%2F%2Factions-badge.atrox.dev%2Falextran1502%2Fimmich%2Fbadge%3Fref%3Dmain&style=for-the-badge&label=Server Docker&logo=docker&labelColor=ececec" />
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<br/>
|
||||||
|
<br/>
|
||||||
|
<br/>
|
||||||
|
<br/>
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<img src="design/immich-logo.svg" width="200" title="Immich Logo">
|
||||||
|
</p>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
# Immich
|
# Immich
|
||||||
@@ -28,8 +47,8 @@ This project is under heavy development, there will be continous functions, feat
|
|||||||
|
|
||||||
# Features
|
# Features
|
||||||
|
|
||||||
- Upload assets(videos/images).
|
- Upload and view assets(videos/images).
|
||||||
- View assets.
|
- Multi-user supported.
|
||||||
- Quick navigation with drag scroll bar.
|
- Quick navigation with drag scroll bar.
|
||||||
- Auto Backup.
|
- Auto Backup.
|
||||||
- Support HEIC/HEIF Backup.
|
- Support HEIC/HEIF Backup.
|
||||||
@@ -40,6 +59,7 @@ This project is under heavy development, there will be continous functions, feat
|
|||||||
- Upload assets from your local computer/server using [immich cli tools](https://www.npmjs.com/package/immich)
|
- Upload assets from your local computer/server using [immich cli tools](https://www.npmjs.com/package/immich)
|
||||||
- [Optional] Reserve geocoding using Mapbox (Generous free-tier of 100,000 search/month)
|
- [Optional] Reserve geocoding using Mapbox (Generous free-tier of 100,000 search/month)
|
||||||
- Show asset's location information on map (OpenStreetMap).
|
- Show asset's location information on map (OpenStreetMap).
|
||||||
|
- Show curated places on the search page
|
||||||
|
|
||||||
# Development
|
# Development
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ version: "3.8"
|
|||||||
|
|
||||||
services:
|
services:
|
||||||
immich_server:
|
immich_server:
|
||||||
image: immich-server-dev:1.0.0
|
image: immich-server-dev:1.3.0
|
||||||
build:
|
build:
|
||||||
context: ../server
|
context: ../server
|
||||||
target: development
|
target: development
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ version: "3.8"
|
|||||||
|
|
||||||
services:
|
services:
|
||||||
immich_server:
|
immich_server:
|
||||||
image: immich-server-dev:1.0.0
|
image: immich-server-dev:1.3.0
|
||||||
build:
|
build:
|
||||||
context: ../server
|
context: ../server
|
||||||
target: development
|
target: development
|
||||||
|
|||||||
@@ -13,6 +13,9 @@
|
|||||||
## CPU BUILD
|
## CPU BUILD
|
||||||
FROM python:3.8 as cpu
|
FROM python:3.8 as cpu
|
||||||
|
|
||||||
|
RUN apt-get update
|
||||||
|
RUN apt-get install ffmpeg libsm6 libxext6 -y
|
||||||
|
|
||||||
WORKDIR /code
|
WORKDIR /code
|
||||||
|
|
||||||
COPY ./requirements.txt /code/requirements.txt
|
COPY ./requirements.txt /code/requirements.txt
|
||||||
|
|||||||
@@ -2,15 +2,20 @@ from tensorflow.keras.applications import InceptionV3
|
|||||||
from tensorflow.keras.applications.inception_v3 import preprocess_input, decode_predictions
|
from tensorflow.keras.applications.inception_v3 import preprocess_input, decode_predictions
|
||||||
from tensorflow.keras.preprocessing import image
|
from tensorflow.keras.preprocessing import image
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
from PIL import Image
|
||||||
|
import cv2
|
||||||
IMG_SIZE = 299
|
IMG_SIZE = 299
|
||||||
PREDICTION_MODEL = InceptionV3(weights='imagenet')
|
PREDICTION_MODEL = InceptionV3(weights='imagenet')
|
||||||
|
|
||||||
|
|
||||||
def classify_image(image_path: str):
|
def classify_image(image_path: str):
|
||||||
img_path = f'./app/{image_path}'
|
img_path = f'./app/{image_path}'
|
||||||
img = image.load_img(img_path, target_size=(IMG_SIZE, IMG_SIZE))
|
# img = image.load_img(img_path, target_size=(IMG_SIZE, IMG_SIZE))
|
||||||
x = image.img_to_array(img)
|
|
||||||
|
target_image = cv2.imread(img_path, cv2.IMREAD_UNCHANGED)
|
||||||
|
resized_target_image = cv2.resize(target_image, (IMG_SIZE, IMG_SIZE))
|
||||||
|
|
||||||
|
x = image.img_to_array(resized_target_image)
|
||||||
x = np.expand_dims(x, axis=0)
|
x = np.expand_dims(x, axis=0)
|
||||||
x = preprocess_input(x)
|
x = preprocess_input(x)
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
opencv-python==4.5.5.64
|
||||||
fastapi>=0.68.0,<0.69.0
|
fastapi>=0.68.0,<0.69.0
|
||||||
pydantic>=1.8.0,<2.0.0
|
pydantic>=1.8.0,<2.0.0
|
||||||
uvicorn>=0.15.0,<0.16.0
|
uvicorn>=0.15.0,<0.16.0
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
PODS:
|
PODS:
|
||||||
- device_info_plus (0.0.1):
|
|
||||||
- Flutter
|
|
||||||
- Flutter (1.0.0)
|
- Flutter (1.0.0)
|
||||||
- flutter_udid (0.0.1):
|
- flutter_udid (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
@@ -11,6 +9,8 @@ PODS:
|
|||||||
- FMDB (2.7.5):
|
- FMDB (2.7.5):
|
||||||
- FMDB/standard (= 2.7.5)
|
- FMDB/standard (= 2.7.5)
|
||||||
- FMDB/standard (2.7.5)
|
- FMDB/standard (2.7.5)
|
||||||
|
- package_info_plus (0.4.5):
|
||||||
|
- Flutter
|
||||||
- path_provider_ios (0.0.1):
|
- path_provider_ios (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- photo_manager (1.0.0):
|
- photo_manager (1.0.0):
|
||||||
@@ -27,10 +27,10 @@ PODS:
|
|||||||
- Flutter
|
- Flutter
|
||||||
|
|
||||||
DEPENDENCIES:
|
DEPENDENCIES:
|
||||||
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
|
|
||||||
- Flutter (from `Flutter`)
|
- Flutter (from `Flutter`)
|
||||||
- flutter_udid (from `.symlinks/plugins/flutter_udid/ios`)
|
- flutter_udid (from `.symlinks/plugins/flutter_udid/ios`)
|
||||||
- fluttertoast (from `.symlinks/plugins/fluttertoast/ios`)
|
- fluttertoast (from `.symlinks/plugins/fluttertoast/ios`)
|
||||||
|
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
|
||||||
- path_provider_ios (from `.symlinks/plugins/path_provider_ios/ios`)
|
- path_provider_ios (from `.symlinks/plugins/path_provider_ios/ios`)
|
||||||
- photo_manager (from `.symlinks/plugins/photo_manager/ios`)
|
- photo_manager (from `.symlinks/plugins/photo_manager/ios`)
|
||||||
- sqflite (from `.symlinks/plugins/sqflite/ios`)
|
- sqflite (from `.symlinks/plugins/sqflite/ios`)
|
||||||
@@ -44,14 +44,14 @@ SPEC REPOS:
|
|||||||
- Toast
|
- Toast
|
||||||
|
|
||||||
EXTERNAL SOURCES:
|
EXTERNAL SOURCES:
|
||||||
device_info_plus:
|
|
||||||
:path: ".symlinks/plugins/device_info_plus/ios"
|
|
||||||
Flutter:
|
Flutter:
|
||||||
:path: Flutter
|
:path: Flutter
|
||||||
flutter_udid:
|
flutter_udid:
|
||||||
:path: ".symlinks/plugins/flutter_udid/ios"
|
:path: ".symlinks/plugins/flutter_udid/ios"
|
||||||
fluttertoast:
|
fluttertoast:
|
||||||
:path: ".symlinks/plugins/fluttertoast/ios"
|
:path: ".symlinks/plugins/fluttertoast/ios"
|
||||||
|
package_info_plus:
|
||||||
|
:path: ".symlinks/plugins/package_info_plus/ios"
|
||||||
path_provider_ios:
|
path_provider_ios:
|
||||||
:path: ".symlinks/plugins/path_provider_ios/ios"
|
:path: ".symlinks/plugins/path_provider_ios/ios"
|
||||||
photo_manager:
|
photo_manager:
|
||||||
@@ -64,11 +64,11 @@ EXTERNAL SOURCES:
|
|||||||
:path: ".symlinks/plugins/wakelock/ios"
|
:path: ".symlinks/plugins/wakelock/ios"
|
||||||
|
|
||||||
SPEC CHECKSUMS:
|
SPEC CHECKSUMS:
|
||||||
device_info_plus: e5c5da33f982a436e103237c0c85f9031142abed
|
|
||||||
Flutter: 50d75fe2f02b26cc09d224853bb45737f8b3214a
|
Flutter: 50d75fe2f02b26cc09d224853bb45737f8b3214a
|
||||||
flutter_udid: 0848809dbed4c055175747ae6a45a8b4f6771e1c
|
flutter_udid: 0848809dbed4c055175747ae6a45a8b4f6771e1c
|
||||||
fluttertoast: 6122fa75143e992b1d3470f61000f591a798cc58
|
fluttertoast: 6122fa75143e992b1d3470f61000f591a798cc58
|
||||||
FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a
|
FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a
|
||||||
|
package_info_plus: 6c92f08e1f853dc01228d6f553146438dafcd14e
|
||||||
path_provider_ios: 7d7ce634493af4477d156294792024ec3485acd5
|
path_provider_ios: 7d7ce634493af4477d156294792024ec3485acd5
|
||||||
photo_manager: 84fa94fbeb82e607333ea9a13c43b58e0903a463
|
photo_manager: 84fa94fbeb82e607333ea9a13c43b58e0903a463
|
||||||
SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c
|
SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c
|
||||||
|
|||||||
@@ -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 = "$(FLUTTER_BUILD_NUMBER)";
|
CURRENT_PROJECT_VERSION = 2;
|
||||||
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 = "$(FLUTTER_BUILD_NUMBER)";
|
CURRENT_PROJECT_VERSION = 2;
|
||||||
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 = "$(FLUTTER_BUILD_NUMBER)";
|
CURRENT_PROJECT_VERSION = 2;
|
||||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
<key>CFBundleSignature</key>
|
<key>CFBundleSignature</key>
|
||||||
<string>????</string>
|
<string>????</string>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
<string>2</string>
|
||||||
<key>LSRequiresIPhoneOS</key>
|
<key>LSRequiresIPhoneOS</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>MGLMapboxMetricsEnabledSettingShownInApp</key>
|
<key>MGLMapboxMetricsEnabledSettingShownInApp</key>
|
||||||
@@ -60,5 +60,7 @@
|
|||||||
<true/>
|
<true/>
|
||||||
<key>io.flutter.embedded_views_preview</key>
|
<key>io.flutter.embedded_views_preview</key>
|
||||||
<true/>
|
<true/>
|
||||||
|
<key>ITSAppUsesNonExemptEncryption</key>
|
||||||
|
<false/>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
<dict/>
|
<dict>
|
||||||
|
<key>aps-environment</key>
|
||||||
|
<string>development</string>
|
||||||
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|||||||
@@ -16,8 +16,27 @@
|
|||||||
default_platform(:ios)
|
default_platform(:ios)
|
||||||
|
|
||||||
platform :ios do
|
platform :ios do
|
||||||
desc "iOS deployment"
|
desc "iOS Beta"
|
||||||
lane :beta do
|
lane :beta do
|
||||||
|
increment_build_number({
|
||||||
|
build_number: latest_testflight_build_number + 1
|
||||||
|
})
|
||||||
|
build_app(scheme: "Runner",
|
||||||
|
workspace: "Runner.xcworkspace",
|
||||||
|
xcargs: "-allowProvisioningUpdates")
|
||||||
|
upload_to_testflight(
|
||||||
|
skip_waiting_for_build_processing: true
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
desc "iOS Release"
|
||||||
|
lane :release do
|
||||||
|
increment_version_number(
|
||||||
|
version_number: "1.3.0"
|
||||||
|
)
|
||||||
|
increment_build_number({
|
||||||
|
build_number: latest_testflight_build_number + 1
|
||||||
|
})
|
||||||
build_app(scheme: "Runner",
|
build_app(scheme: "Runner",
|
||||||
workspace: "Runner.xcworkspace",
|
workspace: "Runner.xcworkspace",
|
||||||
xcargs: "-allowProvisioningUpdates")
|
xcargs: "-allowProvisioningUpdates")
|
||||||
|
|||||||
@@ -5,17 +5,27 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.001066">
|
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000332">
|
||||||
|
|
||||||
</testcase>
|
</testcase>
|
||||||
|
|
||||||
|
|
||||||
<testcase classname="fastlane.lanes" name="1: build_app" time="71.433647">
|
<testcase classname="fastlane.lanes" name="1: latest_testflight_build_number" time="4.608292">
|
||||||
|
|
||||||
</testcase>
|
</testcase>
|
||||||
|
|
||||||
|
|
||||||
<testcase classname="fastlane.lanes" name="2: upload_to_testflight" time="104.299383">
|
<testcase classname="fastlane.lanes" name="2: increment_build_number" time="0.747162">
|
||||||
|
|
||||||
|
</testcase>
|
||||||
|
|
||||||
|
|
||||||
|
<testcase classname="fastlane.lanes" name="3: build_app" time="88.727281">
|
||||||
|
|
||||||
|
</testcase>
|
||||||
|
|
||||||
|
|
||||||
|
<testcase classname="fastlane.lanes" name="4: upload_to_testflight" time="7.79397">
|
||||||
|
|
||||||
</testcase>
|
</testcase>
|
||||||
|
|
||||||
|
|||||||
@@ -4,8 +4,10 @@ import 'package:hive_flutter/hive_flutter.dart';
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/modules/home/providers/asset.provider.dart';
|
import 'package:immich_mobile/modules/home/providers/asset.provider.dart';
|
||||||
import 'package:immich_mobile/routing/router.dart';
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
|
import 'package:immich_mobile/routing/tab_navigation_observer.dart';
|
||||||
import 'package:immich_mobile/shared/providers/app_state.provider.dart';
|
import 'package:immich_mobile/shared/providers/app_state.provider.dart';
|
||||||
import 'package:immich_mobile/shared/providers/backup.provider.dart';
|
import 'package:immich_mobile/shared/providers/backup.provider.dart';
|
||||||
|
import 'package:immich_mobile/shared/providers/server_info.provider.dart';
|
||||||
import 'package:immich_mobile/shared/providers/websocket.provider.dart';
|
import 'package:immich_mobile/shared/providers/websocket.provider.dart';
|
||||||
import 'constants/hive_box.dart';
|
import 'constants/hive_box.dart';
|
||||||
import 'package:google_fonts/google_fonts.dart';
|
import 'package:google_fonts/google_fonts.dart';
|
||||||
@@ -42,7 +44,10 @@ class _ImmichAppState extends ConsumerState<ImmichApp> with WidgetsBindingObserv
|
|||||||
ref.watch(backupProvider.notifier).resumeBackup();
|
ref.watch(backupProvider.notifier).resumeBackup();
|
||||||
ref.watch(websocketProvider.notifier).connect();
|
ref.watch(websocketProvider.notifier).connect();
|
||||||
ref.watch(assetProvider.notifier).getAllAsset();
|
ref.watch(assetProvider.notifier).getAllAsset();
|
||||||
|
ref.watch(serverInfoProvider.notifier).getServerVersion();
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case AppLifecycleState.inactive:
|
case AppLifecycleState.inactive:
|
||||||
debugPrint("[APP STATE] inactive");
|
debugPrint("[APP STATE] inactive");
|
||||||
ref.watch(appStateProvider.notifier).state = AppStateEnum.inactive;
|
ref.watch(appStateProvider.notifier).state = AppStateEnum.inactive;
|
||||||
@@ -50,10 +55,12 @@ class _ImmichAppState extends ConsumerState<ImmichApp> with WidgetsBindingObserv
|
|||||||
ref.watch(backupProvider.notifier).cancelBackup();
|
ref.watch(backupProvider.notifier).cancelBackup();
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case AppLifecycleState.paused:
|
case AppLifecycleState.paused:
|
||||||
debugPrint("[APP STATE] paused");
|
debugPrint("[APP STATE] paused");
|
||||||
ref.watch(appStateProvider.notifier).state = AppStateEnum.paused;
|
ref.watch(appStateProvider.notifier).state = AppStateEnum.paused;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case AppLifecycleState.detached:
|
case AppLifecycleState.detached:
|
||||||
debugPrint("[APP STATE] detached");
|
debugPrint("[APP STATE] detached");
|
||||||
ref.watch(appStateProvider.notifier).state = AppStateEnum.detached;
|
ref.watch(appStateProvider.notifier).state = AppStateEnum.detached;
|
||||||
@@ -100,7 +107,7 @@ class _ImmichAppState extends ConsumerState<ImmichApp> with WidgetsBindingObserv
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
routeInformationParser: _immichRouter.defaultRouteParser(),
|
routeInformationParser: _immichRouter.defaultRouteParser(),
|
||||||
routerDelegate: _immichRouter.delegate(),
|
routerDelegate: _immichRouter.delegate(navigatorObservers: () => [TabNavigationObserver(ref: ref)]),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,9 @@ import 'package:immich_mobile/modules/login/providers/authentication.provider.da
|
|||||||
|
|
||||||
import 'package:immich_mobile/routing/router.dart';
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
import 'package:immich_mobile/shared/models/backup_state.model.dart';
|
import 'package:immich_mobile/shared/models/backup_state.model.dart';
|
||||||
|
import 'package:immich_mobile/shared/models/server_info_state.model.dart';
|
||||||
import 'package:immich_mobile/shared/providers/backup.provider.dart';
|
import 'package:immich_mobile/shared/providers/backup.provider.dart';
|
||||||
|
import 'package:immich_mobile/shared/providers/server_info.provider.dart';
|
||||||
|
|
||||||
class ImmichSliverAppBar extends ConsumerWidget {
|
class ImmichSliverAppBar extends ConsumerWidget {
|
||||||
const ImmichSliverAppBar({
|
const ImmichSliverAppBar({
|
||||||
@@ -21,6 +23,8 @@ class ImmichSliverAppBar extends ConsumerWidget {
|
|||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final BackUpState _backupState = ref.watch(backupProvider);
|
final BackUpState _backupState = ref.watch(backupProvider);
|
||||||
bool _isEnableAutoBackup = ref.watch(authenticationProvider).deviceInfo.isAutoBackup;
|
bool _isEnableAutoBackup = ref.watch(authenticationProvider).deviceInfo.isAutoBackup;
|
||||||
|
final ServerInfoState _serverInfoState = ref.watch(serverInfoProvider);
|
||||||
|
|
||||||
return SliverAppBar(
|
return SliverAppBar(
|
||||||
centerTitle: true,
|
centerTitle: true,
|
||||||
floating: true,
|
floating: true,
|
||||||
@@ -30,12 +34,46 @@ class ImmichSliverAppBar extends ConsumerWidget {
|
|||||||
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(5))),
|
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(5))),
|
||||||
leading: Builder(
|
leading: Builder(
|
||||||
builder: (BuildContext context) {
|
builder: (BuildContext context) {
|
||||||
return IconButton(
|
return Stack(
|
||||||
icon: const Icon(Icons.account_circle_rounded),
|
children: [
|
||||||
onPressed: () {
|
Positioned(
|
||||||
Scaffold.of(context).openDrawer();
|
top: 5,
|
||||||
},
|
child: IconButton(
|
||||||
tooltip: MaterialLocalizations.of(context).openAppDrawerTooltip,
|
splashRadius: 25,
|
||||||
|
icon: const Icon(
|
||||||
|
Icons.account_circle_rounded,
|
||||||
|
size: 30,
|
||||||
|
),
|
||||||
|
onPressed: () {
|
||||||
|
Scaffold.of(context).openDrawer();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
_serverInfoState.isVersionMismatch
|
||||||
|
? Positioned(
|
||||||
|
bottom: 12,
|
||||||
|
right: 12,
|
||||||
|
child: GestureDetector(
|
||||||
|
onTap: () => Scaffold.of(context).openDrawer(),
|
||||||
|
child: Material(
|
||||||
|
color: Colors.grey[200],
|
||||||
|
elevation: 1,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(50.0),
|
||||||
|
),
|
||||||
|
child: const Padding(
|
||||||
|
padding: EdgeInsets.all(2.0),
|
||||||
|
child: Icon(
|
||||||
|
Icons.info,
|
||||||
|
color: Color.fromARGB(255, 243, 188, 106),
|
||||||
|
size: 15,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: Container(),
|
||||||
|
],
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,18 +1,40 @@
|
|||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/modules/home/providers/asset.provider.dart';
|
import 'package:immich_mobile/modules/home/providers/asset.provider.dart';
|
||||||
import 'package:immich_mobile/modules/login/models/authentication_state.model.dart';
|
import 'package:immich_mobile/modules/login/models/authentication_state.model.dart';
|
||||||
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
|
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
|
||||||
|
import 'package:immich_mobile/shared/models/server_info_state.model.dart';
|
||||||
import 'package:immich_mobile/shared/providers/backup.provider.dart';
|
import 'package:immich_mobile/shared/providers/backup.provider.dart';
|
||||||
|
import 'package:immich_mobile/shared/providers/server_info.provider.dart';
|
||||||
import 'package:immich_mobile/shared/providers/websocket.provider.dart';
|
import 'package:immich_mobile/shared/providers/websocket.provider.dart';
|
||||||
|
import 'package:package_info_plus/package_info_plus.dart';
|
||||||
|
|
||||||
class ProfileDrawer extends ConsumerWidget {
|
class ProfileDrawer extends HookConsumerWidget {
|
||||||
const ProfileDrawer({Key? key}) : super(key: key);
|
const ProfileDrawer({Key? key}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
AuthenticationState _authState = ref.watch(authenticationProvider);
|
AuthenticationState _authState = ref.watch(authenticationProvider);
|
||||||
|
ServerInfoState _serverInfoState = ref.watch(serverInfoProvider);
|
||||||
|
|
||||||
|
final appInfo = useState({});
|
||||||
|
|
||||||
|
_getPackageInfo() async {
|
||||||
|
PackageInfo packageInfo = await PackageInfo.fromPlatform();
|
||||||
|
|
||||||
|
appInfo.value = {
|
||||||
|
"version": packageInfo.version,
|
||||||
|
"buildNumber": packageInfo.buildNumber,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() {
|
||||||
|
_getPackageInfo();
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}, []);
|
||||||
|
|
||||||
return Drawer(
|
return Drawer(
|
||||||
shape: const RoundedRectangleBorder(
|
shape: const RoundedRectangleBorder(
|
||||||
@@ -21,50 +43,125 @@ class ProfileDrawer extends ConsumerWidget {
|
|||||||
bottomRight: Radius.circular(5),
|
bottomRight: Radius.circular(5),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: ListView(
|
child: Column(
|
||||||
padding: EdgeInsets.zero,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
DrawerHeader(
|
ListView(
|
||||||
decoration: BoxDecoration(
|
shrinkWrap: true,
|
||||||
color: Colors.grey[200],
|
padding: EdgeInsets.zero,
|
||||||
),
|
children: [
|
||||||
child: Column(
|
DrawerHeader(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
decoration: BoxDecoration(
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
color: Colors.grey[200],
|
||||||
children: [
|
|
||||||
const Image(
|
|
||||||
image: AssetImage('assets/immich-logo-no-outline.png'),
|
|
||||||
width: 50,
|
|
||||||
filterQuality: FilterQuality.high,
|
|
||||||
),
|
),
|
||||||
const Padding(padding: EdgeInsets.all(8)),
|
child: Column(
|
||||||
Text(
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
_authState.userEmail,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
style: TextStyle(color: Theme.of(context).primaryColor, fontWeight: FontWeight.bold),
|
children: [
|
||||||
)
|
const Image(
|
||||||
],
|
image: AssetImage('assets/immich-logo-no-outline.png'),
|
||||||
),
|
width: 50,
|
||||||
),
|
filterQuality: FilterQuality.high,
|
||||||
ListTile(
|
),
|
||||||
tileColor: Colors.grey[100],
|
const Padding(padding: EdgeInsets.all(8)),
|
||||||
leading: const Icon(
|
Text(
|
||||||
Icons.logout_rounded,
|
_authState.userEmail,
|
||||||
color: Colors.black54,
|
style: TextStyle(color: Theme.of(context).primaryColor, fontWeight: FontWeight.bold),
|
||||||
),
|
)
|
||||||
title: const Text(
|
],
|
||||||
"Sign Out",
|
),
|
||||||
style: TextStyle(color: Colors.black54, fontSize: 14),
|
),
|
||||||
),
|
ListTile(
|
||||||
onTap: () async {
|
tileColor: Colors.grey[100],
|
||||||
bool res = await ref.read(authenticationProvider.notifier).logout();
|
leading: const Icon(
|
||||||
|
Icons.logout_rounded,
|
||||||
|
color: Colors.black54,
|
||||||
|
),
|
||||||
|
title: const Text(
|
||||||
|
"Sign Out",
|
||||||
|
style: TextStyle(color: Colors.black54, fontSize: 14),
|
||||||
|
),
|
||||||
|
onTap: () async {
|
||||||
|
bool res = await ref.read(authenticationProvider.notifier).logout();
|
||||||
|
|
||||||
if (res) {
|
if (res) {
|
||||||
ref.watch(backupProvider.notifier).cancelBackup();
|
ref.watch(backupProvider.notifier).cancelBackup();
|
||||||
ref.watch(assetProvider.notifier).clearAllAsset();
|
ref.watch(assetProvider.notifier).clearAllAsset();
|
||||||
ref.watch(websocketProvider.notifier).disconnect();
|
ref.watch(websocketProvider.notifier).disconnect();
|
||||||
AutoRouter.of(context).popUntilRoot();
|
AutoRouter.of(context).popUntilRoot();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(8.0),
|
||||||
|
child: Card(
|
||||||
|
color: Colors.grey[100],
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 8),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(8.0),
|
||||||
|
child: Text(
|
||||||
|
_serverInfoState.isVersionMismatch
|
||||||
|
? _serverInfoState.versionMismatchErrorMessage
|
||||||
|
: "Client and Server are up-to-date",
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style:
|
||||||
|
TextStyle(fontSize: 11, color: Theme.of(context).primaryColor, fontWeight: FontWeight.w600),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Divider(),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
"App Version",
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 11,
|
||||||
|
color: Colors.grey[500],
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
"${appInfo.value["version"]} build.${appInfo.value["buildNumber"]}",
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 11,
|
||||||
|
color: Colors.grey[500],
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const Divider(),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
"Server Version",
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 11,
|
||||||
|
color: Colors.grey[500],
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
"${_serverInfoState.serverVersion.major}.${_serverInfoState.serverVersion.minor}.${_serverInfoState.serverVersion.patch}",
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 11,
|
||||||
|
color: Colors.grey[500],
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import 'package:immich_mobile/modules/home/ui/immich_sliver_appbar.dart';
|
|||||||
import 'package:immich_mobile/modules/home/ui/monthly_title_text.dart';
|
import 'package:immich_mobile/modules/home/ui/monthly_title_text.dart';
|
||||||
import 'package:immich_mobile/modules/home/ui/profile_drawer.dart';
|
import 'package:immich_mobile/modules/home/ui/profile_drawer.dart';
|
||||||
import 'package:immich_mobile/modules/home/providers/asset.provider.dart';
|
import 'package:immich_mobile/modules/home/providers/asset.provider.dart';
|
||||||
|
import 'package:immich_mobile/shared/providers/server_info.provider.dart';
|
||||||
import 'package:immich_mobile/shared/providers/websocket.provider.dart';
|
import 'package:immich_mobile/shared/providers/websocket.provider.dart';
|
||||||
import 'package:sliver_tools/sliver_tools.dart';
|
import 'package:sliver_tools/sliver_tools.dart';
|
||||||
|
|
||||||
@@ -28,6 +29,7 @@ class HomePage extends HookConsumerWidget {
|
|||||||
useEffect(() {
|
useEffect(() {
|
||||||
ref.read(websocketProvider.notifier).connect();
|
ref.read(websocketProvider.notifier).connect();
|
||||||
ref.read(assetProvider.notifier).getAllAsset();
|
ref.read(assetProvider.notifier).getAllAsset();
|
||||||
|
ref.watch(serverInfoProvider.notifier).getServerVersion();
|
||||||
return null;
|
return null;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|||||||
79
mobile/lib/modules/search/models/curated_location.model.dart
Normal file
79
mobile/lib/modules/search/models/curated_location.model.dart
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
class CuratedLocation {
|
||||||
|
final String id;
|
||||||
|
final String city;
|
||||||
|
final String resizePath;
|
||||||
|
final String deviceAssetId;
|
||||||
|
final String deviceId;
|
||||||
|
|
||||||
|
CuratedLocation({
|
||||||
|
required this.id,
|
||||||
|
required this.city,
|
||||||
|
required this.resizePath,
|
||||||
|
required this.deviceAssetId,
|
||||||
|
required this.deviceId,
|
||||||
|
});
|
||||||
|
|
||||||
|
CuratedLocation copyWith({
|
||||||
|
String? id,
|
||||||
|
String? city,
|
||||||
|
String? resizePath,
|
||||||
|
String? deviceAssetId,
|
||||||
|
String? deviceId,
|
||||||
|
}) {
|
||||||
|
return CuratedLocation(
|
||||||
|
id: id ?? this.id,
|
||||||
|
city: city ?? this.city,
|
||||||
|
resizePath: resizePath ?? this.resizePath,
|
||||||
|
deviceAssetId: deviceAssetId ?? this.deviceAssetId,
|
||||||
|
deviceId: deviceId ?? this.deviceId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> toMap() {
|
||||||
|
return {
|
||||||
|
'id': id,
|
||||||
|
'city': city,
|
||||||
|
'resizePath': resizePath,
|
||||||
|
'deviceAssetId': deviceAssetId,
|
||||||
|
'deviceId': deviceId,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
factory CuratedLocation.fromMap(Map<String, dynamic> map) {
|
||||||
|
return CuratedLocation(
|
||||||
|
id: map['id'] ?? '',
|
||||||
|
city: map['city'] ?? '',
|
||||||
|
resizePath: map['resizePath'] ?? '',
|
||||||
|
deviceAssetId: map['deviceAssetId'] ?? '',
|
||||||
|
deviceId: map['deviceId'] ?? '',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
String toJson() => json.encode(toMap());
|
||||||
|
|
||||||
|
factory CuratedLocation.fromJson(String source) => CuratedLocation.fromMap(json.decode(source));
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'CuratedLocation(id: $id, city: $city, resizePath: $resizePath, deviceAssetId: $deviceAssetId, deviceId: $deviceId)';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
if (identical(this, other)) return true;
|
||||||
|
|
||||||
|
return other is CuratedLocation &&
|
||||||
|
other.id == id &&
|
||||||
|
other.city == city &&
|
||||||
|
other.resizePath == resizePath &&
|
||||||
|
other.deviceAssetId == deviceAssetId &&
|
||||||
|
other.deviceId == deviceId;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode {
|
||||||
|
return id.hashCode ^ city.hashCode ^ resizePath.hashCode ^ deviceAssetId.hashCode ^ deviceId.hashCode;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
|
|
||||||
|
class SearchPageState {
|
||||||
|
final String searchTerm;
|
||||||
|
final bool isSearchEnabled;
|
||||||
|
final List<String> searchSuggestion;
|
||||||
|
final List<String> userSuggestedSearchTerms;
|
||||||
|
|
||||||
|
SearchPageState({
|
||||||
|
required this.searchTerm,
|
||||||
|
required this.isSearchEnabled,
|
||||||
|
required this.searchSuggestion,
|
||||||
|
required this.userSuggestedSearchTerms,
|
||||||
|
});
|
||||||
|
|
||||||
|
SearchPageState copyWith({
|
||||||
|
String? searchTerm,
|
||||||
|
bool? isSearchEnabled,
|
||||||
|
List<String>? searchSuggestion,
|
||||||
|
List<String>? userSuggestedSearchTerms,
|
||||||
|
}) {
|
||||||
|
return SearchPageState(
|
||||||
|
searchTerm: searchTerm ?? this.searchTerm,
|
||||||
|
isSearchEnabled: isSearchEnabled ?? this.isSearchEnabled,
|
||||||
|
searchSuggestion: searchSuggestion ?? this.searchSuggestion,
|
||||||
|
userSuggestedSearchTerms: userSuggestedSearchTerms ?? this.userSuggestedSearchTerms,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> toMap() {
|
||||||
|
return {
|
||||||
|
'searchTerm': searchTerm,
|
||||||
|
'isSearchEnabled': isSearchEnabled,
|
||||||
|
'searchSuggestion': searchSuggestion,
|
||||||
|
'userSuggestedSearchTerms': userSuggestedSearchTerms,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
factory SearchPageState.fromMap(Map<String, dynamic> map) {
|
||||||
|
return SearchPageState(
|
||||||
|
searchTerm: map['searchTerm'] ?? '',
|
||||||
|
isSearchEnabled: map['isSearchEnabled'] ?? false,
|
||||||
|
searchSuggestion: List<String>.from(map['searchSuggestion']),
|
||||||
|
userSuggestedSearchTerms: List<String>.from(map['userSuggestedSearchTerms']),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
String toJson() => json.encode(toMap());
|
||||||
|
|
||||||
|
factory SearchPageState.fromJson(String source) => SearchPageState.fromMap(json.decode(source));
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'SearchPageState(searchTerm: $searchTerm, isSearchEnabled: $isSearchEnabled, searchSuggestion: $searchSuggestion, userSuggestedSearchTerms: $userSuggestedSearchTerms)';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
if (identical(this, other)) return true;
|
||||||
|
final listEquals = const DeepCollectionEquality().equals;
|
||||||
|
|
||||||
|
return other is SearchPageState &&
|
||||||
|
other.searchTerm == searchTerm &&
|
||||||
|
other.isSearchEnabled == isSearchEnabled &&
|
||||||
|
listEquals(other.searchSuggestion, searchSuggestion) &&
|
||||||
|
listEquals(other.userSuggestedSearchTerms, userSuggestedSearchTerms);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode {
|
||||||
|
return searchTerm.hashCode ^
|
||||||
|
isSearchEnabled.hashCode ^
|
||||||
|
searchSuggestion.hashCode ^
|
||||||
|
userSuggestedSearchTerms.hashCode;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,32 +1,28 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|
||||||
|
|
||||||
import 'package:immich_mobile/modules/search/services/search.service.dart';
|
|
||||||
import 'package:immich_mobile/shared/models/immich_asset.model.dart';
|
import 'package:immich_mobile/shared/models/immich_asset.model.dart';
|
||||||
import 'package:intl/intl.dart';
|
|
||||||
|
|
||||||
class SearchresultPageState {
|
class SearchResultPageState {
|
||||||
final bool isLoading;
|
final bool isLoading;
|
||||||
final bool isSuccess;
|
final bool isSuccess;
|
||||||
final bool isError;
|
final bool isError;
|
||||||
final List<ImmichAsset> searchResult;
|
final List<ImmichAsset> searchResult;
|
||||||
|
|
||||||
SearchresultPageState({
|
SearchResultPageState({
|
||||||
required this.isLoading,
|
required this.isLoading,
|
||||||
required this.isSuccess,
|
required this.isSuccess,
|
||||||
required this.isError,
|
required this.isError,
|
||||||
required this.searchResult,
|
required this.searchResult,
|
||||||
});
|
});
|
||||||
|
|
||||||
SearchresultPageState copyWith({
|
SearchResultPageState copyWith({
|
||||||
bool? isLoading,
|
bool? isLoading,
|
||||||
bool? isSuccess,
|
bool? isSuccess,
|
||||||
bool? isError,
|
bool? isError,
|
||||||
List<ImmichAsset>? searchResult,
|
List<ImmichAsset>? searchResult,
|
||||||
}) {
|
}) {
|
||||||
return SearchresultPageState(
|
return SearchResultPageState(
|
||||||
isLoading: isLoading ?? this.isLoading,
|
isLoading: isLoading ?? this.isLoading,
|
||||||
isSuccess: isSuccess ?? this.isSuccess,
|
isSuccess: isSuccess ?? this.isSuccess,
|
||||||
isError: isError ?? this.isError,
|
isError: isError ?? this.isError,
|
||||||
@@ -43,8 +39,8 @@ class SearchresultPageState {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
factory SearchresultPageState.fromMap(Map<String, dynamic> map) {
|
factory SearchResultPageState.fromMap(Map<String, dynamic> map) {
|
||||||
return SearchresultPageState(
|
return 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,
|
||||||
@@ -54,7 +50,7 @@ class SearchresultPageState {
|
|||||||
|
|
||||||
String toJson() => json.encode(toMap());
|
String toJson() => json.encode(toMap());
|
||||||
|
|
||||||
factory SearchresultPageState.fromJson(String source) => SearchresultPageState.fromMap(json.decode(source));
|
factory SearchResultPageState.fromJson(String source) => SearchResultPageState.fromMap(json.decode(source));
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
@@ -66,7 +62,7 @@ class SearchresultPageState {
|
|||||||
if (identical(this, other)) return true;
|
if (identical(this, other)) return true;
|
||||||
final listEquals = const DeepCollectionEquality().equals;
|
final listEquals = const DeepCollectionEquality().equals;
|
||||||
|
|
||||||
return other is SearchresultPageState &&
|
return other is SearchResultPageState &&
|
||||||
other.isLoading == isLoading &&
|
other.isLoading == isLoading &&
|
||||||
other.isSuccess == isSuccess &&
|
other.isSuccess == isSuccess &&
|
||||||
other.isError == isError &&
|
other.isError == isError &&
|
||||||
@@ -78,34 +74,3 @@ class SearchresultPageState {
|
|||||||
return isLoading.hashCode ^ isSuccess.hashCode ^ isError.hashCode ^ searchResult.hashCode;
|
return isLoading.hashCode ^ isSuccess.hashCode ^ isError.hashCode ^ searchResult.hashCode;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class SearchResultPageStateNotifier extends StateNotifier<SearchresultPageState> {
|
|
||||||
SearchResultPageStateNotifier()
|
|
||||||
: super(SearchresultPageState(searchResult: [], isError: false, isLoading: true, isSuccess: false));
|
|
||||||
|
|
||||||
final SearchService _searchService = SearchService();
|
|
||||||
|
|
||||||
search(String searchTerm) async {
|
|
||||||
state = state.copyWith(searchResult: [], isError: false, isLoading: true, isSuccess: false);
|
|
||||||
|
|
||||||
List<ImmichAsset>? assets = await _searchService.searchAsset(searchTerm);
|
|
||||||
|
|
||||||
if (assets != null) {
|
|
||||||
state = state.copyWith(searchResult: assets, isError: false, isLoading: false, isSuccess: true);
|
|
||||||
} else {
|
|
||||||
state = state.copyWith(searchResult: [], isError: true, isLoading: false, isSuccess: false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
final searchResultPageStateProvider =
|
|
||||||
StateNotifierProvider<SearchResultPageStateNotifier, SearchresultPageState>((ref) {
|
|
||||||
return SearchResultPageStateNotifier();
|
|
||||||
});
|
|
||||||
|
|
||||||
final searchResultGroupByDateTimeProvider = StateProvider((ref) {
|
|
||||||
var assets = ref.watch(searchResultPageStateProvider).searchResult;
|
|
||||||
|
|
||||||
assets.sortByCompare<DateTime>((e) => DateTime.parse(e.createdAt), (a, b) => b.compareTo(a));
|
|
||||||
return assets.groupListsBy((element) => DateFormat('y-MM-dd').format(DateTime.parse(element.createdAt)));
|
|
||||||
});
|
|
||||||
@@ -1,85 +1,9 @@
|
|||||||
import 'dart:convert';
|
|
||||||
|
|
||||||
import 'package:collection/collection.dart';
|
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/modules/search/models/curated_location.model.dart';
|
||||||
|
import 'package:immich_mobile/modules/search/models/search_page_state.model.dart';
|
||||||
|
|
||||||
import 'package:immich_mobile/modules/search/services/search.service.dart';
|
import 'package:immich_mobile/modules/search/services/search.service.dart';
|
||||||
|
|
||||||
class SearchPageState {
|
|
||||||
final String searchTerm;
|
|
||||||
final bool isSearchEnabled;
|
|
||||||
final List<String> searchSuggestion;
|
|
||||||
final List<String> userSuggestedSearchTerms;
|
|
||||||
|
|
||||||
SearchPageState({
|
|
||||||
required this.searchTerm,
|
|
||||||
required this.isSearchEnabled,
|
|
||||||
required this.searchSuggestion,
|
|
||||||
required this.userSuggestedSearchTerms,
|
|
||||||
});
|
|
||||||
|
|
||||||
SearchPageState copyWith({
|
|
||||||
String? searchTerm,
|
|
||||||
bool? isSearchEnabled,
|
|
||||||
List<String>? searchSuggestion,
|
|
||||||
List<String>? userSuggestedSearchTerms,
|
|
||||||
}) {
|
|
||||||
return SearchPageState(
|
|
||||||
searchTerm: searchTerm ?? this.searchTerm,
|
|
||||||
isSearchEnabled: isSearchEnabled ?? this.isSearchEnabled,
|
|
||||||
searchSuggestion: searchSuggestion ?? this.searchSuggestion,
|
|
||||||
userSuggestedSearchTerms: userSuggestedSearchTerms ?? this.userSuggestedSearchTerms,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Map<String, dynamic> toMap() {
|
|
||||||
return {
|
|
||||||
'searchTerm': searchTerm,
|
|
||||||
'isSearchEnabled': isSearchEnabled,
|
|
||||||
'searchSuggestion': searchSuggestion,
|
|
||||||
'userSuggestedSearchTerms': userSuggestedSearchTerms,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
factory SearchPageState.fromMap(Map<String, dynamic> map) {
|
|
||||||
return SearchPageState(
|
|
||||||
searchTerm: map['searchTerm'] ?? '',
|
|
||||||
isSearchEnabled: map['isSearchEnabled'] ?? false,
|
|
||||||
searchSuggestion: List<String>.from(map['searchSuggestion']),
|
|
||||||
userSuggestedSearchTerms: List<String>.from(map['userSuggestedSearchTerms']),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
String toJson() => json.encode(toMap());
|
|
||||||
|
|
||||||
factory SearchPageState.fromJson(String source) => SearchPageState.fromMap(json.decode(source));
|
|
||||||
|
|
||||||
@override
|
|
||||||
String toString() {
|
|
||||||
return 'SearchPageState(searchTerm: $searchTerm, isSearchEnabled: $isSearchEnabled, searchSuggestion: $searchSuggestion, userSuggestedSearchTerms: $userSuggestedSearchTerms)';
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool operator ==(Object other) {
|
|
||||||
if (identical(this, other)) return true;
|
|
||||||
final listEquals = const DeepCollectionEquality().equals;
|
|
||||||
|
|
||||||
return other is SearchPageState &&
|
|
||||||
other.searchTerm == searchTerm &&
|
|
||||||
other.isSearchEnabled == isSearchEnabled &&
|
|
||||||
listEquals(other.searchSuggestion, searchSuggestion) &&
|
|
||||||
listEquals(other.userSuggestedSearchTerms, userSuggestedSearchTerms);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
int get hashCode {
|
|
||||||
return searchTerm.hashCode ^
|
|
||||||
isSearchEnabled.hashCode ^
|
|
||||||
searchSuggestion.hashCode ^
|
|
||||||
userSuggestedSearchTerms.hashCode;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class SearchPageStateNotifier extends StateNotifier<SearchPageState> {
|
class SearchPageStateNotifier extends StateNotifier<SearchPageState> {
|
||||||
SearchPageStateNotifier()
|
SearchPageStateNotifier()
|
||||||
: super(
|
: super(
|
||||||
@@ -129,3 +53,14 @@ class SearchPageStateNotifier extends StateNotifier<SearchPageState> {
|
|||||||
final searchPageStateProvider = StateNotifierProvider<SearchPageStateNotifier, SearchPageState>((ref) {
|
final searchPageStateProvider = StateNotifierProvider<SearchPageStateNotifier, SearchPageState>((ref) {
|
||||||
return SearchPageStateNotifier();
|
return SearchPageStateNotifier();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
final getCuratedLocationProvider = FutureProvider.autoDispose<List<CuratedLocation>>((ref) async {
|
||||||
|
final SearchService _searchService = SearchService();
|
||||||
|
|
||||||
|
var curatedLocation = await _searchService.getCuratedLocation();
|
||||||
|
if (curatedLocation != null) {
|
||||||
|
return curatedLocation;
|
||||||
|
} else {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import 'package:collection/collection.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/modules/search/models/search_result_page_state.model.dart';
|
||||||
|
|
||||||
|
import 'package:immich_mobile/modules/search/services/search.service.dart';
|
||||||
|
import 'package:immich_mobile/shared/models/immich_asset.model.dart';
|
||||||
|
import 'package:intl/intl.dart';
|
||||||
|
|
||||||
|
class SearchResultPageNotifier extends StateNotifier<SearchResultPageState> {
|
||||||
|
SearchResultPageNotifier()
|
||||||
|
: super(SearchResultPageState(searchResult: [], isError: false, isLoading: true, isSuccess: false));
|
||||||
|
|
||||||
|
final SearchService _searchService = SearchService();
|
||||||
|
|
||||||
|
void search(String searchTerm) async {
|
||||||
|
state = state.copyWith(searchResult: [], isError: false, isLoading: true, isSuccess: false);
|
||||||
|
|
||||||
|
List<ImmichAsset>? assets = await _searchService.searchAsset(searchTerm);
|
||||||
|
|
||||||
|
if (assets != null) {
|
||||||
|
state = state.copyWith(searchResult: assets, isError: false, isLoading: false, isSuccess: true);
|
||||||
|
} else {
|
||||||
|
state = state.copyWith(searchResult: [], isError: true, isLoading: false, isSuccess: false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final searchResultPageProvider = StateNotifierProvider<SearchResultPageNotifier, SearchResultPageState>((ref) {
|
||||||
|
return SearchResultPageNotifier();
|
||||||
|
});
|
||||||
|
|
||||||
|
final searchResultGroupByDateTimeProvider = StateProvider((ref) {
|
||||||
|
var assets = ref.watch(searchResultPageProvider).searchResult;
|
||||||
|
|
||||||
|
assets.sortByCompare<DateTime>((e) => DateTime.parse(e.createdAt), (a, b) => b.compareTo(a));
|
||||||
|
return assets.groupListsBy((element) => DateFormat('y-MM-dd').format(DateTime.parse(element.createdAt)));
|
||||||
|
});
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:immich_mobile/modules/search/models/curated_location.model.dart';
|
||||||
import 'package:immich_mobile/shared/models/immich_asset.model.dart';
|
import 'package:immich_mobile/shared/models/immich_asset.model.dart';
|
||||||
import 'package:immich_mobile/shared/services/network.service.dart';
|
import 'package:immich_mobile/shared/services/network.service.dart';
|
||||||
|
|
||||||
@@ -36,4 +37,19 @@ class SearchService {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<List<CuratedLocation>?> getCuratedLocation() async {
|
||||||
|
try {
|
||||||
|
var res = await _networkService.getRequest(url: "asset/allLocation");
|
||||||
|
|
||||||
|
List<dynamic> decodedData = jsonDecode(res.toString());
|
||||||
|
|
||||||
|
List<CuratedLocation> result = List.from(decodedData.map((a) => CuratedLocation.fromMap(a)));
|
||||||
|
|
||||||
|
return result;
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint("[ERROR] [getCuratedLocation] ${e.toString()}");
|
||||||
|
throw Error();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
|
import 'package:hive_flutter/hive_flutter.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/constants/hive_box.dart';
|
||||||
|
import 'package:immich_mobile/modules/search/models/curated_location.model.dart';
|
||||||
import 'package:immich_mobile/modules/search/providers/search_page_state.provider.dart';
|
import 'package:immich_mobile/modules/search/providers/search_page_state.provider.dart';
|
||||||
import 'package:immich_mobile/modules/search/ui/search_bar.dart';
|
import 'package:immich_mobile/modules/search/ui/search_bar.dart';
|
||||||
import 'package:immich_mobile/modules/search/ui/search_suggestion_list.dart';
|
import 'package:immich_mobile/modules/search/ui/search_suggestion_list.dart';
|
||||||
@@ -15,7 +19,9 @@ class SearchPage extends HookConsumerWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
var box = Hive.box(userInfoBox);
|
||||||
final isSearchEnabled = ref.watch(searchPageStateProvider).isSearchEnabled;
|
final isSearchEnabled = ref.watch(searchPageStateProvider).isSearchEnabled;
|
||||||
|
AsyncValue<List<CuratedLocation>> curatedLocation = ref.watch(getCuratedLocationProvider);
|
||||||
|
|
||||||
useEffect(() {
|
useEffect(() {
|
||||||
searchFocusNode = FocusNode();
|
searchFocusNode = FocusNode();
|
||||||
@@ -29,6 +35,53 @@ class SearchPage extends HookConsumerWidget {
|
|||||||
AutoRouter.of(context).push(SearchResultRoute(searchTerm: searchTerm));
|
AutoRouter.of(context).push(SearchResultRoute(searchTerm: searchTerm));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_buildPlaces() {
|
||||||
|
return curatedLocation.when(
|
||||||
|
loading: () => const CircularProgressIndicator(),
|
||||||
|
error: (err, stack) => Text('Error: $err'),
|
||||||
|
data: (curatedLocations) {
|
||||||
|
return curatedLocations.isNotEmpty
|
||||||
|
? SizedBox(
|
||||||
|
height: MediaQuery.of(context).size.width / 3,
|
||||||
|
child: ListView.builder(
|
||||||
|
padding: const EdgeInsets.only(left: 16),
|
||||||
|
scrollDirection: Axis.horizontal,
|
||||||
|
itemCount: curatedLocation.value?.length,
|
||||||
|
itemBuilder: ((context, index) {
|
||||||
|
CuratedLocation locationInfo = curatedLocations[index];
|
||||||
|
var thumbnailRequestUrl =
|
||||||
|
'${box.get(serverEndpointKey)}/asset/file?aid=${locationInfo.deviceAssetId}&did=${locationInfo.deviceId}&isThumb=true';
|
||||||
|
|
||||||
|
return ThumbnailWithInfo(
|
||||||
|
imageUrl: thumbnailRequestUrl,
|
||||||
|
textInfo: locationInfo.city,
|
||||||
|
onTap: () {
|
||||||
|
AutoRouter.of(context).push(SearchResultRoute(searchTerm: locationInfo.city));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: SizedBox(
|
||||||
|
height: MediaQuery.of(context).size.width / 3,
|
||||||
|
child: ListView.builder(
|
||||||
|
padding: const EdgeInsets.only(left: 16),
|
||||||
|
scrollDirection: Axis.horizontal,
|
||||||
|
itemCount: 1,
|
||||||
|
itemBuilder: ((context, index) {
|
||||||
|
return ThumbnailWithInfo(
|
||||||
|
imageUrl:
|
||||||
|
'https://images.unsplash.com/photo-1612178537253-bccd437b730e?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxzZWFyY2h8NXx8Ymxhbmt8ZW58MHx8MHx8&auto=format&fit=crop&w=700&q=60',
|
||||||
|
textInfo: 'No Places Info Available',
|
||||||
|
onTap: () {},
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: SearchBar(
|
appBar: SearchBar(
|
||||||
searchFocusNode: searchFocusNode,
|
searchFocusNode: searchFocusNode,
|
||||||
@@ -41,11 +94,17 @@ class SearchPage extends HookConsumerWidget {
|
|||||||
},
|
},
|
||||||
child: Stack(
|
child: Stack(
|
||||||
children: [
|
children: [
|
||||||
const Center(
|
|
||||||
child: Text("Start typing to search for your photos"),
|
|
||||||
),
|
|
||||||
ListView(
|
ListView(
|
||||||
children: const [],
|
children: [
|
||||||
|
const Padding(
|
||||||
|
padding: EdgeInsets.all(16.0),
|
||||||
|
child: Text(
|
||||||
|
"Places",
|
||||||
|
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
_buildPlaces(),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
isSearchEnabled ? SearchSuggestionList(onSubmitted: _onSearchSubmitted) : Container(),
|
isSearchEnabled ? SearchSuggestionList(onSubmitted: _onSearchSubmitted) : Container(),
|
||||||
],
|
],
|
||||||
@@ -54,3 +113,66 @@ class SearchPage extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class ThumbnailWithInfo extends StatelessWidget {
|
||||||
|
const ThumbnailWithInfo({Key? key, required this.textInfo, required this.imageUrl, required this.onTap})
|
||||||
|
: super(key: key);
|
||||||
|
|
||||||
|
final String textInfo;
|
||||||
|
final String imageUrl;
|
||||||
|
final Function onTap;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
var box = Hive.box(userInfoBox);
|
||||||
|
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: () {
|
||||||
|
onTap();
|
||||||
|
},
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.only(right: 8.0),
|
||||||
|
child: SizedBox(
|
||||||
|
width: MediaQuery.of(context).size.width / 3,
|
||||||
|
height: MediaQuery.of(context).size.width / 3,
|
||||||
|
child: Stack(
|
||||||
|
alignment: Alignment.bottomCenter,
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
foregroundDecoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
color: Colors.black26,
|
||||||
|
),
|
||||||
|
child: ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
child: CachedNetworkImage(
|
||||||
|
width: 150,
|
||||||
|
height: 150,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
imageUrl: imageUrl,
|
||||||
|
httpHeaders: {"Authorization": "Bearer ${box.get(accessTokenKey)}"},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Positioned(
|
||||||
|
bottom: 8,
|
||||||
|
left: 10,
|
||||||
|
child: SizedBox(
|
||||||
|
width: MediaQuery.of(context).size.width / 3,
|
||||||
|
child: Text(
|
||||||
|
textInfo,
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 12,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import 'package:immich_mobile/modules/home/ui/draggable_scrollbar.dart';
|
|||||||
import 'package:immich_mobile/modules/home/ui/image_grid.dart';
|
import 'package:immich_mobile/modules/home/ui/image_grid.dart';
|
||||||
import 'package:immich_mobile/modules/home/ui/monthly_title_text.dart';
|
import 'package:immich_mobile/modules/home/ui/monthly_title_text.dart';
|
||||||
import 'package:immich_mobile/modules/search/providers/search_page_state.provider.dart';
|
import 'package:immich_mobile/modules/search/providers/search_page_state.provider.dart';
|
||||||
import 'package:immich_mobile/modules/search/providers/search_result_page_state.provider.dart';
|
import 'package:immich_mobile/modules/search/providers/search_result_page.provider.dart';
|
||||||
import 'package:immich_mobile/modules/search/ui/search_suggestion_list.dart';
|
import 'package:immich_mobile/modules/search/ui/search_suggestion_list.dart';
|
||||||
|
|
||||||
class SearchResultPage extends HookConsumerWidget {
|
class SearchResultPage extends HookConsumerWidget {
|
||||||
@@ -28,7 +28,7 @@ class SearchResultPage extends HookConsumerWidget {
|
|||||||
useEffect(() {
|
useEffect(() {
|
||||||
searchFocusNode = FocusNode();
|
searchFocusNode = FocusNode();
|
||||||
|
|
||||||
Future.delayed(Duration.zero, () => ref.read(searchResultPageStateProvider.notifier).search(searchTerm));
|
Future.delayed(Duration.zero, () => ref.read(searchResultPageProvider.notifier).search(searchTerm));
|
||||||
return () => searchFocusNode.dispose();
|
return () => searchFocusNode.dispose();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -37,7 +37,7 @@ class SearchResultPage extends HookConsumerWidget {
|
|||||||
searchFocusNode.unfocus();
|
searchFocusNode.unfocus();
|
||||||
isNewSearch.value = false;
|
isNewSearch.value = false;
|
||||||
currentSearchTerm.value = newSearchTerm;
|
currentSearchTerm.value = newSearchTerm;
|
||||||
ref.watch(searchResultPageStateProvider.notifier).search(newSearchTerm);
|
ref.watch(searchResultPageProvider.notifier).search(newSearchTerm);
|
||||||
}
|
}
|
||||||
|
|
||||||
_buildTextField() {
|
_buildTextField() {
|
||||||
@@ -99,7 +99,7 @@ class SearchResultPage extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_buildSearchResult() {
|
_buildSearchResult() {
|
||||||
var searchResultPageState = ref.watch(searchResultPageStateProvider);
|
var searchResultPageState = ref.watch(searchResultPageProvider);
|
||||||
var assetGroupByDateTime = ref.watch(searchResultGroupByDateTimeProvider);
|
var assetGroupByDateTime = ref.watch(searchResultGroupByDateTimeProvider);
|
||||||
|
|
||||||
if (searchResultPageState.isError) {
|
if (searchResultPageState.isError) {
|
||||||
|
|||||||
33
mobile/lib/routing/tab_navigation_observer.dart
Normal file
33
mobile/lib/routing/tab_navigation_observer.dart
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import 'package:auto_route/auto_route.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
|
||||||
|
import 'package:immich_mobile/modules/search/providers/search_page_state.provider.dart';
|
||||||
|
import 'package:immich_mobile/shared/providers/server_info.provider.dart';
|
||||||
|
|
||||||
|
class TabNavigationObserver extends AutoRouterObserver {
|
||||||
|
/// Riverpod Instance
|
||||||
|
final WidgetRef ref;
|
||||||
|
|
||||||
|
TabNavigationObserver({
|
||||||
|
required this.ref,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didInitTabRoute(TabPageRoute route, TabPageRoute? previousRoute) {
|
||||||
|
// Perform tasks on first navigation to SearchRoute
|
||||||
|
if (route.name == 'SearchRoute') {
|
||||||
|
// ref.refresh(getCuratedLocationProvider);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> didChangeTabRoute(TabPageRoute route, TabPageRoute previousRoute) async {
|
||||||
|
// Perform tasks on re-visit to SearchRoute
|
||||||
|
if (route.name == 'SearchRoute') {
|
||||||
|
// Refresh Location State
|
||||||
|
ref.refresh(getCuratedLocationProvider);
|
||||||
|
}
|
||||||
|
|
||||||
|
ref.watch(serverInfoProvider.notifier).getServerVersion();
|
||||||
|
}
|
||||||
|
}
|
||||||
78
mobile/lib/shared/models/server_info_state.model.dart
Normal file
78
mobile/lib/shared/models/server_info_state.model.dart
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:immich_mobile/shared/models/mapbox_info.model.dart';
|
||||||
|
import 'package:immich_mobile/shared/models/server_version.model.dart';
|
||||||
|
|
||||||
|
class ServerInfoState {
|
||||||
|
final MapboxInfo mapboxInfo;
|
||||||
|
final ServerVersion serverVersion;
|
||||||
|
final bool isVersionMismatch;
|
||||||
|
final String versionMismatchErrorMessage;
|
||||||
|
|
||||||
|
ServerInfoState({
|
||||||
|
required this.mapboxInfo,
|
||||||
|
required this.serverVersion,
|
||||||
|
required this.isVersionMismatch,
|
||||||
|
required this.versionMismatchErrorMessage,
|
||||||
|
});
|
||||||
|
|
||||||
|
ServerInfoState copyWith({
|
||||||
|
MapboxInfo? mapboxInfo,
|
||||||
|
ServerVersion? serverVersion,
|
||||||
|
bool? isVersionMismatch,
|
||||||
|
String? versionMismatchErrorMessage,
|
||||||
|
}) {
|
||||||
|
return ServerInfoState(
|
||||||
|
mapboxInfo: mapboxInfo ?? this.mapboxInfo,
|
||||||
|
serverVersion: serverVersion ?? this.serverVersion,
|
||||||
|
isVersionMismatch: isVersionMismatch ?? this.isVersionMismatch,
|
||||||
|
versionMismatchErrorMessage: versionMismatchErrorMessage ?? this.versionMismatchErrorMessage,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> toMap() {
|
||||||
|
return {
|
||||||
|
'mapboxInfo': mapboxInfo.toMap(),
|
||||||
|
'serverVersion': serverVersion.toMap(),
|
||||||
|
'isVersionMismatch': isVersionMismatch,
|
||||||
|
'versionMismatchErrorMessage': versionMismatchErrorMessage,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
factory ServerInfoState.fromMap(Map<String, dynamic> map) {
|
||||||
|
return ServerInfoState(
|
||||||
|
mapboxInfo: MapboxInfo.fromMap(map['mapboxInfo']),
|
||||||
|
serverVersion: ServerVersion.fromMap(map['serverVersion']),
|
||||||
|
isVersionMismatch: map['isVersionMismatch'] ?? false,
|
||||||
|
versionMismatchErrorMessage: map['versionMismatchErrorMessage'] ?? '',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
String toJson() => json.encode(toMap());
|
||||||
|
|
||||||
|
factory ServerInfoState.fromJson(String source) => ServerInfoState.fromMap(json.decode(source));
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'ServerInfoState(mapboxInfo: $mapboxInfo, serverVersion: $serverVersion, isVersionMismatch: $isVersionMismatch, versionMismatchErrorMessage: $versionMismatchErrorMessage)';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
if (identical(this, other)) return true;
|
||||||
|
|
||||||
|
return other is ServerInfoState &&
|
||||||
|
other.mapboxInfo == mapboxInfo &&
|
||||||
|
other.serverVersion == serverVersion &&
|
||||||
|
other.isVersionMismatch == isVersionMismatch &&
|
||||||
|
other.versionMismatchErrorMessage == versionMismatchErrorMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode {
|
||||||
|
return mapboxInfo.hashCode ^
|
||||||
|
serverVersion.hashCode ^
|
||||||
|
isVersionMismatch.hashCode ^
|
||||||
|
versionMismatchErrorMessage.hashCode;
|
||||||
|
}
|
||||||
|
}
|
||||||
72
mobile/lib/shared/models/server_version.model.dart
Normal file
72
mobile/lib/shared/models/server_version.model.dart
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
class ServerVersion {
|
||||||
|
final int major;
|
||||||
|
final int minor;
|
||||||
|
final int patch;
|
||||||
|
final int build;
|
||||||
|
|
||||||
|
ServerVersion({
|
||||||
|
required this.major,
|
||||||
|
required this.minor,
|
||||||
|
required this.patch,
|
||||||
|
required this.build,
|
||||||
|
});
|
||||||
|
|
||||||
|
ServerVersion copyWith({
|
||||||
|
int? major,
|
||||||
|
int? minor,
|
||||||
|
int? patch,
|
||||||
|
int? build,
|
||||||
|
}) {
|
||||||
|
return ServerVersion(
|
||||||
|
major: major ?? this.major,
|
||||||
|
minor: minor ?? this.minor,
|
||||||
|
patch: patch ?? this.patch,
|
||||||
|
build: build ?? this.build,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> toMap() {
|
||||||
|
return {
|
||||||
|
'major': major,
|
||||||
|
'minor': minor,
|
||||||
|
'patch': patch,
|
||||||
|
'build': build,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
factory ServerVersion.fromMap(Map<String, dynamic> map) {
|
||||||
|
return ServerVersion(
|
||||||
|
major: map['major']?.toInt() ?? 0,
|
||||||
|
minor: map['minor']?.toInt() ?? 0,
|
||||||
|
patch: map['patch']?.toInt() ?? 0,
|
||||||
|
build: map['build']?.toInt() ?? 0,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
String toJson() => json.encode(toMap());
|
||||||
|
|
||||||
|
factory ServerVersion.fromJson(String source) => ServerVersion.fromMap(json.decode(source));
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'ServerVersion(major: $major, minor: $minor, patch: $patch, build: $build)';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
if (identical(this, other)) return true;
|
||||||
|
|
||||||
|
return other is ServerVersion &&
|
||||||
|
other.major == major &&
|
||||||
|
other.minor == minor &&
|
||||||
|
other.patch == patch &&
|
||||||
|
other.build == build;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode {
|
||||||
|
return major.hashCode ^ minor.hashCode ^ patch.hashCode ^ build.hashCode;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,59 +1,19 @@
|
|||||||
import 'dart:convert';
|
|
||||||
|
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
|
||||||
import 'package:immich_mobile/shared/models/mapbox_info.model.dart';
|
import 'package:immich_mobile/shared/models/mapbox_info.model.dart';
|
||||||
|
import 'package:immich_mobile/shared/models/server_info_state.model.dart';
|
||||||
|
import 'package:immich_mobile/shared/models/server_version.model.dart';
|
||||||
import 'package:immich_mobile/shared/services/server_info.service.dart';
|
import 'package:immich_mobile/shared/services/server_info.service.dart';
|
||||||
|
import 'package:package_info_plus/package_info_plus.dart';
|
||||||
class ServerInfoState {
|
|
||||||
final MapboxInfo mapboxInfo;
|
|
||||||
ServerInfoState({
|
|
||||||
required this.mapboxInfo,
|
|
||||||
});
|
|
||||||
|
|
||||||
ServerInfoState copyWith({
|
|
||||||
MapboxInfo? mapboxInfo,
|
|
||||||
}) {
|
|
||||||
return ServerInfoState(
|
|
||||||
mapboxInfo: mapboxInfo ?? this.mapboxInfo,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Map<String, dynamic> toMap() {
|
|
||||||
return {
|
|
||||||
'mapboxInfo': mapboxInfo.toMap(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
factory ServerInfoState.fromMap(Map<String, dynamic> map) {
|
|
||||||
return ServerInfoState(
|
|
||||||
mapboxInfo: MapboxInfo.fromMap(map['mapboxInfo']),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
String toJson() => json.encode(toMap());
|
|
||||||
|
|
||||||
factory ServerInfoState.fromJson(String source) => ServerInfoState.fromMap(json.decode(source));
|
|
||||||
|
|
||||||
@override
|
|
||||||
String toString() => 'ServerInfoState(mapboxInfo: $mapboxInfo)';
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool operator ==(Object other) {
|
|
||||||
if (identical(this, other)) return true;
|
|
||||||
|
|
||||||
return other is ServerInfoState && other.mapboxInfo == mapboxInfo;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
int get hashCode => mapboxInfo.hashCode;
|
|
||||||
}
|
|
||||||
|
|
||||||
class ServerInfoNotifier extends StateNotifier<ServerInfoState> {
|
class ServerInfoNotifier extends StateNotifier<ServerInfoState> {
|
||||||
ServerInfoNotifier()
|
ServerInfoNotifier()
|
||||||
: super(
|
: super(
|
||||||
ServerInfoState(
|
ServerInfoState(
|
||||||
mapboxInfo: MapboxInfo(isEnable: false, mapboxSecret: ""),
|
mapboxInfo: MapboxInfo(isEnable: false, mapboxSecret: ""),
|
||||||
|
serverVersion: ServerVersion(major: 0, patch: 0, minor: 0, build: 0),
|
||||||
|
isVersionMismatch: false,
|
||||||
|
versionMismatchErrorMessage: "",
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -61,9 +21,63 @@ class ServerInfoNotifier extends StateNotifier<ServerInfoState> {
|
|||||||
|
|
||||||
getMapboxInfo() async {
|
getMapboxInfo() async {
|
||||||
MapboxInfo mapboxInfoRes = await _serverInfoService.getMapboxInfo();
|
MapboxInfo mapboxInfoRes = await _serverInfoService.getMapboxInfo();
|
||||||
print(mapboxInfoRes);
|
|
||||||
state = state.copyWith(mapboxInfo: mapboxInfoRes);
|
state = state.copyWith(mapboxInfo: mapboxInfoRes);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getServerVersion() async {
|
||||||
|
ServerVersion? serverVersion = await _serverInfoService.getServerVersion();
|
||||||
|
|
||||||
|
if (serverVersion == null) {
|
||||||
|
state = state.copyWith(
|
||||||
|
isVersionMismatch: true,
|
||||||
|
versionMismatchErrorMessage:
|
||||||
|
"Server is out of date. Some functionalities might not working correctly. Download and rebuild server",
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
state = state.copyWith(serverVersion: serverVersion);
|
||||||
|
|
||||||
|
PackageInfo packageInfo = await PackageInfo.fromPlatform();
|
||||||
|
|
||||||
|
Map<String, int> appVersion = _getDetailVersion(packageInfo.version);
|
||||||
|
|
||||||
|
if (appVersion["major"]! > serverVersion.major) {
|
||||||
|
state = state.copyWith(
|
||||||
|
isVersionMismatch: true,
|
||||||
|
versionMismatchErrorMessage:
|
||||||
|
"Server is out of date in major version. Some functionalities might not work correctly. Download and rebuild server",
|
||||||
|
);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (appVersion["minor"]! > serverVersion.minor) {
|
||||||
|
state = state.copyWith(
|
||||||
|
isVersionMismatch: true,
|
||||||
|
versionMismatchErrorMessage:
|
||||||
|
"Server is out of date in minor version. Some functionalities might not work correctly. Consider download and rebuild server",
|
||||||
|
);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
state = state.copyWith(isVersionMismatch: false, versionMismatchErrorMessage: "");
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, int> _getDetailVersion(String version) {
|
||||||
|
List<String> detail = version.split(".");
|
||||||
|
|
||||||
|
var major = detail[0];
|
||||||
|
var minor = detail[1];
|
||||||
|
var patch = detail[2];
|
||||||
|
|
||||||
|
return {
|
||||||
|
"major": int.parse(major),
|
||||||
|
"minor": int.parse(minor),
|
||||||
|
"patch": int.parse(patch),
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final serverInfoProvider = StateNotifierProvider<ServerInfoNotifier, ServerInfoState>((ref) {
|
final serverInfoProvider = StateNotifierProvider<ServerInfoNotifier, ServerInfoState>((ref) {
|
||||||
|
|||||||
@@ -30,10 +30,14 @@ class BackupService {
|
|||||||
Function(int, int) uploadProgress) async {
|
Function(int, int) uploadProgress) async {
|
||||||
var dio = Dio();
|
var dio = Dio();
|
||||||
dio.interceptors.add(AuthenticatedRequestInterceptor());
|
dio.interceptors.add(AuthenticatedRequestInterceptor());
|
||||||
|
|
||||||
String deviceId = Hive.box(userInfoBox).get(deviceIdKey);
|
String deviceId = Hive.box(userInfoBox).get(deviceIdKey);
|
||||||
String savedEndpoint = Hive.box(userInfoBox).get(serverEndpointKey);
|
String savedEndpoint = Hive.box(userInfoBox).get(serverEndpointKey);
|
||||||
File? file;
|
File? file;
|
||||||
|
|
||||||
|
MultipartFile assetRawUploadData;
|
||||||
|
MultipartFile thumbnailUploadData;
|
||||||
|
|
||||||
for (var entity in assetList) {
|
for (var entity in assetList) {
|
||||||
try {
|
try {
|
||||||
if (entity.type == AssetType.video) {
|
if (entity.type == AssetType.video) {
|
||||||
@@ -43,12 +47,20 @@ class BackupService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (file != null) {
|
if (file != null) {
|
||||||
|
FormData formData;
|
||||||
String originalFileName = await entity.titleAsync;
|
String originalFileName = await entity.titleAsync;
|
||||||
String fileNameWithoutPath = originalFileName.toString().split(".")[0];
|
String fileNameWithoutPath = originalFileName.toString().split(".")[0];
|
||||||
var fileExtension = p.extension(file.path);
|
var fileExtension = p.extension(file.path);
|
||||||
var mimeType = FileHelper.getMimeType(file.path);
|
var mimeType = FileHelper.getMimeType(file.path);
|
||||||
|
assetRawUploadData = await MultipartFile.fromFile(
|
||||||
var formData = FormData.fromMap({
|
file.path,
|
||||||
|
filename: fileNameWithoutPath,
|
||||||
|
contentType: MediaType(
|
||||||
|
mimeType["type"],
|
||||||
|
mimeType["subType"],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
formData = FormData.fromMap({
|
||||||
'deviceAssetId': entity.id,
|
'deviceAssetId': entity.id,
|
||||||
'deviceId': deviceId,
|
'deviceId': deviceId,
|
||||||
'assetType': _getAssetType(entity.type),
|
'assetType': _getAssetType(entity.type),
|
||||||
@@ -57,18 +69,36 @@ class BackupService {
|
|||||||
'isFavorite': entity.isFavorite,
|
'isFavorite': entity.isFavorite,
|
||||||
'fileExtension': fileExtension,
|
'fileExtension': fileExtension,
|
||||||
'duration': entity.videoDuration,
|
'duration': entity.videoDuration,
|
||||||
'files': [
|
'assetData': [assetRawUploadData]
|
||||||
await MultipartFile.fromFile(
|
|
||||||
file.path,
|
|
||||||
filename: fileNameWithoutPath,
|
|
||||||
contentType: MediaType(
|
|
||||||
mimeType["type"],
|
|
||||||
mimeType["subType"],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Build thumbnail multipart data
|
||||||
|
var thumbnailData = await entity.thumbDataWithSize(1280, 720);
|
||||||
|
if (thumbnailData != null) {
|
||||||
|
thumbnailUploadData = MultipartFile.fromBytes(
|
||||||
|
List.from(thumbnailData),
|
||||||
|
filename: fileNameWithoutPath,
|
||||||
|
contentType: MediaType(
|
||||||
|
"image",
|
||||||
|
"jpeg",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Send thumbnail data if it is exist
|
||||||
|
formData = FormData.fromMap({
|
||||||
|
'deviceAssetId': entity.id,
|
||||||
|
'deviceId': deviceId,
|
||||||
|
'assetType': _getAssetType(entity.type),
|
||||||
|
'createdAt': entity.createDateTime.toIso8601String(),
|
||||||
|
'modifiedAt': entity.modifiedDateTime.toIso8601String(),
|
||||||
|
'isFavorite': entity.isFavorite,
|
||||||
|
'fileExtension': fileExtension,
|
||||||
|
'duration': entity.videoDuration,
|
||||||
|
'thumbnailData': [thumbnailUploadData],
|
||||||
|
'assetData': [assetRawUploadData]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
Response res = await dio.post(
|
Response res = await dio.post(
|
||||||
'$savedEndpoint/asset/upload',
|
'$savedEndpoint/asset/upload',
|
||||||
data: formData,
|
data: formData,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
import 'package:immich_mobile/shared/models/mapbox_info.model.dart';
|
import 'package:immich_mobile/shared/models/mapbox_info.model.dart';
|
||||||
|
import 'package:immich_mobile/shared/models/server_version.model.dart';
|
||||||
import 'package:immich_mobile/shared/services/network.service.dart';
|
import 'package:immich_mobile/shared/services/network.service.dart';
|
||||||
import 'package:immich_mobile/shared/models/server_info.model.dart';
|
import 'package:immich_mobile/shared/models/server_info.model.dart';
|
||||||
|
|
||||||
@@ -17,4 +18,10 @@ class ServerInfoService {
|
|||||||
|
|
||||||
return MapboxInfo.fromJson(response.toString());
|
return MapboxInfo.fromJson(response.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<ServerVersion?> getServerVersion() async {
|
||||||
|
Response response = await _networkService.getRequest(url: 'server-info/version');
|
||||||
|
|
||||||
|
return ServerVersion.fromJson(response.toString());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,6 +32,9 @@ class FileHelper {
|
|||||||
case 'heif':
|
case 'heif':
|
||||||
return {"type": "image", "subType": "heif"};
|
return {"type": "image", "subType": "heif"};
|
||||||
|
|
||||||
|
case 'dng':
|
||||||
|
return {"type": "image", "subType": "dng"};
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return {"type": "unsupport", "subType": "unsupport"};
|
return {"type": "unsupport", "subType": "unsupport"};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -555,6 +555,48 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.2"
|
version: "2.0.2"
|
||||||
|
package_info_plus:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: package_info_plus
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "1.4.0"
|
||||||
|
package_info_plus_linux:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: package_info_plus_linux
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.3"
|
||||||
|
package_info_plus_macos:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: package_info_plus_macos
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "1.3.0"
|
||||||
|
package_info_plus_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: package_info_plus_platform_interface
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.2"
|
||||||
|
package_info_plus_web:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: package_info_plus_web
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.4"
|
||||||
|
package_info_plus_windows:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: package_info_plus_windows
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.4"
|
||||||
path:
|
path:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ name: immich_mobile
|
|||||||
description: A new Flutter project.
|
description: A new Flutter project.
|
||||||
|
|
||||||
publish_to: "none"
|
publish_to: "none"
|
||||||
version: 1.2.0+2
|
version: 1.3.0+0
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ">=2.15.1 <3.0.0"
|
sdk: ">=2.15.1 <3.0.0"
|
||||||
@@ -36,6 +36,7 @@ dependencies:
|
|||||||
# mapbox_gl: ^0.15.0
|
# mapbox_gl: ^0.15.0
|
||||||
flutter_map: ^0.14.0
|
flutter_map: ^0.14.0
|
||||||
flutter_udid: ^2.0.0
|
flutter_udid: ^2.0.0
|
||||||
|
package_info_plus: ^1.4.0
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
|||||||
@@ -1,30 +0,0 @@
|
|||||||
// This is a basic Flutter widget test.
|
|
||||||
//
|
|
||||||
// To perform an interaction with a widget in your test, use the WidgetTester
|
|
||||||
// utility that Flutter provides. For example, you can send tap and scroll
|
|
||||||
// gestures. You can also use WidgetTester to find child widgets in the widget
|
|
||||||
// tree, read text, and verify that the values of widget properties are correct.
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
|
||||||
|
|
||||||
import 'package:immich_mobile/main.dart';
|
|
||||||
|
|
||||||
void main() {
|
|
||||||
testWidgets('Counter increments smoke test', (WidgetTester tester) async {
|
|
||||||
// Build our app and trigger a frame.
|
|
||||||
await tester.pumpWidget(const ImmichApp());
|
|
||||||
|
|
||||||
// Verify that our counter starts at 0.
|
|
||||||
expect(find.text('0'), findsOneWidget);
|
|
||||||
expect(find.text('1'), findsNothing);
|
|
||||||
|
|
||||||
// Tap the '+' icon and trigger a frame.
|
|
||||||
await tester.tap(find.byIcon(Icons.add));
|
|
||||||
await tester.pump();
|
|
||||||
|
|
||||||
// Verify that our counter has incremented.
|
|
||||||
expect(find.text('0'), findsNothing);
|
|
||||||
expect(find.text('1'), findsOneWidget);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -9,7 +9,7 @@ WORKDIR /usr/src/app
|
|||||||
|
|
||||||
COPY package.json package-lock.json ./
|
COPY package.json package-lock.json ./
|
||||||
|
|
||||||
RUN apk add --update-cache build-base python3 libheif vips-dev vips ffmpeg
|
# RUN apk add --update-cache build-base python3 libheif vips-dev vips ffmpeg
|
||||||
|
|
||||||
RUN npm install
|
RUN npm install
|
||||||
|
|
||||||
@@ -30,7 +30,7 @@ WORKDIR /usr/src/app
|
|||||||
|
|
||||||
COPY package.json package-lock.json ./
|
COPY package.json package-lock.json ./
|
||||||
|
|
||||||
RUN apk add --update-cache build-base python3 libheif vips-dev vips ffmpeg
|
# RUN apk add --update-cache build-base python3 libheif vips-dev vips ffmpeg
|
||||||
|
|
||||||
RUN npm install --only=production
|
RUN npm install --only=production
|
||||||
|
|
||||||
|
|||||||
1099
server/package-lock.json
generated
1099
server/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -43,7 +43,6 @@
|
|||||||
"class-validator": "^0.13.2",
|
"class-validator": "^0.13.2",
|
||||||
"dotenv": "^14.2.0",
|
"dotenv": "^14.2.0",
|
||||||
"exifr": "^7.1.3",
|
"exifr": "^7.1.3",
|
||||||
"fluent-ffmpeg": "^2.1.2",
|
|
||||||
"joi": "^17.5.0",
|
"joi": "^17.5.0",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"passport": "^0.5.2",
|
"passport": "^0.5.2",
|
||||||
@@ -53,26 +52,23 @@
|
|||||||
"reflect-metadata": "^0.1.13",
|
"reflect-metadata": "^0.1.13",
|
||||||
"rimraf": "^3.0.2",
|
"rimraf": "^3.0.2",
|
||||||
"rxjs": "^7.2.0",
|
"rxjs": "^7.2.0",
|
||||||
"sharp": "0.28",
|
|
||||||
"socket.io-redis": "^6.1.1",
|
"socket.io-redis": "^6.1.1",
|
||||||
"systeminformation": "^5.11.0",
|
"systeminformation": "^5.11.0",
|
||||||
"typeorm": "^0.2.41"
|
"typeorm": "^0.2.41"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@nestjs/cli": "^8.0.0",
|
"@nestjs/cli": "^8.2.4",
|
||||||
"@nestjs/schematics": "^8.0.0",
|
"@nestjs/schematics": "^8.0.0",
|
||||||
"@nestjs/testing": "^8.0.0",
|
"@nestjs/testing": "^8.0.0",
|
||||||
"@types/bcrypt": "^5.0.0",
|
"@types/bcrypt": "^5.0.0",
|
||||||
"@types/bull": "^3.15.7",
|
"@types/bull": "^3.15.7",
|
||||||
"@types/express": "^4.17.13",
|
"@types/express": "^4.17.13",
|
||||||
"@types/fluent-ffmpeg": "^2.1.20",
|
|
||||||
"@types/imagemin": "^8.0.0",
|
"@types/imagemin": "^8.0.0",
|
||||||
"@types/jest": "27.0.2",
|
"@types/jest": "27.0.2",
|
||||||
"@types/lodash": "^4.14.178",
|
"@types/lodash": "^4.14.178",
|
||||||
"@types/multer": "^1.4.7",
|
"@types/multer": "^1.4.7",
|
||||||
"@types/node": "^16.0.0",
|
"@types/node": "^16.0.0",
|
||||||
"@types/passport-jwt": "^3.0.6",
|
"@types/passport-jwt": "^3.0.6",
|
||||||
"@types/sharp": "^0.29.5",
|
|
||||||
"@types/supertest": "^2.0.11",
|
"@types/supertest": "^2.0.11",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.0.0",
|
"@typescript-eslint/eslint-plugin": "^5.0.0",
|
||||||
"@typescript-eslint/parser": "^5.0.0",
|
"@typescript-eslint/parser": "^5.0.0",
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import {
|
|||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { JwtAuthGuard } from '../../modules/immich-jwt/guards/jwt-auth.guard';
|
import { JwtAuthGuard } from '../../modules/immich-jwt/guards/jwt-auth.guard';
|
||||||
import { AssetService } from './asset.service';
|
import { AssetService } from './asset.service';
|
||||||
import { FilesInterceptor } from '@nestjs/platform-express';
|
import { FileFieldsInterceptor, FilesInterceptor } from '@nestjs/platform-express';
|
||||||
import { multerOption } from '../../config/multer-option.config';
|
import { multerOption } from '../../config/multer-option.config';
|
||||||
import { AuthUserDto, GetAuthUser } from '../../decorators/auth-user.decorator';
|
import { AuthUserDto, GetAuthUser } from '../../decorators/auth-user.decorator';
|
||||||
import { CreateAssetDto } from './dto/create-asset.dto';
|
import { CreateAssetDto } from './dto/create-asset.dto';
|
||||||
@@ -29,34 +29,43 @@ import { GetNewAssetQueryDto } from './dto/get-new-asset-query.dto';
|
|||||||
import { BackgroundTaskService } from '../../modules/background-task/background-task.service';
|
import { BackgroundTaskService } from '../../modules/background-task/background-task.service';
|
||||||
import { DeleteAssetDto } from './dto/delete-asset.dto';
|
import { DeleteAssetDto } from './dto/delete-asset.dto';
|
||||||
import { SearchAssetDto } from './dto/search-asset.dto';
|
import { SearchAssetDto } from './dto/search-asset.dto';
|
||||||
|
import { CommunicationGateway } from '../communication/communication.gateway';
|
||||||
|
|
||||||
@UseGuards(JwtAuthGuard)
|
@UseGuards(JwtAuthGuard)
|
||||||
@Controller('asset')
|
@Controller('asset')
|
||||||
export class AssetController {
|
export class AssetController {
|
||||||
constructor(
|
constructor(
|
||||||
|
private wsCommunicateionGateway: CommunicationGateway,
|
||||||
private assetService: AssetService,
|
private assetService: AssetService,
|
||||||
private assetOptimizeService: AssetOptimizeService,
|
|
||||||
private backgroundTaskService: BackgroundTaskService,
|
private backgroundTaskService: BackgroundTaskService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@Post('upload')
|
@Post('upload')
|
||||||
@UseInterceptors(FilesInterceptor('files', 30, multerOption))
|
@UseInterceptors(
|
||||||
|
FileFieldsInterceptor(
|
||||||
|
[
|
||||||
|
{ name: 'assetData', maxCount: 1 },
|
||||||
|
{ name: 'thumbnailData', maxCount: 1 },
|
||||||
|
],
|
||||||
|
multerOption,
|
||||||
|
),
|
||||||
|
)
|
||||||
async uploadFile(
|
async uploadFile(
|
||||||
@GetAuthUser() authUser,
|
@GetAuthUser() authUser,
|
||||||
@UploadedFiles() files: Express.Multer.File[],
|
@UploadedFiles() uploadFiles: { assetData: Express.Multer.File[]; thumbnailData?: Express.Multer.File[] },
|
||||||
@Body(ValidationPipe) assetInfo: CreateAssetDto,
|
@Body(ValidationPipe) assetInfo: CreateAssetDto,
|
||||||
) {
|
) {
|
||||||
files.forEach(async (file) => {
|
uploadFiles.assetData.forEach(async (file) => {
|
||||||
const savedAsset = await this.assetService.createUserAsset(authUser, assetInfo, file.path, file.mimetype);
|
const savedAsset = await this.assetService.createUserAsset(authUser, assetInfo, file.path, file.mimetype);
|
||||||
|
|
||||||
if (savedAsset && savedAsset.type == AssetType.IMAGE) {
|
if (uploadFiles.thumbnailData != null) {
|
||||||
await this.assetOptimizeService.resizeImage(savedAsset);
|
await this.assetService.updateThumbnailInfo(savedAsset.id, uploadFiles.thumbnailData[0].path);
|
||||||
await this.backgroundTaskService.extractExif(savedAsset, file.originalname, file.size);
|
await this.backgroundTaskService.tagImage(uploadFiles.thumbnailData[0].path, savedAsset);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (savedAsset && savedAsset.type == AssetType.VIDEO) {
|
await this.backgroundTaskService.extractExif(savedAsset, file.originalname, file.size);
|
||||||
await this.assetOptimizeService.getVideoThumbnail(savedAsset, file.originalname);
|
|
||||||
}
|
this.wsCommunicateionGateway.server.to(savedAsset.userId).emit('on_upload_success', JSON.stringify(savedAsset));
|
||||||
});
|
});
|
||||||
|
|
||||||
return 'ok';
|
return 'ok';
|
||||||
@@ -72,6 +81,11 @@ export class AssetController {
|
|||||||
return this.assetService.serveFile(authUser, query, res, headers);
|
return this.assetService.serveFile(authUser, query, res, headers);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Get('/allLocation')
|
||||||
|
async getCuratedLocation(@GetAuthUser() authUser: AuthUserDto) {
|
||||||
|
return this.assetService.getCuratedLocation(authUser);
|
||||||
|
}
|
||||||
|
|
||||||
@Get('/searchTerm')
|
@Get('/searchTerm')
|
||||||
async getAssetSearchTerm(@GetAuthUser() authUser: AuthUserDto) {
|
async getAssetSearchTerm(@GetAuthUser() authUser: AuthUserDto) {
|
||||||
return this.assetService.getAssetSearchTerm(authUser);
|
return this.assetService.getAssetSearchTerm(authUser);
|
||||||
|
|||||||
@@ -8,9 +8,12 @@ import { AssetOptimizeService } from '../../modules/image-optimize/image-optimiz
|
|||||||
import { BullModule } from '@nestjs/bull';
|
import { BullModule } from '@nestjs/bull';
|
||||||
import { BackgroundTaskModule } from '../../modules/background-task/background-task.module';
|
import { BackgroundTaskModule } from '../../modules/background-task/background-task.module';
|
||||||
import { BackgroundTaskService } from '../../modules/background-task/background-task.service';
|
import { BackgroundTaskService } from '../../modules/background-task/background-task.service';
|
||||||
|
import { CommunicationModule } from '../communication/communication.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
|
CommunicationModule,
|
||||||
|
|
||||||
BullModule.registerQueue({
|
BullModule.registerQueue({
|
||||||
name: 'optimize',
|
name: 'optimize',
|
||||||
defaultJobOptions: {
|
defaultJobOptions: {
|
||||||
|
|||||||
@@ -24,6 +24,12 @@ export class AssetService {
|
|||||||
private assetRepository: Repository<AssetEntity>,
|
private assetRepository: Repository<AssetEntity>,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
public async updateThumbnailInfo(assetId: string, path: string) {
|
||||||
|
return await this.assetRepository.update(assetId, {
|
||||||
|
resizePath: path,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
public async createUserAsset(authUser: AuthUserDto, assetInfo: CreateAssetDto, path: string, mimeType: string) {
|
public async createUserAsset(authUser: AuthUserDto, assetInfo: CreateAssetDto, path: string, mimeType: string) {
|
||||||
const asset = new AssetEntity();
|
const asset = new AssetEntity();
|
||||||
asset.deviceAssetId = assetInfo.deviceAssetId;
|
asset.deviceAssetId = assetInfo.deviceAssetId;
|
||||||
@@ -303,4 +309,20 @@ export class AssetService {
|
|||||||
|
|
||||||
return rows;
|
return rows;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getCuratedLocation(authUser: AuthUserDto) {
|
||||||
|
const rows = await this.assetRepository.query(
|
||||||
|
`
|
||||||
|
select distinct on (e.city) a.id, e.city, a."resizePath", a."deviceAssetId", a."deviceId"
|
||||||
|
from assets a
|
||||||
|
left join exif e on a.id = e."assetId"
|
||||||
|
where a."userId" = $1
|
||||||
|
and e.city is not null
|
||||||
|
and a.type = 'IMAGE';
|
||||||
|
`,
|
||||||
|
[authUser.id],
|
||||||
|
);
|
||||||
|
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { JwtAuthGuard } from '../../modules/immich-jwt/guards/jwt-auth.guard';
|
|||||||
import { ServerInfoService } from './server-info.service';
|
import { ServerInfoService } from './server-info.service';
|
||||||
import mapboxGeocoding, { GeocodeService } from '@mapbox/mapbox-sdk/services/geocoding';
|
import mapboxGeocoding, { GeocodeService } from '@mapbox/mapbox-sdk/services/geocoding';
|
||||||
import { MapiResponse } from '@mapbox/mapbox-sdk/lib/classes/mapi-response';
|
import { MapiResponse } from '@mapbox/mapbox-sdk/lib/classes/mapi-response';
|
||||||
|
import { serverVersion } from '../../constants/server_version.constant';
|
||||||
|
|
||||||
@Controller('server-info')
|
@Controller('server-info')
|
||||||
export class ServerInfoController {
|
export class ServerInfoController {
|
||||||
@@ -30,4 +31,9 @@ export class ServerInfoController {
|
|||||||
mapboxSecret: this.configService.get('MAPBOX_KEY'),
|
mapboxSecret: this.configService.get('MAPBOX_KEY'),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Get('/version')
|
||||||
|
async getServerVersion() {
|
||||||
|
return serverVersion;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,9 +13,9 @@ export const immichAppConfig: ConfigModuleOptions = {
|
|||||||
JWT_SECRET: Joi.string().required(),
|
JWT_SECRET: Joi.string().required(),
|
||||||
ENABLE_MAPBOX: Joi.boolean().required().valid(true, false),
|
ENABLE_MAPBOX: Joi.boolean().required().valid(true, false),
|
||||||
MAPBOX_KEY: Joi.any().when('ENABLE_MAPBOX', {
|
MAPBOX_KEY: Joi.any().when('ENABLE_MAPBOX', {
|
||||||
is: true,
|
is: false,
|
||||||
then: Joi.string().required(),
|
then: Joi.string().optional().allow(null, ''),
|
||||||
otherwise: Joi.string().optional,
|
otherwise: Joi.string().required(),
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ export const multerConfig = {
|
|||||||
|
|
||||||
export const multerOption: MulterOptions = {
|
export const multerOption: MulterOptions = {
|
||||||
fileFilter: (req: Request, file: any, cb: any) => {
|
fileFilter: (req: Request, file: any, cb: any) => {
|
||||||
if (file.mimetype.match(/\/(jpg|jpeg|png|gif|mp4|x-msvideo|quicktime|heic|heif)$/)) {
|
if (file.mimetype.match(/\/(jpg|jpeg|png|gif|mp4|x-msvideo|quicktime|heic|heif|dng)$/)) {
|
||||||
cb(null, true);
|
cb(null, true);
|
||||||
} else {
|
} else {
|
||||||
cb(new HttpException(`Unsupported file type ${extname(file.originalname)}`, HttpStatus.BAD_REQUEST), false);
|
cb(new HttpException(`Unsupported file type ${extname(file.originalname)}`, HttpStatus.BAD_REQUEST), false);
|
||||||
@@ -23,17 +23,33 @@ export const multerOption: MulterOptions = {
|
|||||||
destination: (req: Request, file: Express.Multer.File, cb: any) => {
|
destination: (req: Request, file: Express.Multer.File, cb: any) => {
|
||||||
const uploadPath = multerConfig.dest;
|
const uploadPath = multerConfig.dest;
|
||||||
|
|
||||||
const userPath = `${uploadPath}/${req.user['id']}/original/${req.body['deviceId']}`;
|
if (file.fieldname == 'assetData') {
|
||||||
|
const originalUploadFolder = `${uploadPath}/${req.user['id']}/original/${req.body['deviceId']}`;
|
||||||
|
|
||||||
if (!existsSync(userPath)) {
|
if (!existsSync(originalUploadFolder)) {
|
||||||
mkdirSync(userPath, { recursive: true });
|
mkdirSync(originalUploadFolder, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
cb(null, originalUploadFolder);
|
||||||
|
} else if (file.fieldname == 'thumbnailData') {
|
||||||
|
const thumbnailUploadFolder = `${uploadPath}/${req.user['id']}/thumb/${req.body['deviceId']}`;
|
||||||
|
|
||||||
|
if (!existsSync(thumbnailUploadFolder)) {
|
||||||
|
mkdirSync(thumbnailUploadFolder, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
cb(null, thumbnailUploadFolder);
|
||||||
}
|
}
|
||||||
|
|
||||||
cb(null, userPath);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
filename: (req: Request, file: Express.Multer.File, cb: any) => {
|
filename: (req: Request, file: Express.Multer.File, cb: any) => {
|
||||||
cb(null, `${file.originalname.split('.')[0]}${req.body['fileExtension']}`);
|
// console.log(req, file);
|
||||||
|
|
||||||
|
if (file.fieldname == 'assetData') {
|
||||||
|
cb(null, `${file.originalname.split('.')[0]}${req.body['fileExtension']}`);
|
||||||
|
} else if (file.fieldname == 'thumbnailData') {
|
||||||
|
cb(null, `${file.originalname.split('.')[0]}.jpeg`);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|||||||
9
server/src/constants/server_version.constant.ts
Normal file
9
server/src/constants/server_version.constant.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
// major.minor.patch+build
|
||||||
|
// check mobile/pubspec.yml for current release version
|
||||||
|
|
||||||
|
export const serverVersion = {
|
||||||
|
major: 1,
|
||||||
|
minor: 3,
|
||||||
|
patch: 0,
|
||||||
|
build: 0,
|
||||||
|
};
|
||||||
@@ -41,7 +41,6 @@ export class BackgroundTaskProcessor {
|
|||||||
async extractExif(job: Job) {
|
async extractExif(job: Job) {
|
||||||
const { savedAsset, fileName, fileSize }: { savedAsset: AssetEntity; fileName: string; fileSize: number } =
|
const { savedAsset, fileName, fileSize }: { savedAsset: AssetEntity; fileName: string; fileSize: number } =
|
||||||
job.data;
|
job.data;
|
||||||
|
|
||||||
const fileBuffer = await readFile(savedAsset.originalPath);
|
const fileBuffer = await readFile(savedAsset.originalPath);
|
||||||
|
|
||||||
const exifData = await exifr.parse(fileBuffer);
|
const exifData = await exifr.parse(fileBuffer);
|
||||||
|
|||||||
@@ -1,15 +1,7 @@
|
|||||||
import { InjectQueue, Process, Processor } from '@nestjs/bull';
|
import { Processor } from '@nestjs/bull';
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
import { Job, Queue } from 'bull';
|
|
||||||
import { Repository } from 'typeorm';
|
import { Repository } from 'typeorm';
|
||||||
import { AssetEntity } from '../../api-v1/asset/entities/asset.entity';
|
import { AssetEntity } from '../../api-v1/asset/entities/asset.entity';
|
||||||
import sharp from 'sharp';
|
|
||||||
import { existsSync, mkdirSync, readFile } from 'fs';
|
|
||||||
import { ConfigService } from '@nestjs/config';
|
|
||||||
import ffmpeg from 'fluent-ffmpeg';
|
|
||||||
import { APP_UPLOAD_LOCATION } from '../../constants/upload_location.constant';
|
|
||||||
import { WebSocketServer } from '@nestjs/websockets';
|
|
||||||
import { Socket, Server as SocketIoServer } from 'socket.io';
|
|
||||||
import { CommunicationGateway } from '../../api-v1/communication/communication.gateway';
|
import { CommunicationGateway } from '../../api-v1/communication/communication.gateway';
|
||||||
import { BackgroundTaskService } from '../background-task/background-task.service';
|
import { BackgroundTaskService } from '../background-task/background-task.service';
|
||||||
|
|
||||||
@@ -22,115 +14,4 @@ export class ImageOptimizeProcessor {
|
|||||||
|
|
||||||
private backgroundTaskService: BackgroundTaskService,
|
private backgroundTaskService: BackgroundTaskService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@Process('resize-image')
|
|
||||||
async resizeUploadedImage(job: Job) {
|
|
||||||
const { savedAsset }: { savedAsset: AssetEntity } = job.data;
|
|
||||||
|
|
||||||
const basePath = APP_UPLOAD_LOCATION;
|
|
||||||
const resizePath = savedAsset.originalPath.replace('/original/', '/thumb/');
|
|
||||||
|
|
||||||
// Create folder for thumb image if not exist
|
|
||||||
|
|
||||||
const resizeDir = `${basePath}/${savedAsset.userId}/thumb/${savedAsset.deviceId}`;
|
|
||||||
|
|
||||||
if (!existsSync(resizeDir)) {
|
|
||||||
mkdirSync(resizeDir, { recursive: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
readFile(savedAsset.originalPath, async (err, data) => {
|
|
||||||
if (err) {
|
|
||||||
console.error('Error Reading File');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (savedAsset.mimeType == 'image/heic' || savedAsset.mimeType == 'image/heif') {
|
|
||||||
let desitnation = '';
|
|
||||||
if (savedAsset.mimeType == 'image/heic') {
|
|
||||||
desitnation = resizePath.replace('.HEIC', '.jpeg');
|
|
||||||
} else {
|
|
||||||
desitnation = resizePath.replace('.HEIF', '.jpeg');
|
|
||||||
}
|
|
||||||
|
|
||||||
sharp(data)
|
|
||||||
.toFormat('jpeg')
|
|
||||||
.resize(512, 512, { fit: 'outside' })
|
|
||||||
.toFile(desitnation, async (err, info) => {
|
|
||||||
if (err) {
|
|
||||||
console.error('Error resizing file ', err);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const res = await this.assetRepository.update(savedAsset, { resizePath: desitnation });
|
|
||||||
|
|
||||||
if (res.affected) {
|
|
||||||
this.wsCommunicateionGateway.server
|
|
||||||
.to(savedAsset.userId)
|
|
||||||
.emit('on_upload_success', JSON.stringify(savedAsset));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tag Image
|
|
||||||
this.backgroundTaskService.tagImage(desitnation, savedAsset);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
sharp(data)
|
|
||||||
.resize(512, 512, { fit: 'outside' })
|
|
||||||
.toFile(resizePath, async (err, info) => {
|
|
||||||
if (err) {
|
|
||||||
console.error('Error resizing file ', err);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const res = await this.assetRepository.update(savedAsset, { resizePath: resizePath });
|
|
||||||
if (res.affected) {
|
|
||||||
this.wsCommunicateionGateway.server
|
|
||||||
.to(savedAsset.userId)
|
|
||||||
.emit('on_upload_success', JSON.stringify(savedAsset));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tag Image
|
|
||||||
this.backgroundTaskService.tagImage(resizePath, savedAsset);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return 'ok';
|
|
||||||
}
|
|
||||||
|
|
||||||
@Process('get-video-thumbnail')
|
|
||||||
async resizeUploadedVideo(job: Job) {
|
|
||||||
const { savedAsset, filename }: { savedAsset: AssetEntity; filename: String } = job.data;
|
|
||||||
|
|
||||||
const basePath = APP_UPLOAD_LOCATION;
|
|
||||||
// const resizePath = savedAsset.originalPath.replace('/original/', '/thumb/');
|
|
||||||
// Create folder for thumb image if not exist
|
|
||||||
const resizeDir = `${basePath}/${savedAsset.userId}/thumb/${savedAsset.deviceId}`;
|
|
||||||
|
|
||||||
if (!existsSync(resizeDir)) {
|
|
||||||
mkdirSync(resizeDir, { recursive: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
ffmpeg(savedAsset.originalPath)
|
|
||||||
.thumbnail({
|
|
||||||
count: 1,
|
|
||||||
timestamps: [1],
|
|
||||||
folder: resizeDir,
|
|
||||||
filename: `${filename}.png`,
|
|
||||||
})
|
|
||||||
.on('end', async (a) => {
|
|
||||||
const thumbnailPath = `${resizeDir}/${filename}.png`;
|
|
||||||
|
|
||||||
const res = await this.assetRepository.update(savedAsset, { resizePath: `${resizeDir}/${filename}.png` });
|
|
||||||
|
|
||||||
if (res.affected) {
|
|
||||||
this.wsCommunicateionGateway.server
|
|
||||||
.to(savedAsset.userId)
|
|
||||||
.emit('on_upload_success', JSON.stringify(savedAsset));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tag Image
|
|
||||||
this.backgroundTaskService.tagImage(thumbnailPath, savedAsset);
|
|
||||||
});
|
|
||||||
|
|
||||||
return 'ok';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,33 +7,4 @@ import { AssetEntity } from '../../api-v1/asset/entities/asset.entity';
|
|||||||
@Injectable()
|
@Injectable()
|
||||||
export class AssetOptimizeService {
|
export class AssetOptimizeService {
|
||||||
constructor(@InjectQueue('optimize') private optimizeQueue: Queue) {}
|
constructor(@InjectQueue('optimize') private optimizeQueue: Queue) {}
|
||||||
|
|
||||||
public async resizeImage(savedAsset: AssetEntity) {
|
|
||||||
const job = await this.optimizeQueue.add(
|
|
||||||
'resize-image',
|
|
||||||
{
|
|
||||||
savedAsset,
|
|
||||||
},
|
|
||||||
{ jobId: randomUUID() },
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
jobId: job.id,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public async getVideoThumbnail(savedAsset: AssetEntity, filename: string) {
|
|
||||||
const job = await this.optimizeQueue.add(
|
|
||||||
'get-video-thumbnail',
|
|
||||||
{
|
|
||||||
savedAsset,
|
|
||||||
filename,
|
|
||||||
},
|
|
||||||
{ jobId: randomUUID() },
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
jobId: job.id,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user