Compare commits

...

19 Commits

Author SHA1 Message Date
Jonas Janz
97aed8ef23 fix(nginx): revert nginx image to support arm/v7 (#692) 2022-09-14 13:36:29 -05:00
Alex
0ee3fe9157 Update install.sh to use latest released tag 2022-09-14 11:07:37 -05:00
Alex
434770155f Up version for release 2022-09-14 10:27:34 -05:00
Alex
7e8bf94543 fix/cache read write error ios16 (#691)
* Fix(mobile) cache read/write issue, cannot load image on ios16

* Update
2022-09-14 10:18:25 -05:00
Zack Pollard
8d8944705c Merge pull request #690 from beune/fix-typo
Fix typo
2022-09-14 13:16:47 +01:00
Pim Beune
7c9c1a5169 Fix typo 2022-09-14 13:53:34 +02:00
Jonas Janz
1a6c16d8ea breaking(setup): use non-root image for immich-proxy (#651)
* feat(nginx): use non-root container for immich-proxy

Signed-off-by: PixelJonas <5434875+PixelJonas@users.noreply.github.com>

* re-add test env

* feat(nginx): add correct port for staging

* add the new port to the default docker-compose.yml

Signed-off-by: PixelJonas <5434875+PixelJonas@users.noreply.github.com>
2022-09-13 21:50:10 -05:00
Alex
ccf792f9d3 fix(server): mismatch createdAt value in table and table (#688) 2022-09-13 20:12:42 -05:00
Fynn Petersen-Frey
789bc8563c fix Android BackgroundServiceStartNotAllowedException (#687) 2022-09-13 20:12:31 -05:00
Manuel
99a50f70dd readme: add app store links (#689) 2022-09-13 18:23:27 -05:00
Alex Tran
9bef411056 Up server version: 2022-09-13 12:14:36 -05:00
Alex
e79e92c60f Added Log level to background service (#685) 2022-09-13 12:09:57 -05:00
Alex
858ad43d3b fix(server): harden inserting process, self-healing timestamp info on bad timestamp (#682)
* fix(server): harden inserting process, self-healing timestamp info
2022-09-12 23:35:44 -05:00
Alex
5761765ea7 fix(server): remove album thumbnail when the asset is deleted from the database (#681) 2022-09-12 22:06:52 -05:00
Thanh Pham
6abc733763 fix(web): datetime display and add TZ into environment (#618)
* fix(web): timezone

* doc(): update readme.md

* feat(web): keep using UTC timezone in default

* chore(): update doc and remove debug code

* chore(): update readme.md

* Move timezone into to .env.example

* Run prettier check

Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
2022-09-12 14:40:18 -05:00
Alex Tran
4271e24e59 Up version for release 2022-09-11 16:05:53 -05:00
Alex
9e4ed2214b fix(web): incorrect shared album count (#677) 2022-09-11 10:07:04 -05:00
Alex
011332e509 fix(mobile) memory leaked causes app to crash when swiping (#673)
* Dispose image provider when swiping away from the asset
2022-09-11 09:56:26 -05:00
Alex
5403ef4d84 Fix(mobile) oversize play button (#672) 2022-09-11 00:25:04 -05:00
55 changed files with 518 additions and 299 deletions

View File

@@ -147,6 +147,7 @@ wget -O .env https://raw.githubusercontent.com/immich-app/immich/main/docker/.en
* Populate `UPLOAD_LOCATION` as prefered location for storing backup assets.
* Populate a secret value for `JWT_SECRET`, you can use this command: `openssl rand -base64 128`
* [Optional] Populate Mapbox value to use reverse geocoding.
* [Optional] Populate `TZ` as your timezone, default is `Etc/UTC`.
### Step 3 - Start the containers
@@ -185,9 +186,9 @@ docker-compose pull && docker-compose up -d
| F-Droid | Google Play | iOS |
| - | - | - |
| <a href="https://f-droid.org/packages/app.alextran.immich"><img src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png" alt="Get it on F-Droid" height="80"></a> | <p align="left"> <img src="design/google-play-qr-code.png" width="200" title="Google Play Store"> <p/> | <p align="left"> <img src="design/ios-qr-code.png" width="200" title="Apple App Store"> <p/> |
| <a href="https://f-droid.org/packages/app.alextran.immich"><img src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png" alt="Get it on F-Droid" height="80"></a> | <p align="left"> <a href="https://play.google.com/store/apps/details?id=app.alextran.immich"><img src="design/google-play-qr-code.png" width="200" title="Google Play Store"></a> <p/> | <p align="left"> <a href="https://apps.apple.com/us/app/immich/id1613945652"><img src="design/ios-qr-code.png" width="200" title="Apple App Store"></a> <p/> |
> *The App version might be lagging behind the latest release due to the review process.*
> *The Play/App Store version might be lagging behind the latest release due to the review process.*
<br/>

View File

@@ -36,6 +36,11 @@ REDIS_HOSTNAME=immich_redis
UPLOAD_LOCATION=absolute_location_on_your_machine_where_you_want_to_store_the_backup
###################################################################################
# Log message level - [simple|verbose]
###################################################################################
LOG_LEVEL=simple
###################################################################################
@@ -63,4 +68,12 @@ MAPBOX_KEY=
# Custom message on the login page, should be written in HTML form.
# For example PUBLIC_LOGIN_PAGE_MESSAGE="This is a demo instance of Immich.<br><br>Email: <i>demo@demo.de</i><br>Password: <i>demo</i>"
PUBLIC_LOGIN_PAGE_MESSAGE=
PUBLIC_LOGIN_PAGE_MESSAGE=
# For correctly display your local time zone on the web, you can set the time zone here.
# Should work fine by default value, however, in case of incorrect timezone in EXIF, this value
# should be set to the correct timezone.
# Command to get timezone:
# - Linux: curl -s http://ip-api.com/json/ | grep -oP '(?<=timezone":")(.*?)(?=")'
# TZ=Etc/UTC

View File

@@ -19,4 +19,4 @@ ENABLE_MAPBOX=false
# WEB
MAPBOX_KEY=
VITE_SERVER_ENDPOINT=http://localhost:2283/api
VITE_SERVER_ENDPOINT=http://localhost:2283/api

View File

@@ -102,8 +102,7 @@ services:
context: ../nginx
dockerfile: Dockerfile
ports:
- 2283:80
- 2284:443
- 2283:8080
logging:
driver: none
depends_on:

View File

@@ -72,8 +72,7 @@ services:
container_name: immich_proxy
image: altran1502/immich-proxy:staging
ports:
- 2283:80
- 2284:443
- 2283:8080
logging:
driver: none
depends_on:

View File

@@ -47,6 +47,8 @@ services:
entrypoint: ["/bin/sh", "./entrypoint.sh"]
env_file:
- .env
environment:
- PUBLIC_TZ=${TZ}
restart: always
redis:
@@ -72,7 +74,7 @@ services:
container_name: immich_proxy
image: altran1502/immich-proxy:release
ports:
- 2283:80
- 2283:8080
logging:
driver: none
depends_on:

View File

@@ -2,10 +2,19 @@ echo "Starting Immich installation..."
ip_address=$(hostname -I | awk '{print $1}')
release_version=$(curl --silent "https://api.github.com/repos/immich-app/immich/releases/latest" |
grep '"tag_name":' |
sed -E 's/.*"([^"]+)".*/\1/')
RED='\033[0;31m'
GREEN='\032[0;31m'
NC='\033[0m' # No Color
get_release_version() {
curl --silent "https://api.github.com/repos/immich-app/immich/releases/latest" | # Get latest release from GitHub api
grep '"tag_name":' | # Get tag line
sed -E 's/.*"([^"]+)".*/\1/' # Pluck JSON value
}
create_immich_directory() {
echo "Creating Immich directory..."
mkdir -p ./immich-app/immich-data
@@ -13,12 +22,12 @@ create_immich_directory() {
download_docker_compose_file() {
echo "Downloading docker-compose.yml..."
curl -L https://raw.githubusercontent.com/immich-app/immich/main/docker/docker-compose.yml -o ./immich-app/docker-compose.yml >/dev/null 2>&1
curl -L https://raw.githubusercontent.com/immich-app/immich/$release_version/docker/docker-compose.yml -o ./immich-app/docker-compose.yml >/dev/null 2>&1
}
download_dot_env_file() {
echo "Downloading .env file..."
curl -L https://raw.githubusercontent.com/immich-app/immich/main/docker/.env.example -o ./immich-app/.env >/dev/null 2>&1
curl -L https://raw.githubusercontent.com/immich-app/immich/$release_version/docker/.env.example -o ./immich-app/.env >/dev/null 2>&1
}
populate_upload_location() {
@@ -41,9 +50,9 @@ populate_upload_location() {
start_docker_compose() {
echo "Starting Immich's docker containers"
if docker compose &> /dev/null; then
if docker compose &>/dev/null; then
docker_bin="docker compose"
elif docker-compose &> /dev/null; then
elif docker-compose &>/dev/null; then
docker_bin="docker-compose"
else
echo 'Cannot find `docker compose` or `docker-compose`.'
@@ -64,7 +73,7 @@ show_friendly_message() {
echo "You can access the website at http://$ip_address:2283 and the server URL for the mobile app is http://$ip_address:2283/api"
echo "The backup (or upload) location is $upload_location"
echo "---------------------------------------------------"
echo "If you want to confgure custom information of the server, including the database, Redis information, or the backup (or upload) location, etc.
echo "If you want to configure custom information of the server, including the database, Redis information, or the backup (or upload) location, etc.
1. First bring down the containers with the command 'docker-compose down' in the immich-app directory,

View File

@@ -14,7 +14,12 @@ class MainActivity: FlutterActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
startService(Intent(getBaseContext(), AppClearedService::class.java));
try {
startService(Intent(getBaseContext(), AppClearedService::class.java));
} catch (e: Exception) {
// startService must not be called when app is in background (crashes app)
// there is nothing we can do
}
}
}

View File

@@ -30,8 +30,8 @@ platform :android do
task: 'bundle',
build_type: 'Release',
properties: {
"android.injected.version.code" => 40,
"android.injected.version.name" => "1.28.2",
"android.injected.version.code" => 42,
"android.injected.version.name" => "1.29.0",
}
)
upload_to_play_store(skip_upload_apk: true, skip_upload_images: true, skip_upload_screenshots: true, aab: '../build/app/outputs/bundle/release/app-release.aab')

View File

@@ -0,0 +1,2 @@
* Fixed oversize play button on video
* Fixed app crashing when swipe between assets

View File

@@ -0,0 +1,2 @@
* Fixed Android BackgroundServiceStartNotAllowedException
* Restore old cache mechanism

View File

@@ -5,17 +5,17 @@
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000222">
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.00023">
</testcase>
<testcase classname="fastlane.lanes" name="1: bundleRelease" time="55.311329">
<testcase classname="fastlane.lanes" name="1: bundleRelease" time="58.722434">
</testcase>
<testcase classname="fastlane.lanes" name="2: upload_to_play_store" time="30.070842">
<testcase classname="fastlane.lanes" name="2: upload_to_play_store" time="36.768014">
</testcase>

View File

@@ -360,11 +360,11 @@
CODE_SIGN_ENTITLEMENTS = Runner/RunnerProfile.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 52;
CURRENT_PROJECT_VERSION = 57;
DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@@ -495,11 +495,11 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 52;
CURRENT_PROJECT_VERSION = 57;
DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@@ -522,11 +522,11 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 52;
CURRENT_PROJECT_VERSION = 57;
DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",

View File

@@ -17,11 +17,11 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.27.0</string>
<string>1.29.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>52</string>
<string>57</string>
<key>LSRequiresIPhoneOS</key>
<true />
<key>MGLMapboxMetricsEnabledSettingShownInApp</key>

View File

@@ -19,7 +19,7 @@ platform :ios do
desc "iOS Beta"
lane :beta do
increment_version_number(
version_number: "1.28.2"
version_number: "1.29.0"
)
increment_build_number(
build_number: latest_testflight_build_number + 1,

View File

@@ -5,32 +5,34 @@
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000269">
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000204">
</testcase>
<testcase classname="fastlane.lanes" name="1: increment_version_number" time="0.499192">
<testcase classname="fastlane.lanes" name="1: increment_version_number" time="1.04331">
</testcase>
<testcase classname="fastlane.lanes" name="2: latest_testflight_build_number" time="30.057077">
<testcase classname="fastlane.lanes" name="2: latest_testflight_build_number" time="5.166831">
</testcase>
<testcase classname="fastlane.lanes" name="3: increment_build_number" time="0.438506">
<testcase classname="fastlane.lanes" name="3: increment_build_number" time="0.411879">
</testcase>
<testcase classname="fastlane.lanes" name="4: build_app" time="91.259106">
<testcase classname="fastlane.lanes" name="4: build_app" time="71.643901">
</testcase>
<testcase classname="fastlane.lanes" name="5: upload_to_testflight" time="102.092139">
<testcase classname="fastlane.lanes" name="5: upload_to_testflight" time="7.590505">
<failure message="/opt/homebrew/Cellar/fastlane/2.209.0/libexec/gems/fastlane-2.209.0/fastlane/lib/fastlane/actions/actions_helper.rb:67:in `execute_action&apos;&#10;/opt/homebrew/Cellar/fastlane/2.209.0/libexec/gems/fastlane-2.209.0/fastlane/lib/fastlane/runner.rb:255:in `block in execute_action&apos;&#10;/opt/homebrew/Cellar/fastlane/2.209.0/libexec/gems/fastlane-2.209.0/fastlane/lib/fastlane/runner.rb:229:in `chdir&apos;&#10;/opt/homebrew/Cellar/fastlane/2.209.0/libexec/gems/fastlane-2.209.0/fastlane/lib/fastlane/runner.rb:229:in `execute_action&apos;&#10;/opt/homebrew/Cellar/fastlane/2.209.0/libexec/gems/fastlane-2.209.0/fastlane/lib/fastlane/runner.rb:157:in `trigger_action_by_name&apos;&#10;/opt/homebrew/Cellar/fastlane/2.209.0/libexec/gems/fastlane-2.209.0/fastlane/lib/fastlane/fast_file.rb:159:in `method_missing&apos;&#10;Fastfile:30:in `block (2 levels) in parsing_binding&apos;&#10;/opt/homebrew/Cellar/fastlane/2.209.0/libexec/gems/fastlane-2.209.0/fastlane/lib/fastlane/lane.rb:33:in `call&apos;&#10;/opt/homebrew/Cellar/fastlane/2.209.0/libexec/gems/fastlane-2.209.0/fastlane/lib/fastlane/runner.rb:49:in `block in execute&apos;&#10;/opt/homebrew/Cellar/fastlane/2.209.0/libexec/gems/fastlane-2.209.0/fastlane/lib/fastlane/runner.rb:45:in `chdir&apos;&#10;/opt/homebrew/Cellar/fastlane/2.209.0/libexec/gems/fastlane-2.209.0/fastlane/lib/fastlane/runner.rb:45:in `execute&apos;&#10;/opt/homebrew/Cellar/fastlane/2.209.0/libexec/gems/fastlane-2.209.0/fastlane/lib/fastlane/lane_manager.rb:47:in `cruise_lane&apos;&#10;/opt/homebrew/Cellar/fastlane/2.209.0/libexec/gems/fastlane-2.209.0/fastlane/lib/fastlane/command_line_handler.rb:36:in `handle&apos;&#10;/opt/homebrew/Cellar/fastlane/2.209.0/libexec/gems/fastlane-2.209.0/fastlane/lib/fastlane/commands_generator.rb:110:in `block (2 levels) in run&apos;&#10;/opt/homebrew/Cellar/fastlane/2.209.0/libexec/gems/commander-4.6.0/lib/commander/command.rb:187:in `call&apos;&#10;/opt/homebrew/Cellar/fastlane/2.209.0/libexec/gems/commander-4.6.0/lib/commander/command.rb:157:in `run&apos;&#10;/opt/homebrew/Cellar/fastlane/2.209.0/libexec/gems/commander-4.6.0/lib/commander/runner.rb:444:in `run_active_command&apos;&#10;/opt/homebrew/Cellar/fastlane/2.209.0/libexec/gems/fastlane-2.209.0/fastlane_core/lib/fastlane_core/ui/fastlane_runner.rb:124:in `run!&apos;&#10;/opt/homebrew/Cellar/fastlane/2.209.0/libexec/gems/commander-4.6.0/lib/commander/delegates.rb:18:in `run!&apos;&#10;/opt/homebrew/Cellar/fastlane/2.209.0/libexec/gems/fastlane-2.209.0/fastlane/lib/fastlane/commands_generator.rb:354:in `run&apos;&#10;/opt/homebrew/Cellar/fastlane/2.209.0/libexec/gems/fastlane-2.209.0/fastlane/lib/fastlane/commands_generator.rb:43:in `start&apos;&#10;/opt/homebrew/Cellar/fastlane/2.209.0/libexec/gems/fastlane-2.209.0/fastlane/lib/fastlane/cli_tools_distributor.rb:123:in `take_off&apos;&#10;/opt/homebrew/Cellar/fastlane/2.209.0/libexec/gems/fastlane-2.209.0/bin/fastlane:23:in `&lt;top (required)&gt;&apos;&#10;/opt/homebrew/Cellar/fastlane/2.209.0/libexec/bin/fastlane:25:in `load&apos;&#10;/opt/homebrew/Cellar/fastlane/2.209.0/libexec/bin/fastlane:25:in `&lt;main&gt;&apos;&#10;&#10;Could not find transporter at /Applications/Xcode.app/Contents/Developer/. Please make sure you set the correct path to your Xcode installation." />
</testcase>

View File

@@ -7,7 +7,6 @@ import 'package:flutter/material.dart';
import 'package:hive/hive.dart';
import 'package:immich_mobile/constants/hive_box.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/shared/services/cache.service.dart';
import 'package:immich_mobile/utils/image_url_builder.dart';
import 'package:openapi/api.dart';
@@ -15,11 +14,9 @@ class AlbumThumbnailCard extends StatelessWidget {
const AlbumThumbnailCard({
Key? key,
required this.album,
required this.cacheService,
}) : super(key: key);
final AlbumResponseDto album;
final CacheService cacheService;
@override
Widget build(BuildContext context) {
@@ -39,7 +36,6 @@ class AlbumThumbnailCard extends StatelessWidget {
ClipRRect(
borderRadius: BorderRadius.circular(8),
child: CachedNetworkImage(
cacheManager: cacheService.getCache(CacheType.albumThumbnail),
memCacheHeight: max(400, cardSize.toInt() * 3),
width: cardSize,
height: cardSize,

View File

@@ -1,15 +1,12 @@
import 'package:auto_route/auto_route.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/hive_box.dart';
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
import 'package:immich_mobile/modules/album/providers/asset_selection.provider.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/shared/services/cache.service.dart';
import 'package:immich_mobile/utils/image_url_builder.dart';
import 'package:openapi/api.dart';
@@ -17,13 +14,11 @@ class AlbumViewerThumbnail extends HookConsumerWidget {
final AssetResponseDto asset;
final List<AssetResponseDto> assetList;
final bool showStorageIndicator;
final BaseCacheManager? cacheManager;
const AlbumViewerThumbnail({
Key? key,
required this.asset,
required this.assetList,
this.cacheManager,
this.showStorageIndicator = true,
}) : super(key: key);
@@ -126,7 +121,6 @@ class AlbumViewerThumbnail extends HookConsumerWidget {
return Container(
decoration: BoxDecoration(border: drawBorderColor()),
child: CachedNetworkImage(
cacheManager: cacheManager,
cacheKey: asset.id,
width: 300,
height: 300,

View File

@@ -1,11 +1,9 @@
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';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/hive_box.dart';
import 'package:immich_mobile/modules/album/providers/asset_selection.provider.dart';
import 'package:immich_mobile/shared/services/cache.service.dart';
import 'package:immich_mobile/utils/image_url_builder.dart';
import 'package:openapi/api.dart';
@@ -24,7 +22,6 @@ class SelectionThumbnailImage extends HookConsumerWidget {
var newAssetsForAlbum =
ref.watch(assetSelectionProvider).selectedAdditionalAssetsForAlbum;
var isAlbumExist = ref.watch(assetSelectionProvider).isAlbumExist;
final cacheService = ref.watch(cacheServiceProvider);
Widget _buildSelectionIcon(AssetResponseDto asset) {
var isSelected = selectedAsset.map((item) => item.id).contains(asset.id);
@@ -114,7 +111,6 @@ class SelectionThumbnailImage extends HookConsumerWidget {
Container(
decoration: BoxDecoration(border: drawBorderColor()),
child: CachedNetworkImage(
cacheManager: cacheService.getCache(CacheType.thumbnail),
cacheKey: asset.id,
width: 150,
height: 150,

View File

@@ -1,10 +1,8 @@
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';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/hive_box.dart';
import 'package:immich_mobile/shared/services/cache.service.dart';
import 'package:immich_mobile/utils/image_url_builder.dart';
import 'package:openapi/api.dart';
@@ -16,7 +14,6 @@ class SharedAlbumThumbnailImage extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final cacheService = ref.watch(cacheServiceProvider);
var box = Hive.box(userInfoBox);
return GestureDetector(
@@ -26,7 +23,6 @@ class SharedAlbumThumbnailImage extends HookConsumerWidget {
child: Stack(
children: [
CachedNetworkImage(
cacheManager: cacheService.getCache(CacheType.thumbnail),
cacheKey: asset.id,
width: 500,
height: 500,

View File

@@ -16,7 +16,6 @@ import 'package:immich_mobile/modules/album/ui/album_viewer_thumbnail.dart';
import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart';
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/shared/services/cache.service.dart';
import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
import 'package:immich_mobile/shared/ui/immich_sliver_persistent_app_bar_delegate.dart';
import 'package:immich_mobile/shared/views/immich_loading_overlay.dart';
@@ -192,7 +191,6 @@ class AlbumViewerPage extends HookConsumerWidget {
final appSettingService = ref.watch(appSettingsServiceProvider);
final bool showStorageIndicator =
appSettingService.getSetting(AppSettingsEnum.storageIndicator);
final cacheService = ref.watch(cacheServiceProvider);
if (albumInfo.assets.isNotEmpty) {
return SliverPadding(
@@ -207,7 +205,6 @@ class AlbumViewerPage extends HookConsumerWidget {
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
return AlbumViewerThumbnail(
cacheManager: cacheService.getCache(CacheType.thumbnail),
asset: albumInfo.assets[index],
assetList: albumInfo.assets,
showStorageIndicator: showStorageIndicator,

View File

@@ -6,7 +6,6 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/album/providers/album.provider.dart';
import 'package:immich_mobile/modules/album/ui/album_thumbnail_card.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/shared/services/cache.service.dart';
class LibraryPage extends HookConsumerWidget {
const LibraryPage({Key? key}) : super(key: key);
@@ -14,7 +13,6 @@ class LibraryPage extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final albums = ref.watch(albumProvider);
final cacheService = ref.watch(cacheServiceProvider);
useEffect(
() {
@@ -104,7 +102,6 @@ class LibraryPage extends HookConsumerWidget {
_buildCreateAlbumButton(),
for (var album in albums)
AlbumThumbnailCard(
cacheService: cacheService,
album: album,
),
],

View File

@@ -9,7 +9,6 @@ import 'package:immich_mobile/constants/hive_box.dart';
import 'package:immich_mobile/modules/album/providers/shared_album.provider.dart';
import 'package:immich_mobile/modules/album/ui/sharing_sliver_appbar.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/shared/services/cache.service.dart';
import 'package:immich_mobile/utils/image_url_builder.dart';
import 'package:openapi/api.dart';
@@ -21,7 +20,6 @@ class SharingPage extends HookConsumerWidget {
var box = Hive.box(userInfoBox);
var thumbnailRequestUrl = '${box.get(serverEndpointKey)}/asset/thumbnail';
final List<AlbumResponseDto> sharedAlbums = ref.watch(sharedAlbumProvider);
final CacheService cacheService = ref.watch(cacheServiceProvider);
useEffect(
() {
@@ -47,8 +45,6 @@ class SharingPage extends HookConsumerWidget {
height: 60,
memCacheHeight: 200,
fit: BoxFit.cover,
cacheManager:
cacheService.getCache(CacheType.sharedAlbumThumbnail),
imageUrl: getAlbumThumbnailUrl(album),
cacheKey: album.albumThumbnailAssetId,
httpHeaders: {

View File

@@ -1,7 +1,6 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
import 'package:photo_view/photo_view.dart';
enum _RemoteImageStatus { empty, thumbnail, preview, full }
@@ -12,6 +11,9 @@ class _RemotePhotoViewState extends State<RemotePhotoView> {
bool _zoomedIn = false;
static const int swipeThreshold = 100;
late CachedNetworkImageProvider fullProvider;
late CachedNetworkImageProvider previewProvider;
late CachedNetworkImageProvider thumbnailProvider;
@override
Widget build(BuildContext context) {
@@ -65,12 +67,13 @@ class _RemotePhotoViewState extends State<RemotePhotoView> {
}
CachedNetworkImageProvider _authorizedImageProvider(
String url, String cacheKey, BaseCacheManager? cacheManager) {
String url,
String cacheKey,
) {
return CachedNetworkImageProvider(
url,
headers: {"Authorization": widget.authToken},
cacheKey: cacheKey,
cacheManager: cacheManager,
);
}
@@ -104,10 +107,9 @@ class _RemotePhotoViewState extends State<RemotePhotoView> {
}
void _loadImages() {
CachedNetworkImageProvider thumbnailProvider = _authorizedImageProvider(
thumbnailProvider = _authorizedImageProvider(
widget.thumbnailUrl,
widget.cacheKey,
widget.thumbnailCacheManager,
);
_imageProvider = thumbnailProvider;
@@ -121,10 +123,9 @@ class _RemotePhotoViewState extends State<RemotePhotoView> {
);
if (widget.previewUrl != null) {
CachedNetworkImageProvider previewProvider = _authorizedImageProvider(
previewProvider = _authorizedImageProvider(
widget.previewUrl!,
"${widget.cacheKey}_previewStage",
widget.previewCacheManager,
);
previewProvider.resolve(const ImageConfiguration()).addListener(
ImageStreamListener((ImageInfo imageInfo, _) {
@@ -133,10 +134,9 @@ class _RemotePhotoViewState extends State<RemotePhotoView> {
);
}
CachedNetworkImageProvider fullProvider = _authorizedImageProvider(
fullProvider = _authorizedImageProvider(
widget.imageUrl,
"${widget.cacheKey}_fullStage",
widget.fullCacheManager,
);
fullProvider.resolve(const ImageConfiguration()).addListener(
ImageStreamListener((ImageInfo imageInfo, _) {
@@ -150,6 +150,19 @@ class _RemotePhotoViewState extends State<RemotePhotoView> {
_loadImages();
super.initState();
}
@override
void dispose() async {
super.dispose();
await thumbnailProvider.evict();
await fullProvider.evict();
if (widget.previewUrl != null) {
await previewProvider.evict();
}
_imageProvider.evict();
}
}
class RemotePhotoView extends StatefulWidget {
@@ -165,9 +178,6 @@ class RemotePhotoView extends StatefulWidget {
this.previewUrl,
required this.onLoadingCompleted,
required this.onLoadingStart,
this.thumbnailCacheManager,
this.previewCacheManager,
this.fullCacheManager,
required this.cacheKey,
}) : super(key: key);
@@ -177,9 +187,6 @@ class RemotePhotoView extends StatefulWidget {
final String? previewUrl;
final Function onLoadingCompleted;
final Function onLoadingStart;
final BaseCacheManager? thumbnailCacheManager;
final BaseCacheManager? previewCacheManager;
final BaseCacheManager? fullCacheManager;
final String cacheKey;
final void Function() onSwipeDown;

View File

@@ -8,7 +8,6 @@ import 'package:immich_mobile/modules/asset_viewer/ui/download_loading_indicator
import 'package:immich_mobile/modules/asset_viewer/ui/exif_bottom_sheet.dart';
import 'package:immich_mobile/modules/asset_viewer/ui/remote_photo_view.dart';
import 'package:immich_mobile/modules/home/services/asset.service.dart';
import 'package:immich_mobile/shared/services/cache.service.dart';
import 'package:immich_mobile/utils/image_url_builder.dart';
import 'package:openapi/api.dart';
@@ -41,7 +40,6 @@ class ImageViewerPage extends HookConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
final downloadAssetStatus =
ref.watch(imageViewerStateProvider).downloadAssetStatus;
final cacheService = ref.watch(cacheServiceProvider);
getAssetExif() async {
assetDetail =
@@ -87,12 +85,6 @@ class ImageViewerPage extends HookConsumerWidget {
onSwipeUp: () => showInfo(),
onLoadingCompleted: onLoadingCompleted,
onLoadingStart: onLoadingStart,
thumbnailCacheManager:
cacheService.getCache(CacheType.thumbnail),
previewCacheManager:
cacheService.getCache(CacheType.imageViewerPreview),
fullCacheManager:
cacheService.getCache(CacheType.imageViewerFull),
),
),
),

View File

@@ -79,7 +79,7 @@ class _VideoThumbnailPlayerState extends State<VideoThumbnailPlayer> {
_createChewieController() {
chewieController = ChewieController(
showOptions: true,
showControlsOnInitialize: true,
showControlsOnInitialize: false,
videoPlayerController: videoPlayerController,
autoPlay: true,
autoInitialize: true,

View File

@@ -1,5 +1,4 @@
import 'package:flutter/material.dart';
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/home/ui/thumbnail_image.dart';
import 'package:openapi/api.dart';
@@ -10,13 +9,11 @@ class ImageGrid extends ConsumerWidget {
final List<AssetResponseDto> sortedAssetGroup;
final int tilesPerRow;
final bool showStorageIndicator;
final BaseCacheManager? cacheManager;
ImageGrid({
Key? key,
required this.assetGroup,
required this.sortedAssetGroup,
this.cacheManager,
this.tilesPerRow = 4,
this.showStorageIndicator = true,
}) : super(key: key);
@@ -39,7 +36,6 @@ class ImageGrid extends ConsumerWidget {
child: Stack(
children: [
ThumbnailImage(
cacheManager: cacheManager,
asset: assetGroup[index],
assetList: sortedAssetGroup,
showStorageIndicator: showStorageIndicator,

View File

@@ -2,8 +2,6 @@ import 'package:auto_route/auto_route.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/hive_box.dart';
@@ -17,13 +15,11 @@ class ThumbnailImage extends HookConsumerWidget {
final AssetResponseDto asset;
final List<AssetResponseDto> assetList;
final bool showStorageIndicator;
final BaseCacheManager? cacheManager;
const ThumbnailImage({
Key? key,
required this.asset,
required this.assetList,
this.cacheManager,
this.showStorageIndicator = true,
}) : super(key: key);
@@ -96,7 +92,6 @@ class ThumbnailImage extends HookConsumerWidget {
),
child: CachedNetworkImage(
cacheKey: asset.id,
cacheManager: cacheManager,
width: 300,
height: 300,
memCacheHeight: asset.type == AssetTypeEnum.IMAGE ? 250 : 400,
@@ -114,7 +109,7 @@ class ThumbnailImage extends HookConsumerWidget {
),
),
errorWidget: (context, url, error) {
debugPrint("Error getting thumbnail $url = $error");
// debugPrint("Error getting thumbnail $url = $error");
return Icon(
Icons.image_not_supported_outlined,
color: Theme.of(context).primaryColor,

View File

@@ -16,7 +16,6 @@ import 'package:immich_mobile/modules/settings/services/app_settings.service.dar
import 'package:immich_mobile/shared/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/services/cache.service.dart';
import 'package:openapi/api.dart';
class HomePage extends HookConsumerWidget {
@@ -25,7 +24,6 @@ class HomePage extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final appSettingService = ref.watch(appSettingsServiceProvider);
final cacheService = ref.watch(cacheServiceProvider);
ScrollController scrollController = useScrollController();
var assetGroupByDateTime = ref.watch(assetGroupByDateTimeProvider);
@@ -91,7 +89,6 @@ class HomePage extends HookConsumerWidget {
imageGridGroup.add(
ImageGrid(
cacheManager: cacheService.getCache(CacheType.thumbnail),
assetGroup: immichAssetList,
sortedAssetGroup: sortedAssetList,
tilesPerRow:

View File

@@ -15,7 +15,6 @@ class CacheSettings extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final CacheService cacheService = ref.watch(cacheServiceProvider);
final clearCacheState = useState(false);
Future<void> clearCache() async {

View File

@@ -4,7 +4,6 @@ import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/settings/ui/asset_list_settings/asset_list_settings.dart';
import 'package:immich_mobile/modules/settings/ui/cache_settings/cache_settings.dart';
import 'package:immich_mobile/modules/settings/ui/image_viewer_quality_setting/image_viewer_quality_setting.dart';
import 'package:immich_mobile/modules/settings/ui/notification_setting/notification_setting.dart';
import 'package:immich_mobile/modules/settings/ui/theme_setting/theme_setting.dart';
@@ -42,7 +41,6 @@ class SettingsPage extends HookConsumerWidget {
const ImageViewerQualitySetting(),
const ThemeSetting(),
const AssetListSettings(),
const CacheSettings(),
if (Platform.isAndroid) const NotificationSetting(),
],
).toList(),

View File

@@ -7,7 +7,6 @@ import 'package:immich_mobile/utils/immich_cache_info_repository.dart';
enum CacheType {
// Shared cache for asset thumbnails in various modules
thumbnail,
imageViewerPreview,
imageViewerFull,
albumThumbnail,
@@ -67,7 +66,10 @@ class CacheService {
}
BaseCacheManager _getDefaultCache(
String cacheName, int size, CacheInfoRepository repo) {
String cacheName,
int size,
CacheInfoRepository repo,
) {
return CacheManager(
Config(
cacheName,

View File

@@ -115,4 +115,3 @@ lib/model/user_count_response_dto.dart
lib/model/user_response_dto.dart
lib/model/validate_access_token_response_dto.dart
pubspec.yaml
test/asset_count_by_user_id_response_dto_test.dart

View File

@@ -76,69 +76,72 @@ class AssetResponseDto {
SmartInfoResponseDto? smartInfo;
@override
bool operator ==(Object other) => identical(this, other) || other is AssetResponseDto &&
other.type == type &&
other.id == id &&
other.deviceAssetId == deviceAssetId &&
other.ownerId == ownerId &&
other.deviceId == deviceId &&
other.originalPath == originalPath &&
other.resizePath == resizePath &&
other.createdAt == createdAt &&
other.modifiedAt == modifiedAt &&
other.isFavorite == isFavorite &&
other.mimeType == mimeType &&
other.duration == duration &&
other.webpPath == webpPath &&
other.encodedVideoPath == encodedVideoPath &&
other.exifInfo == exifInfo &&
other.smartInfo == smartInfo;
bool operator ==(Object other) =>
identical(this, other) ||
other is AssetResponseDto &&
other.type == type &&
other.id == id &&
other.deviceAssetId == deviceAssetId &&
other.ownerId == ownerId &&
other.deviceId == deviceId &&
other.originalPath == originalPath &&
other.resizePath == resizePath &&
other.createdAt == createdAt &&
other.modifiedAt == modifiedAt &&
other.isFavorite == isFavorite &&
other.mimeType == mimeType &&
other.duration == duration &&
other.webpPath == webpPath &&
other.encodedVideoPath == encodedVideoPath &&
other.exifInfo == exifInfo &&
other.smartInfo == smartInfo;
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(type.hashCode) +
(id.hashCode) +
(deviceAssetId.hashCode) +
(ownerId.hashCode) +
(deviceId.hashCode) +
(originalPath.hashCode) +
(resizePath == null ? 0 : resizePath!.hashCode) +
(createdAt.hashCode) +
(modifiedAt.hashCode) +
(isFavorite.hashCode) +
(mimeType == null ? 0 : mimeType!.hashCode) +
(duration.hashCode) +
(webpPath == null ? 0 : webpPath!.hashCode) +
(encodedVideoPath == null ? 0 : encodedVideoPath!.hashCode) +
(exifInfo == null ? 0 : exifInfo!.hashCode) +
(smartInfo == null ? 0 : smartInfo!.hashCode);
// ignore: unnecessary_parenthesis
(type.hashCode) +
(id.hashCode) +
(deviceAssetId.hashCode) +
(ownerId.hashCode) +
(deviceId.hashCode) +
(originalPath.hashCode) +
(resizePath == null ? 0 : resizePath!.hashCode) +
(createdAt.hashCode) +
(modifiedAt.hashCode) +
(isFavorite.hashCode) +
(mimeType == null ? 0 : mimeType!.hashCode) +
(duration.hashCode) +
(webpPath == null ? 0 : webpPath!.hashCode) +
(encodedVideoPath == null ? 0 : encodedVideoPath!.hashCode) +
(exifInfo == null ? 0 : exifInfo!.hashCode) +
(smartInfo == null ? 0 : smartInfo!.hashCode);
@override
String toString() => 'AssetResponseDto[type=$type, id=$id, deviceAssetId=$deviceAssetId, ownerId=$ownerId, deviceId=$deviceId, originalPath=$originalPath, resizePath=$resizePath, createdAt=$createdAt, modifiedAt=$modifiedAt, isFavorite=$isFavorite, mimeType=$mimeType, duration=$duration, webpPath=$webpPath, encodedVideoPath=$encodedVideoPath, exifInfo=$exifInfo, smartInfo=$smartInfo]';
String toString() =>
'AssetResponseDto[type=$type, id=$id, deviceAssetId=$deviceAssetId, ownerId=$ownerId, deviceId=$deviceId, originalPath=$originalPath, resizePath=$resizePath, createdAt=$createdAt, modifiedAt=$modifiedAt, isFavorite=$isFavorite, mimeType=$mimeType, duration=$duration, webpPath=$webpPath, encodedVideoPath=$encodedVideoPath, exifInfo=$exifInfo, smartInfo=$smartInfo]';
Map<String, dynamic> toJson() {
final _json = <String, dynamic>{};
_json[r'type'] = type;
_json[r'id'] = id;
_json[r'deviceAssetId'] = deviceAssetId;
_json[r'ownerId'] = ownerId;
_json[r'deviceId'] = deviceId;
_json[r'originalPath'] = originalPath;
_json[r'type'] = type;
_json[r'id'] = id;
_json[r'deviceAssetId'] = deviceAssetId;
_json[r'ownerId'] = ownerId;
_json[r'deviceId'] = deviceId;
_json[r'originalPath'] = originalPath;
if (resizePath != null) {
_json[r'resizePath'] = resizePath;
} else {
_json[r'resizePath'] = null;
}
_json[r'createdAt'] = createdAt;
_json[r'modifiedAt'] = modifiedAt;
_json[r'isFavorite'] = isFavorite;
_json[r'createdAt'] = createdAt;
_json[r'modifiedAt'] = modifiedAt;
_json[r'isFavorite'] = isFavorite;
if (mimeType != null) {
_json[r'mimeType'] = mimeType;
} else {
_json[r'mimeType'] = null;
}
_json[r'duration'] = duration;
_json[r'duration'] = duration;
if (webpPath != null) {
_json[r'webpPath'] = webpPath;
} else {
@@ -172,13 +175,15 @@ class AssetResponseDto {
// Ensure that the map contains the required keys.
// Note 1: the values aren't checked for validity beyond being non-null.
// Note 2: this code is stripped in release mode!
assert(() {
requiredKeys.forEach((key) {
assert(json.containsKey(key), 'Required key "AssetResponseDto[$key]" is missing from JSON.');
assert(json[key] != null, 'Required key "AssetResponseDto[$key]" has a null value in JSON.');
});
return true;
}());
// assert(() {
// requiredKeys.forEach((key) {
// assert(json.containsKey(key),
// 'Required key "AssetResponseDto[$key]" is missing from JSON.');
// assert(json[key] != null,
// 'Required key "AssetResponseDto[$key]" has a null value in JSON.');
// });
// return true;
// }());
return AssetResponseDto(
type: AssetTypeEnum.fromJson(json[r'type'])!,
@@ -202,7 +207,10 @@ class AssetResponseDto {
return null;
}
static List<AssetResponseDto>? listFromJson(dynamic json, {bool growable = false,}) {
static List<AssetResponseDto>? listFromJson(
dynamic json, {
bool growable = false,
}) {
final result = <AssetResponseDto>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
@@ -230,12 +238,18 @@ class AssetResponseDto {
}
// maps a json object with a list of AssetResponseDto-objects as value to a dart map
static Map<String, List<AssetResponseDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
static Map<String, List<AssetResponseDto>> mapListFromJson(
dynamic json, {
bool growable = false,
}) {
final map = <String, List<AssetResponseDto>>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = AssetResponseDto.listFromJson(entry.value, growable: growable,);
final value = AssetResponseDto.listFromJson(
entry.value,
growable: growable,
);
if (value != null) {
map[entry.key] = value;
}
@@ -262,4 +276,3 @@ class AssetResponseDto {
'encodedVideoPath',
};
}

View File

@@ -126,21 +126,21 @@ packages:
name: cached_network_image
url: "https://pub.dartlang.org"
source: hosted
version: "3.2.1"
version: "3.2.2"
cached_network_image_platform_interface:
dependency: transitive
description:
name: cached_network_image_platform_interface
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.0"
version: "2.0.0"
cached_network_image_web:
dependency: transitive
description:
name: cached_network_image_web
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.1"
version: "1.0.2"
cancellation_token:
dependency: transitive
description:
@@ -182,7 +182,7 @@ packages:
name: chewie
url: "https://pub.dartlang.org"
source: hosted
version: "1.3.2"
version: "1.3.5"
clock:
dependency: transitive
description:
@@ -238,7 +238,7 @@ packages:
name: cupertino_icons
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.4"
version: "1.0.5"
dart_style:
dependency: transitive
description:
@@ -839,7 +839,7 @@ packages:
name: provider
url: "https://pub.dartlang.org"
source: hosted
version: "6.0.2"
version: "6.0.3"
pub_semver:
dependency: transitive
description:
@@ -1223,27 +1223,20 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.2"
very_good_analysis:
dependency: transitive
description:
name: very_good_analysis
url: "https://pub.dartlang.org"
source: hosted
version: "2.4.0"
video_player:
dependency: "direct main"
description:
name: video_player
url: "https://pub.dartlang.org"
source: hosted
version: "2.4.2"
version: "2.4.7"
video_player_android:
dependency: transitive
description:
name: video_player_android
url: "https://pub.dartlang.org"
source: hosted
version: "2.3.3"
version: "2.3.9"
video_player_avfoundation:
dependency: transitive
description:
@@ -1271,7 +1264,7 @@ packages:
name: wakelock
url: "https://pub.dartlang.org"
source: hosted
version: "0.6.1+2"
version: "0.6.2"
wakelock_macos:
dependency: transitive
description:
@@ -1350,5 +1343,5 @@ packages:
source: hosted
version: "3.1.1"
sdks:
dart: ">=2.17.0 <3.0.0"
flutter: ">=3.0.0"
dart: ">=2.18.0 <3.0.0"
flutter: ">=3.3.0"

View File

@@ -2,7 +2,7 @@ name: immich_mobile
description: Immich - selfhosted backup media file on mobile phone
publish_to: "none"
version: 1.28.2+40
version: 1.29.0+42
environment:
sdk: ">=2.17.0 <3.0.0"
@@ -17,7 +17,7 @@ dependencies:
hive: ^2.2.1
hive_flutter: ^1.1.0
dio: ^4.0.4
cached_network_image: ^3.2.1
cached_network_image: ^3.2.2
percent_indicator: ^4.2.2
intl: ^0.17.0
auto_route: ^4.0.1
@@ -26,7 +26,7 @@ dependencies:
flutter_launcher_icons: "^0.9.2"
fluttertoast: ^8.0.8
video_player: ^2.2.18
chewie: ^1.2.2
chewie: ^1.3.5
badges: ^2.0.2
photo_view: ^0.14.0
socket_io_client: ^2.0.0-beta.4-nullsafety.0

View File

@@ -1,6 +1,5 @@
FROM nginx:latest
FROM docker.io/nginxinc/nginx-unprivileged:latest
COPY nginx.conf /etc/nginx/conf.d/default.conf
COPY nginx.conf "/etc/nginx/nginx.conf"
EXPOSE 80
EXPOSE 443
CMD nginx -g "daemon off;"

View File

@@ -1,73 +1,93 @@
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
worker_processes auto;
error_log /var/log/nginx/error.log;
pid /tmp/nginx.pid;
# Load dynamic modules. See /usr/share/doc/nginx/README.dynamic.
include /usr/share/nginx/modules/*.conf;
events {
worker_connections 1024;
}
# events {
# worker_connections 1000;
# }
server {
gzip on;
gzip_min_length 1000;
gunzip on;
client_max_body_size 50000M;
listen 80;
access_log off;
location /api {
# Compression
gzip_static on;
gzip_min_length 1000;
gzip_comp_level 2;
proxy_buffering off;
proxy_buffer_size 16k;
proxy_busy_buffers_size 24k;
proxy_buffers 64 4k;
proxy_force_ranges on;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
rewrite /api/(.*) /$1 break;
proxy_pass http://immich-server:3001;
http {
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
location / {
client_body_temp_path /tmp/client_temp;
proxy_temp_path /tmp/proxy_temp_path;
fastcgi_temp_path /tmp/fastcgi_temp;
uwsgi_temp_path /tmp/uwsgi_temp;
scgi_temp_path /tmp/scgi_temp;
# Compression
gzip_static on;
# events {
# worker_connections 1000;
# }
server {
gzip on;
gzip_min_length 1000;
gzip_comp_level 2;
gunzip on;
proxy_buffering off;
proxy_buffer_size 16k;
proxy_busy_buffers_size 24k;
proxy_buffers 64 4k;
proxy_force_ranges on;
client_max_body_size 50000M;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
listen 8080;
access_log off;
proxy_pass http://immich-web:3000;
location /api {
# Compression
gzip_static on;
gzip_min_length 1000;
gzip_comp_level 2;
proxy_buffering off;
proxy_buffer_size 16k;
proxy_busy_buffers_size 24k;
proxy_buffers 64 4k;
proxy_force_ranges on;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
rewrite /api/(.*) /$1 break;
proxy_pass http://immich-server:3001;
}
location / {
# Compression
gzip_static on;
gzip_min_length 1000;
gzip_comp_level 2;
proxy_buffering off;
proxy_buffer_size 16k;
proxy_busy_buffers_size 24k;
proxy_buffers 64 4k;
proxy_force_ranges on;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_pass http://immich-web:3000;
}
}
}
}

View File

@@ -50,9 +50,14 @@ export class AlbumRepository implements IAlbumRepository {
where: { sharedUserId: userId },
});
const sharingAlbums = ownedAlbums.map((album) => album.sharedUsers?.length || 0).reduce((a, b) => a + b, 0);
let sharedAlbumCount = 0;
ownedAlbums.map((album) => {
if (album.sharedUsers?.length) {
sharedAlbumCount += 1;
}
});
return new AlbumCountResponseDto(ownedAlbums.length, sharedAlbums, sharingAlbums);
return new AlbumCountResponseDto(ownedAlbums.length, sharedAlbums, sharedAlbumCount);
}
async create(ownerId: string, createAlbumDto: CreateAlbumDto): Promise<AlbumEntity> {

View File

@@ -8,6 +8,7 @@ import { AlbumEntity } from '../../../../../libs/database/src/entities/album.ent
import { AssetAlbumEntity } from '@app/database/entities/asset-album.entity';
import { UserAlbumEntity } from '@app/database/entities/user-album.entity';
import { AlbumRepository, ALBUM_REPOSITORY } from './album-repository';
import { AssetRepository, ASSET_REPOSITORY } from '../asset/asset-repository';
@Module({
imports: [TypeOrmModule.forFeature([AssetEntity, UserEntity, AlbumEntity, AssetAlbumEntity, UserAlbumEntity])],
@@ -18,6 +19,10 @@ import { AlbumRepository, ALBUM_REPOSITORY } from './album-repository';
provide: ALBUM_REPOSITORY,
useClass: AlbumRepository,
},
{
provide: ASSET_REPOSITORY,
useClass: AssetRepository,
},
],
})
export class AlbumModule {}

View File

@@ -4,10 +4,13 @@ import { AuthUserDto } from '../../decorators/auth-user.decorator';
import { BadRequestException, NotFoundException, ForbiddenException } from '@nestjs/common';
import { AlbumEntity } from '@app/database/entities/album.entity';
import { AlbumResponseDto } from './response-dto/album-response.dto';
import { IAssetRepository } from '../asset/asset-repository';
describe('Album service', () => {
let sut: AlbumService;
let albumRepositoryMock: jest.Mocked<IAlbumRepository>;
let assetRepositoryMock: jest.Mocked<IAssetRepository>;
const authUser: AuthUserDto = Object.freeze({
id: '1111',
email: 'auth@test.com',
@@ -118,7 +121,22 @@ describe('Album service', () => {
getListByAssetId: jest.fn(),
getCountByUserId: jest.fn(),
};
sut = new AlbumService(albumRepositoryMock);
assetRepositoryMock = {
create: jest.fn(),
getAllByUserId: jest.fn(),
getAllByDeviceId: jest.fn(),
getAssetCountByTimeBucket: jest.fn(),
getById: jest.fn(),
getDetectedObjectsByUserId: jest.fn(),
getLocationsByUserId: jest.fn(),
getSearchPropertiesByUserId: jest.fn(),
getAssetByTimeBucket: jest.fn(),
getAssetByChecksum: jest.fn(),
getAssetCountByUserId: jest.fn(),
};
sut = new AlbumService(albumRepositoryMock, assetRepositoryMock);
});
it('creates album', async () => {

View File

@@ -10,10 +10,14 @@ import { GetAlbumsDto } from './dto/get-albums.dto';
import { AlbumResponseDto, mapAlbum, mapAlbumExcludeAssetInfo } from './response-dto/album-response.dto';
import { ALBUM_REPOSITORY, IAlbumRepository } from './album-repository';
import { AlbumCountResponseDto } from './response-dto/album-count-response.dto';
import { ASSET_REPOSITORY, IAssetRepository } from '../asset/asset-repository';
@Injectable()
export class AlbumService {
constructor(@Inject(ALBUM_REPOSITORY) private _albumRepository: IAlbumRepository) {}
constructor(
@Inject(ALBUM_REPOSITORY) private _albumRepository: IAlbumRepository,
@Inject(ASSET_REPOSITORY) private _assetRepository: IAssetRepository,
) {}
private async _getAlbum({
authUser,
@@ -54,6 +58,11 @@ export class AlbumService {
return albums.map(mapAlbumExcludeAssetInfo);
}
const albums = await this._albumRepository.getList(authUser.id, getAlbumsDto);
for (const album of albums) {
await this._checkValidThumbnail(album);
}
return albums.map((album) => mapAlbumExcludeAssetInfo(album));
}
@@ -123,4 +132,18 @@ export class AlbumService {
async getAlbumCountByUserId(authUser: AuthUserDto): Promise<AlbumCountResponseDto> {
return this._albumRepository.getCountByUserId(authUser.id);
}
async _checkValidThumbnail(album: AlbumEntity): Promise<AlbumEntity> {
const assetId = album.albumThumbnailAssetId;
if (assetId) {
try {
await this._assetRepository.getById(assetId);
} catch (e) {
album.albumThumbnailAssetId = null;
return await this._albumRepository.updateAlbum(album, {});
}
}
return album;
}
}

View File

@@ -36,6 +36,7 @@ import {
import { GetAssetCountByTimeBucketDto } from './dto/get-asset-count-by-time-bucket.dto';
import { GetAssetByTimeBucketDto } from './dto/get-asset-by-time-bucket.dto';
import { AssetCountByUserIdResponseDto } from './response-dto/asset-count-by-user-id-response.dto';
import { timeUtils } from '@app/common/utils';
const fileInfo = promisify(stat);
@@ -56,6 +57,18 @@ export class AssetService {
mimeType: string,
checksum: Buffer,
): Promise<AssetEntity> {
// Check valid time.
const createdAt = createAssetDto.createdAt;
const modifiedAt = createAssetDto.modifiedAt;
if (!timeUtils.checkValidTimestamp(createdAt)) {
createAssetDto.createdAt = await timeUtils.getTimestampFromExif(originalPath);
}
if (!timeUtils.checkValidTimestamp(modifiedAt)) {
createAssetDto.modifiedAt = await timeUtils.getTimestampFromExif(originalPath);
}
const assetEntity = await this._assetRepository.create(
createAssetDto,
authUser.id,

View File

@@ -10,7 +10,7 @@ export interface IServerVersion {
export const serverVersion: IServerVersion = {
major: 1,
minor: 28,
patch: 2,
build: 40,
minor: 29,
patch: 0,
build: 42,
};

View File

@@ -13,7 +13,7 @@ import {
} from '@app/job/constants/queue-name.constant';
import { BullModule } from '@nestjs/bull';
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { TypeOrmModule } from '@nestjs/typeorm';
import { CommunicationModule } from '../../immich/src/api-v1/communication/communication.module';
import { MicroservicesService } from './microservices.service';
@@ -40,42 +40,48 @@ import { VideoTranscodeProcessor } from './processors/video-transcode.processor'
},
}),
}),
BullModule.registerQueue({
name: thumbnailGeneratorQueueName,
defaultJobOptions: {
attempts: 3,
removeOnComplete: true,
removeOnFail: false,
BullModule.registerQueue(
{
name: thumbnailGeneratorQueueName,
defaultJobOptions: {
attempts: 3,
removeOnComplete: true,
removeOnFail: false,
},
},
}, {
name: assetUploadedQueueName,
defaultJobOptions: {
attempts: 3,
removeOnComplete: true,
removeOnFail: false,
{
name: assetUploadedQueueName,
defaultJobOptions: {
attempts: 3,
removeOnComplete: true,
removeOnFail: false,
},
},
}, {
name: metadataExtractionQueueName,
defaultJobOptions: {
attempts: 3,
removeOnComplete: true,
removeOnFail: false,
{
name: metadataExtractionQueueName,
defaultJobOptions: {
attempts: 3,
removeOnComplete: true,
removeOnFail: false,
},
},
}, {
name: videoConversionQueueName,
defaultJobOptions: {
attempts: 3,
removeOnComplete: true,
removeOnFail: false,
{
name: videoConversionQueueName,
defaultJobOptions: {
attempts: 3,
removeOnComplete: true,
removeOnFail: false,
},
},
}, {
name: generateChecksumQueueName,
defaultJobOptions: {
attempts: 3,
removeOnComplete: true,
removeOnFail: false,
{
name: generateChecksumQueueName,
defaultJobOptions: {
attempts: 3,
removeOnComplete: true,
removeOnFail: false,
},
},
}),
),
CommunicationModule,
],
controllers: [],
@@ -86,6 +92,7 @@ import { VideoTranscodeProcessor } from './processors/video-transcode.processor'
MetadataExtractionProcessor,
VideoTranscodeProcessor,
GenerateChecksumProcessor,
ConfigService,
],
exports: [],
})

View File

@@ -1,3 +1,4 @@
import { ImmichLogLevel } from '@app/common/constants/log-level.constant';
import { AssetEntity } from '@app/database/entities/asset.entity';
import { ExifEntity } from '@app/database/entities/exif.entity';
import { SmartInfoEntity } from '@app/database/entities/smart-info.entity';
@@ -16,6 +17,7 @@ import { MapiResponse } from '@mapbox/mapbox-sdk/lib/classes/mapi-response';
import mapboxGeocoding, { GeocodeService } from '@mapbox/mapbox-sdk/services/geocoding';
import { Process, Processor } from '@nestjs/bull';
import { Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { InjectRepository } from '@nestjs/typeorm';
import axios from 'axios';
import { Job } from 'bull';
@@ -28,6 +30,7 @@ import { Repository } from 'typeorm/repository/Repository';
@Processor(metadataExtractionQueueName)
export class MetadataExtractionProcessor {
private geocodingClient?: GeocodeService;
private logLevel: ImmichLogLevel;
constructor(
@InjectRepository(AssetEntity)
@@ -38,12 +41,16 @@ export class MetadataExtractionProcessor {
@InjectRepository(SmartInfoEntity)
private smartInfoRepository: Repository<SmartInfoEntity>,
private configService: ConfigService,
) {
if (process.env.ENABLE_MAPBOX == 'true' && process.env.MAPBOX_KEY) {
this.geocodingClient = mapboxGeocoding({
accessToken: process.env.MAPBOX_KEY,
});
}
this.logLevel = this.configService.get('LOG_LEVEL') || ImmichLogLevel.SIMPLE;
}
@Process(exifExtractionProcessorName)
@@ -77,7 +84,7 @@ export class MetadataExtractionProcessor {
newExif.exifImageWidth = exifData['ExifImageWidth'] || exifData['ImageWidth'] || null;
newExif.fileSizeInByte = fileSize || null;
newExif.orientation = exifData['Orientation'] || null;
newExif.dateTimeOriginal = exifData['DateTimeOriginal'] || null;
newExif.dateTimeOriginal = new Date(asset.createdAt) || null;
newExif.modifyDate = exifData['ModifyDate'] || null;
newExif.lensModel = exifData['LensModel'] || null;
newExif.fNumber = exifData['FNumber'] || null;
@@ -139,6 +146,10 @@ export class MetadataExtractionProcessor {
await this.exifRepository.save(newExif);
} catch (e) {
Logger.error(`Error extracting EXIF ${String(e)}`, 'extractExif');
if (this.logLevel === ImmichLogLevel.VERBOSE) {
console.trace('Error extracting EXIF', e);
}
}
}

View File

@@ -1,3 +1,4 @@
import { ImmichLogLevel } from '@app/common/constants/log-level.constant';
import { AssetEntity, AssetType } from '@app/database/entities/asset.entity';
import {
WebpGeneratorProcessor,
@@ -11,6 +12,7 @@ import {
} from '@app/job';
import { InjectQueue, Process, Processor } from '@nestjs/bull';
import { Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { InjectRepository } from '@nestjs/typeorm';
import { mapAsset } from 'apps/immich/src/api-v1/asset/response-dto/asset-response.dto';
import { Job, Queue } from 'bull';
@@ -23,6 +25,8 @@ import { CommunicationGateway } from '../../../immich/src/api-v1/communication/c
@Processor(thumbnailGeneratorQueueName)
export class ThumbnailGeneratorProcessor {
private logLevel: ImmichLogLevel;
constructor(
@InjectRepository(AssetEntity)
private assetRepository: Repository<AssetEntity>,
@@ -34,7 +38,11 @@ export class ThumbnailGeneratorProcessor {
@InjectQueue(metadataExtractionQueueName)
private metadataExtractionQueue: Queue,
) {}
private configService: ConfigService,
) {
this.logLevel = this.configService.get('LOG_LEVEL') || ImmichLogLevel.SIMPLE;
}
@Process({ name: generateJPEGThumbnailProcessorName, concurrency: 3 })
async generateJPEGThumbnail(job: Job<JpegGeneratorProcessor>) {
@@ -51,8 +59,16 @@ export class ThumbnailGeneratorProcessor {
const jpegThumbnailPath = resizePath + originalFilename + '.jpeg';
if (asset.type == AssetType.IMAGE) {
await sharp(asset.originalPath).resize(1440, 2560, { fit: 'inside' }).jpeg().rotate().toFile(jpegThumbnailPath);
await this.assetRepository.update({ id: asset.id }, { resizePath: jpegThumbnailPath });
try {
await sharp(asset.originalPath).resize(1440, 2560, { fit: 'inside' }).jpeg().rotate().toFile(jpegThumbnailPath);
await this.assetRepository.update({ id: asset.id }, { resizePath: jpegThumbnailPath });
} catch (error) {
Logger.error('Failed to generate jpeg thumbnail for asset: ' + asset.id);
if (this.logLevel == ImmichLogLevel.VERBOSE) {
console.trace('Failed to generate jpeg thumbnail for asset', error);
}
}
// Update resize path to send to generate webp queue
asset.resizePath = jpegThumbnailPath;
@@ -105,7 +121,15 @@ export class ThumbnailGeneratorProcessor {
const webpPath = asset.resizePath.replace('jpeg', 'webp');
await sharp(asset.resizePath).resize(250).webp().rotate().toFile(webpPath);
await this.assetRepository.update({ id: asset.id }, { webpPath: webpPath });
try {
await sharp(asset.resizePath).resize(250).webp().rotate().toFile(webpPath);
await this.assetRepository.update({ id: asset.id }, { webpPath: webpPath });
} catch (error) {
Logger.error('Failed to generate webp thumbnail for asset: ' + asset.id);
if (this.logLevel == ImmichLogLevel.VERBOSE) {
console.trace('Failed to generate webp thumbnail for asset', error);
}
}
}
}

View File

@@ -16,5 +16,6 @@ export const immichAppConfig: ConfigModuleOptions = {
then: Joi.string().optional().allow(null, ''),
otherwise: Joi.string().required(),
}),
LOG_LEVEL: Joi.string().optional().valid('simple', 'verbose').default('simple'),
}),
};

View File

@@ -0,0 +1,4 @@
export enum ImmichLogLevel {
SIMPLE = 'simple',
VERBOSE = 'verbose',
}

View File

@@ -1,2 +1,3 @@
export * from './config';
export * from './constants';
export * from './utils';

View File

@@ -0,0 +1 @@
export * from './time-utils';

View File

@@ -0,0 +1,37 @@
// create unit test for time utils
import { timeUtils } from './time-utils';
describe('Time Utilities', () => {
describe('checkValidTimestamp', () => {
it('check for year 0000', () => {
const result = timeUtils.checkValidTimestamp('0000-00-00T00:00:00.000Z');
expect(result).toBeFalsy();
});
it('check for 6-digits year with plus sign', () => {
const result = timeUtils.checkValidTimestamp('+12345-00-00T00:00:00.000Z');
expect(result).toBeFalsy();
});
it('check for 6-digits year with negative sign', () => {
const result = timeUtils.checkValidTimestamp('-12345-00-00T00:00:00.000Z');
expect(result).toBeFalsy();
});
it('check for current date', () => {
const result = timeUtils.checkValidTimestamp(new Date().toISOString());
expect(result).toBeTruthy();
});
it('check for year before 1583', () => {
const result = timeUtils.checkValidTimestamp('1582-12-31T23:59:59.999Z');
expect(result).toBeFalsy();
});
it('check for year after 9999', () => {
const result = timeUtils.checkValidTimestamp('10000-00-00T00:00:00.000Z');
expect(result).toBeFalsy();
});
});
});

View File

@@ -0,0 +1,48 @@
import exifr from 'exifr';
function createTimeUtils() {
const checkValidTimestamp = (timestamp: string): boolean => {
const parsedTimestamp = Date.parse(timestamp);
if (isNaN(parsedTimestamp)) {
return false;
}
const date = new Date(parsedTimestamp);
if (date.getFullYear() < 1583 || date.getFullYear() > 9999) {
return false;
}
return date.getFullYear() > 0;
};
const getTimestampFromExif = async (originalPath: string): Promise<string> => {
try {
const exifData = await exifr.parse(originalPath, {
tiff: true,
ifd0: true as any,
ifd1: true,
exif: true,
gps: true,
interop: true,
xmp: true,
icc: true,
iptc: true,
jfif: true,
ihdr: true,
});
if (exifData && exifData['DateTimeOriginal']) {
return exifData['DateTimeOriginal'];
} else {
return new Date().toISOString();
}
} catch (error) {
return new Date().toISOString();
}
};
return { checkValidTimestamp, getTimestampFromExif };
}
export const timeUtils = createTimeUtils();

View File

@@ -129,6 +129,7 @@
"^@app/database(|/.*)$": "<rootDir>/libs/database/src/$1",
"@app/database/config/(.*)": "<rootDir>/libs/database/src/config/$1",
"@app/database/config": "<rootDir>/libs/database/src/config",
"@app/common": "<rootDir>/libs/common/src",
"^@app/job(|/.*)$": "<rootDir>/libs/job/src/$1"
}
}

View File

@@ -7,6 +7,7 @@
import moment from 'moment';
import { createEventDispatcher, onMount } from 'svelte';
import { browser } from '$app/env';
import { env } from '$env/dynamic/public';
import { AssetResponseDto, AlbumResponseDto } from '@api';
type Leaflet = typeof import('leaflet');
@@ -30,6 +31,13 @@
if (asset.exifInfo?.latitude != null && asset.exifInfo?.longitude != null) {
await drawMap(asset.exifInfo.latitude, asset.exifInfo.longitude);
}
// remove timezone when user not config PUBLIC_TZ var. Etc/UTC is used in default.
if (asset.exifInfo?.dateTimeOriginal && !env.PUBLIC_TZ) {
const dateTimeOriginal = asset.exifInfo.dateTimeOriginal;
asset.exifInfo.dateTimeOriginal = dateTimeOriginal.slice(0, dateTimeOriginal.length - 1);
}
}
});
@@ -126,11 +134,7 @@
<p>{moment(asset.exifInfo.dateTimeOriginal).format('MMM DD, YYYY')}</p>
<div class="flex gap-2 text-sm">
<p>
{moment(
asset.exifInfo.dateTimeOriginal
.toString()
.slice(0, asset.exifInfo.dateTimeOriginal.toString().length - 1)
).format('ddd, hh:mm A')}
{moment(asset.exifInfo.dateTimeOriginal).format('ddd, hh:mm A')}
</p>
<p>GMT{moment(asset.exifInfo.dateTimeOriginal).format('Z')}</p>
</div>