Compare commits

...

17 Commits

Author SHA1 Message Date
Alex
c7dff229db Up minor v1.6.0 (#113) 2022-04-05 10:34:54 -05:00
Alex
8e80825b4f Build and tag docker image for Dockerhub release (#111)
* Clean up Dockerfile and added action to build microservice latest
* Combine build microservices and server into the same action
* Added build and push release version for microservices
2022-04-05 10:16:15 -05:00
Constantin Kraft
a1481c1113 Fix typo: Reserve -> Reverse (geocoding) (#112) 2022-04-05 10:11:40 -05:00
Alex
3bdcdef198 Fixed backup stuck at unsupported format (#108)
* Added webp as supported file type, allow continue upload when an image fail

* Added webp as supported file type, allow continue upload when an image fail

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

* Added distributionSha256Sum tp gradle-wrapper.properties
2022-03-29 14:13:47 -05:00
Alex
fccdbdd66a Update production dockerfile for a cleaner look (#86) 2022-03-29 08:56:59 -05:00
Alex
23ba651705 Fixed npm run start:prod not able to find build directory (#83) 2022-03-28 21:00:17 -05:00
Alex
ac0ad98b55 Fix docker-compose in production (#81)
* Fixed problem with docker-compose not updating new files in the multi-stage build.
* Update readme with a new screenshot
2022-03-28 15:21:15 -05:00
90 changed files with 922 additions and 541 deletions

View File

@@ -0,0 +1,64 @@
name: Build and Push Docker Image - Latest
on:
workflow_dispatch:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
build_and_push_server_latest:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
with:
ref: "main" # branch
- name: Set up QEMU
uses: docker/setup-qemu-action@v1.2.0
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v1.6.0
- name: Login to Docker Hub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push Immich
uses: docker/build-push-action@v2.10.0
with:
context: ./server
file: ./server/Dockerfile
platforms: linux/arm/v7,linux/amd64,linux/arm64
push: ${{ github.event_name != 'pull_request' }}
tags: |
altran1502/immich-server:latest
build_and_push_microservice_latest:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
with:
ref: "main" # branch
- name: Set up QEMU
uses: docker/setup-qemu-action@v1.2.0
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v1.6.0
- name: Login to Docker Hub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and Push Microservices
uses: docker/build-push-action@v2.10.0
with:
context: ./microservices
file: ./microservices/Dockerfile
platforms: linux/arm/v7,linux/amd64
push: ${{ github.event_name != 'pull_request' }}
tags: |
altran1502/immich-microservices:latest

View File

@@ -1,46 +0,0 @@
name: Build Server
on:
# Triggers the workflow on push or pull request events but only for the main branch
#schedule:
# * is a special character in YAML so you have to quote this string
#- cron: '0 0 * * *'
workflow_dispatch:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
buildandpush:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
with:
ref: "main" # branch
# https://github.com/docker/setup-qemu-action#usage
- name: Set up QEMU
uses: docker/setup-qemu-action@v1.2.0
# https://github.com/marketplace/actions/docker-setup-buildx
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v1.6.0
# https://github.com/docker/login-action#docker-hub
- name: Login to Docker Hub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
# https://github.com/docker/build-push-action#multi-platform-image
- name: Build and push Immich
uses: docker/build-push-action@v2.10.0
with:
context: ./server
file: ./server/Dockerfile
#platforms: linux/amd64,linux/arm64,linux/riscv64,linux/ppc64le,linux/s390x,linux/386,linux/mips64le,linux/mips64,linux/arm/v7,linux/arm/v6
platforms: linux/arm/v7,linux/amd64,linux/arm64
pull: true
push: true
tags: |
altran1502/immich-server:latest

View File

@@ -0,0 +1,95 @@
name: Build Server - Release
on:
workflow_dispatch:
release:
types: [published]
jobs:
build_and_push_server_release:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
with:
ref: "main"
fetch-depth: 0
- name: Docker meta
id: meta
uses: docker/metadata-action@v3
with:
images: altran1502/immich-server
- name: 'Get Previous tag'
id: previoustag
uses: "WyriHaximus/github-action-get-previous-tag@v1"
with:
fallback: latest
- name: Set up QEMU
uses: docker/setup-qemu-action@v1.2.0
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v1.6.0
- name: Login to Docker Hub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push immich-server release
uses: docker/build-push-action@v2.10.0
with:
context: ./server
file: ./server/Dockerfile
platforms: linux/arm/v7,linux/amd64,linux/arm64
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.previoustag.outputs.tag }}
labels: ${{ steps.meta.outputs.labels }}
build_and_push_microservice_release:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
with:
ref: "main"
fetch-depth: 0
- name: Docker meta
id: meta
uses: docker/metadata-action@v3
with:
images: altran1502/immich-microservices
- name: 'Get Previous tag'
id: previoustag
uses: "WyriHaximus/github-action-get-previous-tag@v1"
with:
fallback: latest
- name: Set up QEMU
uses: docker/setup-qemu-action@v1.2.0
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v1.6.0
- name: Login to Docker Hub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push immich-microservices release
uses: docker/build-push-action@v2.10.0
with:
context: ./microservices
file: ./microservices/Dockerfile
platforms: linux/arm/v7,linux/amd64
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.previoustag.outputs.tag }}
labels: ${{ steps.meta.outputs.labels }}

View File

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

View File

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

View File

@@ -34,7 +34,8 @@ Loading ~4000 images/videos
<p align="left">
<img src="design/nsc1.png" width="150" title="Login With Custom URL">
<img src="design/nsc2.png" width="150" title="Backup Setting Info">
<img src="design/nsc3.png" width="150" title="Multiple seelct">
<img src="design/nsc3.png" width="150" title="Multiple select">
<img src="design/nsc4.jpeg" width="150" title="Curated Search Info">
<img src="design/nsc6.png" width="150" title="EXIF Info">
</p>
@@ -58,7 +59,7 @@ This project is under heavy development, there will be continous functions, feat
- Object detection based on COCO SSD.
- Search assets based on tags and exif data (lens, make, model, orientation)
- 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] Reverse geocoding using Mapbox (Generous free-tier of 100,000 search/month)
- Show asset's location information on map (OpenStreetMap).
- Show curated places on the search page
- Show curated objects on the search page

BIN
design/nsc4.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 406 KiB

View File

@@ -1,6 +1,3 @@
# STAGE
NODE_ENV=development
# Database
DB_USERNAME=postgres
DB_PASSWORD=postgres

View File

@@ -2,11 +2,10 @@ version: "3.8"
services:
immich_server:
image: immich-server-dev:1.3.2
image: immich-server-dev:1.6.0
build:
context: ../server
target: development
dockerfile: ../server/Dockerfile
dockerfile: Dockerfile
command: npm run start:dev
expose:
- "3000"
@@ -16,6 +15,8 @@ services:
- /usr/src/app/node_modules
env_file:
- .env
environment:
- NODE_ENV=development
depends_on:
- redis
- database
@@ -23,11 +24,10 @@ services:
- immich_network
immich_microservices:
image: immich-microservices-dev:1.3.2
image: immich-microservices-dev:1.6.0
build:
context: ../microservices
target: development
dockerfile: ../microservices/Dockerfile
dockerfile: Dockerfile
command: npm run start:dev
expose:
- "3001"
@@ -37,6 +37,8 @@ services:
- /usr/src/app/node_modules
env_file:
- .env
environment:
- NODE_ENV=development
depends_on:
- database
networks:

View File

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

View File

@@ -2,46 +2,45 @@ version: "3.8"
services:
immich_server:
image: immich-server:1.4.0
image: immich-server:1.6.0
build:
context: ../server
target: production
dockerfile: ../server/Dockerfile
command: npm run start:prod
dockerfile: Dockerfile
entrypoint: ["/bin/sh", "./entrypoint.sh"]
expose:
- "3000"
volumes:
- ../server:/usr/src/app
- ${UPLOAD_LOCATION}:/usr/src/app/upload
- /usr/src/app/node_modules
env_file:
- .env
environment:
- NODE_ENV=production
depends_on:
- redis
- database
networks:
- immich_network
restart: unless-stopped
immich_microservices:
image: immich-microservices:1.4.0
image: immich-microservices:1.6.0
build:
context: ../microservices
target: production
dockerfile: ../microservices/Dockerfile
command: npm run start:prod
dockerfile: Dockerfile
entrypoint: ["/bin/sh", "./entrypoint.sh"]
expose:
- "3001"
volumes:
- ../microservices:/usr/src/app
- ${UPLOAD_LOCATION}:/usr/src/app/upload
- /usr/src/app/node_modules
env_file:
- .env
environment:
- NODE_ENV=production
depends_on:
- database
networks:
- immich_network
restart: unless-stopped
redis:
container_name: immich_redis
@@ -81,28 +80,7 @@ services:
depends_on:
- immich_server
# immich_tf_fastapi:
# container_name: immich_tf_fastapi
# image: tensor_flow_fastapi:1.0.0
# restart: always
# command: uvicorn app.main:app --proxy-headers --host 0.0.0.0 --port 8000 --reload
# build:
# context: ../machine_learning
# target: cpu
# dockerfile: ../machine_learning/Dockerfile
# volumes:
# - ../machine_learning/app:/code/app
# - ${UPLOAD_LOCATION}:/code/app/upload
# ports:
# - 2285:8000
# expose:
# - "8000"
# depends_on:
# - database
# networks:
# - immich_network
networks:
immich_network:
volumes:
pgdata:
pgdata:

3
fastlane/README.md Normal file
View File

@@ -0,0 +1,3 @@
This directory exists because of the F-Droid build process. F-Droid is using the same directory structure as Fastlane for the app metadata.
Because F-Droid expects the metadata to be located in the root of the repository we need to have this symlink.

1
fastlane/metadata Symbolic link
View File

@@ -0,0 +1 @@
../mobile/android/fastlane/metadata

View File

@@ -1,7 +1,4 @@
##################################
# DEVELOPMENT
##################################
FROM node:16-bullseye-slim AS development
FROM node:16-bullseye-slim
ARG DEBIAN_FRONTEND=noninteractive
@@ -17,27 +14,3 @@ RUN npm install
COPY . .
RUN npm run build
#################################
# PRODUCTION
#################################
FROM node:16-bullseye-slim AS production
ARG DEBIAN_FRONTEND=noninteractive
ARG NODE_ENV=production
ENV NODE_ENV=${NODE_ENV}
WORKDIR /usr/src/app
COPY package.json package-lock.json ./
RUN apt-get update
RUN apt-get install gcc g++ make cmake python3 python3-pip ffmpeg -y
RUN npm install --only=production
COPY . .
COPY --from=development /usr/src/app/dist ./dist
CMD ["node", "dist/main"]

View File

@@ -0,0 +1,2 @@
# npm run typeorm migration:run
npm run build && npm run start:prod

View File

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

View File

@@ -1,10 +1,25 @@
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { Logger } from '@nestjs/common';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
await app.listen(3001);
await app.listen(3001, () => {
if (process.env.NODE_ENV == 'development') {
Logger.log(
'Running Immich Microservices in DEVELOPMENT environment',
'IMMICH MICROSERVICES',
);
}
if (process.env.NODE_ENV == 'production') {
Logger.log(
'Running Immich Microservices in PRODUCTION environment',
'IMMICH MICROSERVICES',
);
}
});
}
bootstrap();

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,2 @@
* Accepting webp file format
* Fixed backup stop when an asset is of wrong file type. The app will now skip that asset and try its best to perform the backup operation on the rest of the assets.

View File

@@ -0,0 +1 @@
* Added curated locations and objects on the search page

View File

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

View File

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

View File

@@ -0,0 +1,21 @@
This is a client app for the self-hostable Immich Server (which can be found with the app's source repo). You will need to run/manage the server on your own in order to use the app.
Once set up, this app can be used as photo and video backup solution directly from your mobile phone.
<b>Features:</b>
* Upload and view assets(videos/images).
* Multi-user supported.
* Quick navigation with drag scroll bar.
* Auto Backup.
* Support HEIC/HEIF Backup.
* Extract and display EXIF info.
* Real-time render from multi-device upload event.
* Image Tagging/Classification based on ImageNet dataset
* Object detection based on COCO SSD.
* Search assets based on tags and exif data (lens, make, model, orientation)
* Upload assets from your local computer/server using <a href='https://www.npmjs.com/package/immich' target='_blank' rel='nofollow'>immich cli tools</a>
* [Optional] Reserve geocoding using Mapbox (Generous free-tier of 100,000 search/month)
* Show asset's location information on map (OpenStreetMap).
* Show curated places on the search page
* Show curated objects on the search page

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 173 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 299 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 517 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 185 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 185 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

View File

@@ -0,0 +1 @@
This is a client app for the self-hostable Immich Server

View File

@@ -0,0 +1 @@
Immich

View File

@@ -4,3 +4,4 @@ distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip
distributionSha256Sum=0080de8491f0918e4f529a6db6820fa0b9e818ee2386117f4394f95feb1d5583

Binary file not shown.

Binary file not shown.

BIN
mobile/fonts/WorkSans.ttf Normal file

Binary file not shown.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -37,24 +37,29 @@ class BackupNotifier extends StateNotifier<BackUpState> {
Ref? ref;
final BackupService _backupService = BackupService();
final ServerInfoService _serverInfoService = ServerInfoService();
final StreamController _onAssetBackupStreamCtrl = StreamController.broadcast();
final StreamController _onAssetBackupStreamCtrl =
StreamController.broadcast();
void getBackupInfo() async {
_updateServerInfo();
List<AssetPathEntity> list = await PhotoManager.getAssetPathList(onlyAll: true, type: RequestType.common);
List<AssetPathEntity> list = await PhotoManager.getAssetPathList(
onlyAll: true, type: RequestType.common);
List<String> didBackupAsset = await _backupService.getDeviceBackupAsset();
if (list.isEmpty) {
debugPrint("No Asset On Device");
state = state.copyWith(
backupProgress: BackUpProgressEnum.idle, totalAssetCount: 0, assetOnDatabase: didBackupAsset.length);
backupProgress: BackUpProgressEnum.idle,
totalAssetCount: 0,
assetOnDatabase: didBackupAsset.length);
return;
}
int totalAsset = list[0].assetCount;
state = state.copyWith(totalAssetCount: totalAsset, assetOnDatabase: didBackupAsset.length);
state = state.copyWith(
totalAssetCount: totalAsset, assetOnDatabase: didBackupAsset.length);
}
void startBackupProcess() async {
@@ -67,8 +72,8 @@ class BackupNotifier extends StateNotifier<BackUpState> {
await PhotoManager.clearFileCache();
// await PhotoManager.presentLimited();
// Gather assets info
List<AssetPathEntity> list =
await PhotoManager.getAssetPathList(hasAll: true, onlyAll: true, type: RequestType.common);
List<AssetPathEntity> list = await PhotoManager.getAssetPathList(
hasAll: true, onlyAll: true, type: RequestType.common);
// Get device assets info from database
// Compare and find different assets that has not been backing up
@@ -78,14 +83,18 @@ class BackupNotifier extends StateNotifier<BackUpState> {
if (list.isEmpty) {
debugPrint("No Asset On Device - Abort Backup Process");
state = state.copyWith(
backupProgress: BackUpProgressEnum.idle, totalAssetCount: 0, assetOnDatabase: backupAsset.length);
backupProgress: BackUpProgressEnum.idle,
totalAssetCount: 0,
assetOnDatabase: backupAsset.length);
return;
}
int totalAsset = list[0].assetCount;
List<AssetEntity> currentAssets = await list[0].getAssetListRange(start: 0, end: totalAsset);
List<AssetEntity> currentAssets =
await list[0].getAssetListRange(start: 0, end: totalAsset);
state = state.copyWith(totalAssetCount: totalAsset, assetOnDatabase: backupAsset.length);
state = state.copyWith(
totalAssetCount: totalAsset, assetOnDatabase: backupAsset.length);
// Remove item that has already been backed up
for (var backupAssetId in backupAsset) {
currentAssets.removeWhere((e) => e.id == backupAssetId);
@@ -97,9 +106,10 @@ class BackupNotifier extends StateNotifier<BackUpState> {
state = state.copyWith(backingUpAssetCount: currentAssets.length);
// Perform Packup
// Perform Backup
state = state.copyWith(cancelToken: CancelToken());
_backupService.backupAsset(currentAssets, state.cancelToken, _onAssetUploaded, _onUploadProgress);
_backupService.backupAsset(currentAssets, state.cancelToken,
_onAssetUploaded, _onUploadProgress);
} else {
PhotoManager.openSetting();
}
@@ -107,22 +117,26 @@ class BackupNotifier extends StateNotifier<BackUpState> {
void cancelBackup() {
state.cancelToken.cancel('Cancel Backup');
state = state.copyWith(backupProgress: BackUpProgressEnum.idle, progressInPercentage: 0.0);
state = state.copyWith(
backupProgress: BackUpProgressEnum.idle, progressInPercentage: 0.0);
}
void _onAssetUploaded(String deviceAssetId, String deviceId) {
state =
state.copyWith(backingUpAssetCount: state.backingUpAssetCount - 1, assetOnDatabase: state.assetOnDatabase + 1);
state = state.copyWith(
backingUpAssetCount: state.backingUpAssetCount - 1,
assetOnDatabase: state.assetOnDatabase + 1);
if (state.backingUpAssetCount == 0) {
state = state.copyWith(backupProgress: BackUpProgressEnum.done, progressInPercentage: 0.0);
state = state.copyWith(
backupProgress: BackUpProgressEnum.done, progressInPercentage: 0.0);
}
_updateServerInfo();
}
void _onUploadProgress(int sent, int total) {
state = state.copyWith(progressInPercentage: (sent.toDouble() / total.toDouble() * 100));
state = state.copyWith(
progressInPercentage: (sent.toDouble() / total.toDouble() * 100));
}
void _updateServerInfo() async {
@@ -156,7 +170,8 @@ class BackupNotifier extends StateNotifier<BackUpState> {
}
// Check if this device is enable backup by the user
if ((authState.deviceInfo.deviceId == authState.deviceId) && authState.deviceInfo.isAutoBackup) {
if ((authState.deviceInfo.deviceId == authState.deviceId) &&
authState.deviceInfo.isAutoBackup) {
// check if backup is alreayd in process - then return
if (state.backupProgress == BackUpProgressEnum.inProgress) {
debugPrint("[resumeBackup] Backup is already in progress - abort");
@@ -173,6 +188,7 @@ class BackupNotifier extends StateNotifier<BackUpState> {
}
}
final backupProvider = StateNotifierProvider<BackupNotifier, BackUpState>((ref) {
final backupProvider =
StateNotifierProvider<BackupNotifier, BackUpState>((ref) {
return BackupNotifier(ref: ref);
});

View File

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

View File

@@ -73,7 +73,7 @@ class BackupService {
});
// Build thumbnail multipart data
var thumbnailData = await entity.thumbDataWithSize(1280, 720);
var thumbnailData = await entity.thumbnailDataWithSize(const ThumbnailSize(720, 1280));
if (thumbnailData != null) {
thumbnailUploadData = MultipartFile.fromBytes(
List.from(thumbnailData),
@@ -112,7 +112,10 @@ class BackupService {
}
} on DioError catch (e) {
debugPrint("DioError backupAsset: ${e.response}");
break;
if (e.type == DioErrorType.cancel || e.type == DioErrorType.other) {
return;
}
continue;
} catch (e) {
debugPrint("ERROR backupAsset: ${e.toString()}");
continue;

View File

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

View File

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

View File

@@ -6,6 +6,7 @@ import 'package:immich_mobile/modules/login/models/authentication_state.model.da
import 'package:immich_mobile/shared/models/backup_state.model.dart';
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
import 'package:immich_mobile/shared/providers/backup.provider.dart';
import 'package:immich_mobile/shared/providers/websocket.provider.dart';
import 'package:percent_indicator/linear_percent_indicator.dart';
class BackupControllerPage extends HookConsumerWidget {
@@ -23,6 +24,7 @@ class BackupControllerPage extends HookConsumerWidget {
ref.read(backupProvider.notifier).getBackupInfo();
}
ref.watch(websocketProvider.notifier).stopListenToEvent('on_upload_success');
return null;
}, []);
@@ -85,13 +87,16 @@ class BackupControllerPage extends HookConsumerWidget {
style: TextStyle(fontSize: 14),
)
: Container(),
OutlinedButton(
onPressed: () {
isAutoBackup
? ref.watch(authenticationProvider.notifier).setAutoBackup(false)
: ref.watch(authenticationProvider.notifier).setAutoBackup(true);
},
child: Text("Turn $backupBtnText Backup"),
Padding(
padding: const EdgeInsets.only(top: 8.0),
child: OutlinedButton(
onPressed: () {
isAutoBackup
? ref.watch(authenticationProvider.notifier).setAutoBackup(false)
: ref.watch(authenticationProvider.notifier).setAutoBackup(true);
},
child: Text("Turn $backupBtnText Backup", style: const TextStyle(fontWeight: FontWeight.bold)),
),
)
],
),
@@ -107,6 +112,7 @@ class BackupControllerPage extends HookConsumerWidget {
),
leading: IconButton(
onPressed: () {
ref.watch(websocketProvider.notifier).listenUploadEvent();
AutoRouter.of(context).pop(true);
},
icon: const Icon(Icons.arrow_back_ios_rounded)),

View File

@@ -35,6 +35,9 @@ class FileHelper {
case 'dng':
return {"type": "image", "subType": "dng"};
case 'webp':
return {"type": "image", "subType": "webp"};
default:
return {"type": "unsupport", "subType": "unsupport"};
}

View File

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

View File

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

View File

@@ -1,7 +1,4 @@
##################################
# DEVELOPMENT
##################################
FROM node:16-alpine3.14 AS development
FROM node:16-alpine3.14
ARG DEBIAN_FRONTEND=noninteractive
@@ -15,27 +12,4 @@ RUN npm install
COPY . .
RUN npm run build
#################################
# PRODUCTION
#################################
FROM node:16-alpine3.14 AS production
ARG DEBIAN_FRONTEND=noninteractive
ARG NODE_ENV=production
ENV NODE_ENV=${NODE_ENV}
WORKDIR /usr/src/app
COPY package.json package-lock.json ./
RUN apk add --update-cache build-base python3
RUN npm install --only=production
COPY . .
COPY --from=development /usr/src/app/dist ./dist
CMD ["node", "dist/main"]
RUN npm run build

View File

@@ -1,2 +1,2 @@
# npm run typeorm migration:run
npm run start:dev
npm run build && npm run start:prod

View File

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

View File

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

View File

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

View File

@@ -46,6 +46,8 @@ import { CommunicationModule } from './api-v1/communication/communication.module
})
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer): void {
// consumer.apply(AppLoggerMiddleware).forRoutes('*');
if (process.env.NODE_ENV == 'development') {
consumer.apply(AppLoggerMiddleware).forRoutes('*');
}
}
}

View File

@@ -12,7 +12,7 @@ export const multerConfig = {
export const multerOption: MulterOptions = {
fileFilter: (req: Request, file: any, cb: any) => {
if (file.mimetype.match(/\/(jpg|jpeg|png|gif|mp4|x-msvideo|quicktime|heic|heif|dng)$/)) {
if (file.mimetype.match(/\/(jpg|jpeg|png|gif|mp4|x-msvideo|quicktime|heic|heif|dng|webp)$/)) {
cb(null, true);
} else {
cb(new HttpException(`Unsupported file type ${extname(file.originalname)}`, HttpStatus.BAD_REQUEST), false);

View File

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

View File

@@ -1,3 +1,4 @@
import { Logger } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import { NestExpressApplication } from '@nestjs/platform-express';
import { AppModule } from './app.module';
@@ -10,6 +11,14 @@ async function bootstrap() {
app.useWebSocketAdapter(new RedisIoAdapter(app));
await app.listen(3000);
await app.listen(3000, () => {
if (process.env.NODE_ENV == 'development') {
Logger.log('Running Immich Server in DEVELOPMENT environment', 'IMMICH SERVER');
}
if (process.env.NODE_ENV == 'production') {
Logger.log('Running Immich Server in PRODUCTION environment', 'IMMICH SERVER');
}
});
}
bootstrap();