Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3d251f51fc | ||
|
|
57704522cd | ||
|
|
787926c111 | ||
|
|
08b424b3df | ||
|
|
736a946101 | ||
|
|
6f6f847ee2 | ||
|
|
13be271df7 | ||
|
|
b423852fad | ||
|
|
fe3d6b870a | ||
|
|
14be63039f | ||
|
|
d339d4c8dd |
@@ -80,3 +80,7 @@ docker-compose down -v
|
||||
```
|
||||
|
||||
After removing the the containers and volumes, the **Files** can be cleaned up (if necessary) from the `UPLOAD_LOCATION` by simply deleting an unwanted files or folders.
|
||||
|
||||
### Why iOS app shows duplicate photos on the timeline while the web doesn't?
|
||||
|
||||
If you are using `My Photo Stream`, the Photos app temporarily creates duplicates of photos taken in the last 30 days. These photos are included in the `Recents` album and thus shown up twice. To fix this, you can disable `My Photo Stream` in the native Photos app or choose a different album in the backup screen in Immich.
|
||||
2
machine-learning/.gitignore
vendored
2
machine-learning/.gitignore
vendored
@@ -1,3 +1,5 @@
|
||||
*.zip
|
||||
*.onnx
|
||||
upload/
|
||||
venv/
|
||||
__pycache__/
|
||||
|
||||
@@ -23,6 +23,7 @@ if (flutterVersionName == null) {
|
||||
|
||||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'kotlin-kapt'
|
||||
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
|
||||
|
||||
def keystoreProperties = new Properties()
|
||||
@@ -86,4 +87,6 @@ dependencies {
|
||||
implementation "androidx.work:work-runtime-ktx:$work_version"
|
||||
implementation "androidx.concurrent:concurrent-futures:$concurrent_version"
|
||||
implementation "com.google.guava:guava:$guava_version"
|
||||
implementation "com.github.bumptech.glide:glide:$glide_version"
|
||||
kapt "com.github.bumptech.glide:compiler:$glide_version"
|
||||
}
|
||||
|
||||
@@ -59,6 +59,7 @@
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
|
||||
<queries>
|
||||
<intent>
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
package app.alextran.immich
|
||||
|
||||
import com.bumptech.glide.annotation.GlideModule
|
||||
import com.bumptech.glide.module.AppGlideModule
|
||||
|
||||
@GlideModule
|
||||
class AppGlideModule : AppGlideModule()
|
||||
@@ -1,15 +1,16 @@
|
||||
buildscript {
|
||||
ext.kotlin_version = '1.6.10'
|
||||
ext.kotlin_version = '1.8.20'
|
||||
ext.work_version = '2.7.1'
|
||||
ext.concurrent_version = '1.1.0'
|
||||
ext.guava_version = '31.0.1-android'
|
||||
ext.glide_version = '4.14.2'
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:7.1.2'
|
||||
classpath 'com.android.tools.build:gradle:7.4.2'
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ platform :android do
|
||||
build_type: 'Release',
|
||||
properties: {
|
||||
"android.injected.version.code" => 77,
|
||||
"android.injected.version.name" => "1.54.0",
|
||||
"android.injected.version.name" => "1.54.1",
|
||||
}
|
||||
)
|
||||
upload_to_play_store(skip_upload_apk: true, skip_upload_images: true, skip_upload_screenshots: true, aab: '../build/app/outputs/bundle/release/app-release.aab')
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
* Fixes white navigation bar in Android 8.
|
||||
* Add bottom safe area to video player controls.
|
||||
* Fix null check operator on null value.
|
||||
* Add troubleshooting toggle.
|
||||
* Configurable log level.
|
||||
* Consistent handling of DateTime in SyncService.
|
||||
* Fix asset removal edge cases.
|
||||
* Archive feature on mobile.
|
||||
* Add AssetState and proper asset updating.
|
||||
* Video player disposes early.
|
||||
@@ -5,17 +5,17 @@
|
||||
|
||||
|
||||
|
||||
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000285">
|
||||
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000294">
|
||||
|
||||
</testcase>
|
||||
|
||||
|
||||
<testcase classname="fastlane.lanes" name="1: bundleRelease" time="168.230955">
|
||||
<testcase classname="fastlane.lanes" name="1: bundleRelease" time="75.683384">
|
||||
|
||||
</testcase>
|
||||
|
||||
|
||||
<testcase classname="fastlane.lanes" name="2: upload_to_play_store" time="43.975952">
|
||||
<testcase classname="fastlane.lanes" name="2: upload_to_play_store" time="24.839722">
|
||||
|
||||
</testcase>
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
#Fri Jun 23 08:50:38 CEST 2017
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-all.zip
|
||||
distributionSha256Sum=cd5c2958a107ee7f0722004a12d0f8559b4564c34daad7df06cffd4d12a426d0
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-all.zip
|
||||
distributionSha256Sum=518a863631feb7452b8f1b3dc2aaee5f388355cc3421bbd0275fbeadd77e84b2
|
||||
@@ -378,7 +378,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/RunnerProfile.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 88;
|
||||
CURRENT_PROJECT_VERSION = 94;
|
||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
@@ -514,7 +514,7 @@
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 88;
|
||||
CURRENT_PROJECT_VERSION = 94;
|
||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
@@ -542,7 +542,7 @@
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 88;
|
||||
CURRENT_PROJECT_VERSION = 94;
|
||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
|
||||
@@ -45,11 +45,11 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.50.0</string>
|
||||
<string>1.54.0</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>88</string>
|
||||
<string>94</string>
|
||||
<key>FLTEnableImpeller</key>
|
||||
<true />
|
||||
<key>ITSAppUsesNonExemptEncryption</key>
|
||||
|
||||
@@ -19,7 +19,7 @@ platform :ios do
|
||||
desc "iOS Beta"
|
||||
lane :beta do
|
||||
increment_version_number(
|
||||
version_number: "1.54.0"
|
||||
version_number: "1.54.1"
|
||||
)
|
||||
increment_build_number(
|
||||
build_number: latest_testflight_build_number + 1,
|
||||
|
||||
@@ -5,34 +5,34 @@
|
||||
|
||||
|
||||
|
||||
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000301">
|
||||
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000307">
|
||||
|
||||
</testcase>
|
||||
|
||||
|
||||
<testcase classname="fastlane.lanes" name="1: increment_version_number" time="1.613972">
|
||||
<testcase classname="fastlane.lanes" name="1: increment_version_number" time="3.674618">
|
||||
|
||||
</testcase>
|
||||
|
||||
|
||||
<testcase classname="fastlane.lanes" name="2: latest_testflight_build_number" time="4.887872">
|
||||
<testcase classname="fastlane.lanes" name="2: latest_testflight_build_number" time="94.327489">
|
||||
|
||||
</testcase>
|
||||
|
||||
|
||||
<testcase classname="fastlane.lanes" name="3: increment_build_number" time="1.53884">
|
||||
<testcase classname="fastlane.lanes" name="3: increment_build_number" time="2.317998">
|
||||
|
||||
</testcase>
|
||||
|
||||
|
||||
<testcase classname="fastlane.lanes" name="4: build_app" time="64.096001">
|
||||
<testcase classname="fastlane.lanes" name="4: build_app" time="86.596447">
|
||||
|
||||
</testcase>
|
||||
|
||||
|
||||
<testcase classname="fastlane.lanes" name="5: upload_to_testflight" time="6.017821">
|
||||
<testcase classname="fastlane.lanes" name="5: upload_to_testflight" time="8.496988">
|
||||
|
||||
<failure message="/usr/local/Cellar/fastlane/2.212.1/libexec/gems/fastlane-2.212.1/fastlane/lib/fastlane/actions/actions_helper.rb:67:in `execute_action' /usr/local/Cellar/fastlane/2.212.1/libexec/gems/fastlane-2.212.1/fastlane/lib/fastlane/runner.rb:255:in `block in execute_action' /usr/local/Cellar/fastlane/2.212.1/libexec/gems/fastlane-2.212.1/fastlane/lib/fastlane/runner.rb:229:in `chdir' /usr/local/Cellar/fastlane/2.212.1/libexec/gems/fastlane-2.212.1/fastlane/lib/fastlane/runner.rb:229:in `execute_action' /usr/local/Cellar/fastlane/2.212.1/libexec/gems/fastlane-2.212.1/fastlane/lib/fastlane/runner.rb:157:in `trigger_action_by_name' /usr/local/Cellar/fastlane/2.212.1/libexec/gems/fastlane-2.212.1/fastlane/lib/fastlane/fast_file.rb:159:in `method_missing' Fastfile:30:in `block (2 levels) in parsing_binding' /usr/local/Cellar/fastlane/2.212.1/libexec/gems/fastlane-2.212.1/fastlane/lib/fastlane/lane.rb:33:in `call' /usr/local/Cellar/fastlane/2.212.1/libexec/gems/fastlane-2.212.1/fastlane/lib/fastlane/runner.rb:49:in `block in execute' /usr/local/Cellar/fastlane/2.212.1/libexec/gems/fastlane-2.212.1/fastlane/lib/fastlane/runner.rb:45:in `chdir' /usr/local/Cellar/fastlane/2.212.1/libexec/gems/fastlane-2.212.1/fastlane/lib/fastlane/runner.rb:45:in `execute' /usr/local/Cellar/fastlane/2.212.1/libexec/gems/fastlane-2.212.1/fastlane/lib/fastlane/lane_manager.rb:47:in `cruise_lane' /usr/local/Cellar/fastlane/2.212.1/libexec/gems/fastlane-2.212.1/fastlane/lib/fastlane/command_line_handler.rb:36:in `handle' /usr/local/Cellar/fastlane/2.212.1/libexec/gems/fastlane-2.212.1/fastlane/lib/fastlane/commands_generator.rb:110:in `block (2 levels) in run' /usr/local/Cellar/fastlane/2.212.1/libexec/gems/commander-4.6.0/lib/commander/command.rb:187:in `call' /usr/local/Cellar/fastlane/2.212.1/libexec/gems/commander-4.6.0/lib/commander/command.rb:157:in `run' /usr/local/Cellar/fastlane/2.212.1/libexec/gems/commander-4.6.0/lib/commander/runner.rb:444:in `run_active_command' /usr/local/Cellar/fastlane/2.212.1/libexec/gems/fastlane-2.212.1/fastlane_core/lib/fastlane_core/ui/fastlane_runner.rb:124:in `run!' /usr/local/Cellar/fastlane/2.212.1/libexec/gems/commander-4.6.0/lib/commander/delegates.rb:18:in `run!' /usr/local/Cellar/fastlane/2.212.1/libexec/gems/fastlane-2.212.1/fastlane/lib/fastlane/commands_generator.rb:354:in `run' /usr/local/Cellar/fastlane/2.212.1/libexec/gems/fastlane-2.212.1/fastlane/lib/fastlane/commands_generator.rb:43:in `start' /usr/local/Cellar/fastlane/2.212.1/libexec/gems/fastlane-2.212.1/fastlane/lib/fastlane/cli_tools_distributor.rb:123:in `take_off' /usr/local/Cellar/fastlane/2.212.1/libexec/gems/fastlane-2.212.1/bin/fastlane:23:in `<top (required)>' /usr/local/Cellar/fastlane/2.212.1/libexec/bin/fastlane:25:in `load' /usr/local/Cellar/fastlane/2.212.1/libexec/bin/fastlane:25:in `<main>' Error uploading ipa file: [Application Loader Error Output]: Error uploading '/var/folders/lp/myp2frzj00g93mcbnz6cd8900000gn/T/279a4279-ff7e-48d3-b6fd-1ee020a5b0a9.ipa'.
|
||||
<failure message="/opt/homebrew/Cellar/fastlane/2.212.0/libexec/gems/fastlane-2.212.0/fastlane/lib/fastlane/actions/actions_helper.rb:67:in `execute_action' /opt/homebrew/Cellar/fastlane/2.212.0/libexec/gems/fastlane-2.212.0/fastlane/lib/fastlane/runner.rb:255:in `block in execute_action' /opt/homebrew/Cellar/fastlane/2.212.0/libexec/gems/fastlane-2.212.0/fastlane/lib/fastlane/runner.rb:229:in `chdir' /opt/homebrew/Cellar/fastlane/2.212.0/libexec/gems/fastlane-2.212.0/fastlane/lib/fastlane/runner.rb:229:in `execute_action' /opt/homebrew/Cellar/fastlane/2.212.0/libexec/gems/fastlane-2.212.0/fastlane/lib/fastlane/runner.rb:157:in `trigger_action_by_name' /opt/homebrew/Cellar/fastlane/2.212.0/libexec/gems/fastlane-2.212.0/fastlane/lib/fastlane/fast_file.rb:159:in `method_missing' Fastfile:30:in `block (2 levels) in parsing_binding' /opt/homebrew/Cellar/fastlane/2.212.0/libexec/gems/fastlane-2.212.0/fastlane/lib/fastlane/lane.rb:33:in `call' /opt/homebrew/Cellar/fastlane/2.212.0/libexec/gems/fastlane-2.212.0/fastlane/lib/fastlane/runner.rb:49:in `block in execute' /opt/homebrew/Cellar/fastlane/2.212.0/libexec/gems/fastlane-2.212.0/fastlane/lib/fastlane/runner.rb:45:in `chdir' /opt/homebrew/Cellar/fastlane/2.212.0/libexec/gems/fastlane-2.212.0/fastlane/lib/fastlane/runner.rb:45:in `execute' /opt/homebrew/Cellar/fastlane/2.212.0/libexec/gems/fastlane-2.212.0/fastlane/lib/fastlane/lane_manager.rb:47:in `cruise_lane' /opt/homebrew/Cellar/fastlane/2.212.0/libexec/gems/fastlane-2.212.0/fastlane/lib/fastlane/command_line_handler.rb:36:in `handle' /opt/homebrew/Cellar/fastlane/2.212.0/libexec/gems/fastlane-2.212.0/fastlane/lib/fastlane/commands_generator.rb:110:in `block (2 levels) in run' /opt/homebrew/Cellar/fastlane/2.212.0/libexec/gems/commander-4.6.0/lib/commander/command.rb:187:in `call' /opt/homebrew/Cellar/fastlane/2.212.0/libexec/gems/commander-4.6.0/lib/commander/command.rb:157:in `run' /opt/homebrew/Cellar/fastlane/2.212.0/libexec/gems/commander-4.6.0/lib/commander/runner.rb:444:in `run_active_command' /opt/homebrew/Cellar/fastlane/2.212.0/libexec/gems/fastlane-2.212.0/fastlane_core/lib/fastlane_core/ui/fastlane_runner.rb:124:in `run!' /opt/homebrew/Cellar/fastlane/2.212.0/libexec/gems/commander-4.6.0/lib/commander/delegates.rb:18:in `run!' /opt/homebrew/Cellar/fastlane/2.212.0/libexec/gems/fastlane-2.212.0/fastlane/lib/fastlane/commands_generator.rb:354:in `run' /opt/homebrew/Cellar/fastlane/2.212.0/libexec/gems/fastlane-2.212.0/fastlane/lib/fastlane/commands_generator.rb:43:in `start' /opt/homebrew/Cellar/fastlane/2.212.0/libexec/gems/fastlane-2.212.0/fastlane/lib/fastlane/cli_tools_distributor.rb:123:in `take_off' /opt/homebrew/Cellar/fastlane/2.212.0/libexec/gems/fastlane-2.212.0/bin/fastlane:23:in `<top (required)>' /opt/homebrew/Cellar/fastlane/2.212.0/libexec/bin/fastlane:25:in `load' /opt/homebrew/Cellar/fastlane/2.212.0/libexec/bin/fastlane:25:in `<main>' Error uploading ipa file: [Application Loader Error Output]: Error uploading '/var/folders/_5/z27flzxx02j89sxdq8f1rwqc0000gn/T/77c4b65b-64d8-4545-8f0b-74399639f049.ipa'.
|
||||
[Application Loader Error Output]: Unable to upload archive. Failed to get authorization for username 'alex.tran1502@gmail.com' and password. (
|
||||
[Application Loader Error Output]: The call to the altool completed with a non-zero exit status: 1. This indicates a failure." />
|
||||
|
||||
|
||||
2
mobile/openapi/README.md
generated
2
mobile/openapi/README.md
generated
@@ -3,7 +3,7 @@ Immich API
|
||||
|
||||
This Dart package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project:
|
||||
|
||||
- API version: 1.53.0
|
||||
- API version: 1.54.0
|
||||
- Build package: org.openapitools.codegen.languages.DartClientCodegen
|
||||
|
||||
## Requirements
|
||||
|
||||
@@ -2,7 +2,7 @@ name: immich_mobile
|
||||
description: Immich - selfhosted backup media file on mobile phone
|
||||
|
||||
publish_to: "none"
|
||||
version: 1.54.0+77
|
||||
version: 1.54.1+77
|
||||
isar_version: &isar_version 3.0.5
|
||||
|
||||
environment:
|
||||
|
||||
@@ -3414,7 +3414,7 @@
|
||||
"info": {
|
||||
"title": "Immich",
|
||||
"description": "Immich API",
|
||||
"version": "1.54.0",
|
||||
"version": "1.54.1",
|
||||
"contact": {}
|
||||
},
|
||||
"tags": [],
|
||||
|
||||
2
server/package-lock.json
generated
2
server/package-lock.json
generated
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "immich",
|
||||
"version": "1.54.0",
|
||||
"version": "1.54.1",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "immich",
|
||||
"version": "1.54.0",
|
||||
"version": "1.54.1",
|
||||
"description": "",
|
||||
"author": "",
|
||||
"private": true,
|
||||
|
||||
2
web/src/api/open-api/api.ts
generated
2
web/src/api/open-api/api.ts
generated
@@ -4,7 +4,7 @@
|
||||
* Immich
|
||||
* Immich API
|
||||
*
|
||||
* The version of the OpenAPI document: 1.53.0
|
||||
* The version of the OpenAPI document: 1.54.0
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
|
||||
2
web/src/api/open-api/base.ts
generated
2
web/src/api/open-api/base.ts
generated
@@ -4,7 +4,7 @@
|
||||
* Immich
|
||||
* Immich API
|
||||
*
|
||||
* The version of the OpenAPI document: 1.53.0
|
||||
* The version of the OpenAPI document: 1.54.0
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
|
||||
2
web/src/api/open-api/common.ts
generated
2
web/src/api/open-api/common.ts
generated
@@ -4,7 +4,7 @@
|
||||
* Immich
|
||||
* Immich API
|
||||
*
|
||||
* The version of the OpenAPI document: 1.53.0
|
||||
* The version of the OpenAPI document: 1.54.0
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
|
||||
2
web/src/api/open-api/configuration.ts
generated
2
web/src/api/open-api/configuration.ts
generated
@@ -4,7 +4,7 @@
|
||||
* Immich
|
||||
* Immich API
|
||||
*
|
||||
* The version of the OpenAPI document: 1.53.0
|
||||
* The version of the OpenAPI document: 1.54.0
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
|
||||
2
web/src/api/open-api/index.ts
generated
2
web/src/api/open-api/index.ts
generated
@@ -4,7 +4,7 @@
|
||||
* Immich
|
||||
* Immich API
|
||||
*
|
||||
* The version of the OpenAPI document: 1.53.0
|
||||
* The version of the OpenAPI document: 1.54.0
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
|
||||
@@ -503,7 +503,11 @@
|
||||
/>
|
||||
|
||||
{#if album.assetCount > 0}
|
||||
<p class="my-4 text-sm text-gray-500 font-medium">{getDateRange()}</p>
|
||||
<span class="flex gap-2 my-4 text-sm text-gray-500 font-medium" data-testid="album-details">
|
||||
<p class="">{getDateRange()}</p>
|
||||
<p>·</p>
|
||||
<p>{album.assetCount} items</p>
|
||||
</span>
|
||||
{/if}
|
||||
{#if album.shared}
|
||||
<div class="my-6 flex">
|
||||
@@ -526,7 +530,12 @@
|
||||
{/if}
|
||||
|
||||
{#if album.assetCount > 0}
|
||||
<GalleryViewer assets={album.assets} {sharedLink} bind:selectedAssets={multiSelectAsset} />
|
||||
<GalleryViewer
|
||||
assets={album.assets}
|
||||
{sharedLink}
|
||||
bind:selectedAssets={multiSelectAsset}
|
||||
viewFrom="album-page"
|
||||
/>
|
||||
{:else}
|
||||
<!-- Album is empty - Show asset selectection buttons -->
|
||||
<section id="empty-album" class=" mt-[200px] flex place-content-center place-items-center">
|
||||
|
||||
@@ -14,6 +14,8 @@
|
||||
import ContentCopy from 'svelte-material-icons/ContentCopy.svelte';
|
||||
import MotionPlayOutline from 'svelte-material-icons/MotionPlayOutline.svelte';
|
||||
import MotionPauseOutline from 'svelte-material-icons/MotionPauseOutline.svelte';
|
||||
import ArchiveArrowDownOutline from 'svelte-material-icons/ArchiveArrowDownOutline.svelte';
|
||||
import ArchiveArrowUpOutline from 'svelte-material-icons/ArchiveArrowUpOutline.svelte';
|
||||
|
||||
import { page } from '$app/stores';
|
||||
import { AssetResponseDto } from '../../../api';
|
||||
@@ -49,6 +51,14 @@
|
||||
<CircleIconButton logo={ArrowLeft} on:click={() => dispatch('goBack')} />
|
||||
</div>
|
||||
<div class="text-white flex gap-2">
|
||||
{#if isOwner}
|
||||
<CircleIconButton
|
||||
logo={asset.isArchived ? ArchiveArrowUpOutline : ArchiveArrowDownOutline}
|
||||
title={asset.isArchived ? 'Unarchive' : 'Archive'}
|
||||
on:click={() => dispatch('toggleArchive')}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#if showMotionPlayButton}
|
||||
{#if isMotionPhotoPlaying}
|
||||
<CircleIconButton
|
||||
|
||||
@@ -263,6 +263,35 @@
|
||||
document.addEventListener('keydown', onKeyboardPress);
|
||||
}
|
||||
};
|
||||
|
||||
const toggleArchive = async () => {
|
||||
try {
|
||||
const { data } = await api.assetApi.updateAsset(asset.id, {
|
||||
isArchived: !asset.isArchived
|
||||
});
|
||||
|
||||
asset.isArchived = data.isArchived;
|
||||
|
||||
if (data.isArchived) {
|
||||
dispatch('archived', data);
|
||||
} else {
|
||||
dispatch('unarchived', data);
|
||||
}
|
||||
|
||||
notificationController.show({
|
||||
type: NotificationType.Info,
|
||||
message: asset.isArchived ? `Added to archive` : `Removed from archive`
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
notificationController.show({
|
||||
type: NotificationType.Error,
|
||||
message: `Error ${
|
||||
asset.isArchived ? 'archiving' : 'unarchiving'
|
||||
} asset, check console for more details`
|
||||
});
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<section
|
||||
@@ -285,6 +314,7 @@
|
||||
on:addToSharedAlbum={() => openAlbumPicker(true)}
|
||||
on:playMotionPhoto={() => (shouldPlayMotionPhoto = true)}
|
||||
on:stopMotionPhoto={() => (shouldPlayMotionPhoto = false)}
|
||||
on:toggleArchive={toggleArchive}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
import MotionPauseOutline from 'svelte-material-icons/MotionPauseOutline.svelte';
|
||||
import MotionPlayOutline from 'svelte-material-icons/MotionPlayOutline.svelte';
|
||||
import Star from 'svelte-material-icons/Star.svelte';
|
||||
import ArchiveArrowDownOutline from 'svelte-material-icons/ArchiveArrowDownOutline.svelte';
|
||||
import ImageThumbnail from './image-thumbnail.svelte';
|
||||
import VideoThumbnail from './video-thumbnail.svelte';
|
||||
|
||||
@@ -22,6 +23,7 @@
|
||||
export let disabled = false;
|
||||
export let readonly = false;
|
||||
export let publicSharedKey: string | undefined = undefined;
|
||||
export let showArchiveIcon = false;
|
||||
|
||||
let mouseOver = false;
|
||||
|
||||
@@ -114,6 +116,11 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if showArchiveIcon && asset.isArchived}
|
||||
<div class="absolute {asset.isFavorite ? 'bottom-10' : 'bottom-2'} left-2 z-10">
|
||||
<ArchiveArrowDownOutline size="24" class="text-white" />
|
||||
</div>
|
||||
{/if}
|
||||
<ImageThumbnail
|
||||
url={api.getAssetThumbnailUrl(asset.id, format, publicSharedKey)}
|
||||
altText={asset.originalFileName}
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
} from '$lib/stores/asset-interaction.store';
|
||||
import { locale } from '$lib/stores/preferences.store';
|
||||
import Thumbnail from '../assets/thumbnail/thumbnail.svelte';
|
||||
import { flip } from 'svelte/animate';
|
||||
|
||||
export let assets: AssetResponseDto[];
|
||||
export let bucketDate: string;
|
||||
@@ -107,12 +108,17 @@
|
||||
// Show multi select icon on hover on date group
|
||||
hoveredDateGroup = dateGroupTitle;
|
||||
};
|
||||
|
||||
let clientWidth = 0;
|
||||
</script>
|
||||
|
||||
<!-- <svelte:window bind:innerWidth={clientWidth} /> -->
|
||||
|
||||
<section
|
||||
id="asset-group-by-date"
|
||||
class="flex flex-wrap gap-12 mt-5"
|
||||
bind:clientHeight={actualBucketHeight}
|
||||
bind:clientWidth
|
||||
>
|
||||
{#each assetsGroupByDate as assetsInDateGroup, groupIndex (assetsInDateGroup[0].id)}
|
||||
{@const dateGroupTitle = new Date(assetsInDateGroup[0].fileCreatedAt).toLocaleDateString(
|
||||
@@ -122,6 +128,7 @@
|
||||
<!-- Asset Group By Date -->
|
||||
|
||||
<div
|
||||
animate:flip={{ duration: 300 }}
|
||||
class="flex flex-col"
|
||||
on:mouseenter={() => {
|
||||
isMouseOverGroup = true;
|
||||
@@ -131,7 +138,7 @@
|
||||
>
|
||||
<!-- Date group title -->
|
||||
<p
|
||||
class="font-medium text-sm text-immich-fg dark:text-immich-dark-fg mb-2 flex place-items-center h-6"
|
||||
class="font-medium text-xs md:text-sm text-immich-fg dark:text-immich-dark-fg mb-2 flex place-items-center h-6"
|
||||
>
|
||||
{#if (hoveredDateGroup == dateGroupTitle && isMouseOverGroup) || $selectedGroup.has(dateGroupTitle)}
|
||||
<div
|
||||
@@ -156,16 +163,20 @@
|
||||
<!-- Image grid -->
|
||||
<div class="flex flex-wrap gap-[2px]">
|
||||
{#each assetsInDateGroup as asset (asset.id)}
|
||||
<Thumbnail
|
||||
{asset}
|
||||
{groupIndex}
|
||||
on:click={() => assetClickHandler(asset, assetsInDateGroup, dateGroupTitle)}
|
||||
on:select={() => assetSelectHandler(asset, assetsInDateGroup, dateGroupTitle)}
|
||||
on:mouse-event={() => assetMouseEventHandler(dateGroupTitle)}
|
||||
selected={$selectedAssets.has(asset) ||
|
||||
$assetsInAlbumStoreState.findIndex((a) => a.id == asset.id) != -1}
|
||||
disabled={$assetsInAlbumStoreState.findIndex((a) => a.id == asset.id) != -1}
|
||||
/>
|
||||
<div animate:flip={{ duration: 300 }}>
|
||||
<Thumbnail
|
||||
thumbnailWidth={clientWidth <= 768 ? clientWidth / 4 - 4 : undefined}
|
||||
thumbnailHeight={clientWidth <= 768 ? clientWidth / 4 - 4 : undefined}
|
||||
{asset}
|
||||
{groupIndex}
|
||||
on:click={() => assetClickHandler(asset, assetsInDateGroup, dateGroupTitle)}
|
||||
on:select={() => assetSelectHandler(asset, assetsInDateGroup, dateGroupTitle)}
|
||||
on:mouse-event={() => assetMouseEventHandler(dateGroupTitle)}
|
||||
selected={$selectedAssets.has(asset) ||
|
||||
$assetsInAlbumStoreState.findIndex((a) => a.id == asset.id) != -1}
|
||||
disabled={$assetsInAlbumStoreState.findIndex((a) => a.id == asset.id) != -1}
|
||||
/>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
import IntersectionObserver from '../asset-viewer/intersection-observer.svelte';
|
||||
import { assetGridState, assetStore, loadingBucketState } from '$lib/stores/assets.store';
|
||||
import { api, AssetCountByTimeBucketResponseDto, TimeGroupEnum } from '@api';
|
||||
import { api, AssetCountByTimeBucketResponseDto, AssetResponseDto, TimeGroupEnum } from '@api';
|
||||
import AssetDateGroup from './asset-date-group.svelte';
|
||||
import Portal from '../shared-components/portal/portal.svelte';
|
||||
import AssetViewer from '../asset-viewer/asset-viewer.svelte';
|
||||
@@ -89,6 +89,12 @@
|
||||
const handleScrollbarDrag = (e: OnScrollbarDragDetail) => {
|
||||
assetGridElement.scrollTop = e.scrollTo;
|
||||
};
|
||||
|
||||
const handleArchiveSuccess = (e: CustomEvent) => {
|
||||
const asset = e.detail as AssetResponseDto;
|
||||
navigateToNextAsset();
|
||||
assetStore.removeAsset(asset.id);
|
||||
};
|
||||
</script>
|
||||
|
||||
{#if bucketInfo && viewportHeight && $assetGridState.timelineHeight > viewportHeight}
|
||||
@@ -149,6 +155,7 @@
|
||||
on:close={() => {
|
||||
assetInteractionStore.setIsViewingAsset(false);
|
||||
}}
|
||||
on:archived={handleArchiveSuccess}
|
||||
/>
|
||||
{/if}
|
||||
</Portal>
|
||||
|
||||
@@ -147,6 +147,6 @@
|
||||
</ControlAppBar>
|
||||
{/if}
|
||||
<section class="flex flex-col my-[160px] px-6 sm:px-12 md:px-24 lg:px-40">
|
||||
<GalleryViewer {assets} {sharedLink} bind:selectedAssets />
|
||||
<GalleryViewer {assets} {sharedLink} bind:selectedAssets viewFrom="shared-link-page" />
|
||||
</section>
|
||||
</section>
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
|
||||
{#if showMessage}
|
||||
<div
|
||||
class="text-sm border rounded-xl p-4 text-immich-primary dark:text-immich-dark-primary font-medium bg-immich-primary/5 dark:border-immich-dark-bg w-full border-immich-primary border-2"
|
||||
class="text-sm rounded-xl p-4 text-immich-primary dark:text-immich-dark-primary font-medium bg-immich-primary/5 dark:border-immich-dark-bg w-full border-immich-primary border-2"
|
||||
>
|
||||
<slot name="message" />
|
||||
</div>
|
||||
|
||||
@@ -1,3 +1,12 @@
|
||||
<script lang="ts" context="module">
|
||||
export type ViewFrom =
|
||||
| 'archive-page'
|
||||
| 'album-page'
|
||||
| 'favorites-page'
|
||||
| 'search-page'
|
||||
| 'shared-link-page';
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { page } from '$app/stores';
|
||||
import Thumbnail from '$lib/components/assets/thumbnail/thumbnail.svelte';
|
||||
@@ -6,11 +15,14 @@
|
||||
import AssetViewer from '../../asset-viewer/asset-viewer.svelte';
|
||||
import justifiedLayout from 'justified-layout';
|
||||
import { flip } from 'svelte/animate';
|
||||
import { archivedAsset } from '$lib/stores/archived-asset.store';
|
||||
|
||||
export let assets: AssetResponseDto[];
|
||||
export let sharedLink: SharedLinkResponseDto | undefined = undefined;
|
||||
export let selectedAssets: Set<AssetResponseDto> = new Set();
|
||||
export let disableAssetSelect = false;
|
||||
export let viewFrom: ViewFrom;
|
||||
export let showArchiveIcon = false;
|
||||
|
||||
let isShowAssetViewer = false;
|
||||
|
||||
@@ -20,25 +32,24 @@
|
||||
let viewWidth: number;
|
||||
|
||||
$: isMultiSelectionMode = selectedAssets.size > 0;
|
||||
$: geometry = justifiedLayout(assets.map(getAssetRatio), {
|
||||
boxSpacing: 5,
|
||||
containerWidth: Math.floor(viewWidth),
|
||||
targetRowHeight: 235
|
||||
});
|
||||
|
||||
function getAssetRatio(asset: AssetResponseDto) {
|
||||
let height = asset.exifInfo?.exifImageHeight || 235;
|
||||
let width = asset.exifInfo?.exifImageWidth || 235;
|
||||
|
||||
function getAssetRatio(asset: AssetResponseDto): number {
|
||||
const height = asset.exifInfo?.exifImageHeight;
|
||||
const width = asset.exifInfo?.exifImageWidth;
|
||||
const orientation = Number(asset.exifInfo?.orientation);
|
||||
|
||||
if (height && width) {
|
||||
if (orientation) {
|
||||
if (orientation == 6 || orientation == -90) {
|
||||
return height / width;
|
||||
} else {
|
||||
return width / height;
|
||||
}
|
||||
if (orientation) {
|
||||
if (orientation == 6 || orientation == -90) {
|
||||
[width, height] = [height, width];
|
||||
}
|
||||
|
||||
return width / height;
|
||||
}
|
||||
|
||||
return 1;
|
||||
return { width, height };
|
||||
}
|
||||
|
||||
const viewAssetHandler = (event: CustomEvent) => {
|
||||
@@ -97,29 +108,43 @@
|
||||
isShowAssetViewer = false;
|
||||
history.pushState(null, '', `${$page.url.pathname}`);
|
||||
};
|
||||
|
||||
const handleUnarchivedSuccess = (event: CustomEvent) => {
|
||||
const asset = event.detail as AssetResponseDto;
|
||||
switch (viewFrom) {
|
||||
case 'archive-page':
|
||||
$archivedAsset = $archivedAsset.filter((a) => a.id != asset.id);
|
||||
navigateAssetForward();
|
||||
break;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
{#if assets.length > 0}
|
||||
<div class="flex flex-wrap gap-1 w-full pb-20" bind:clientWidth={viewWidth}>
|
||||
<div
|
||||
class="relative w-full mb-20"
|
||||
bind:clientWidth={viewWidth}
|
||||
style="height: {geometry.containerHeight}px"
|
||||
>
|
||||
{#if viewWidth}
|
||||
{@const geoArray = assets.map(getAssetRatio)}
|
||||
{@const justifiedLayoutResult = justifiedLayout(geoArray, {
|
||||
targetRowHeight: 235,
|
||||
containerWidth: Math.floor(viewWidth)
|
||||
})}
|
||||
|
||||
{#each assets as asset, index (asset.id)}
|
||||
<div animate:flip={{ duration: 500 }}>
|
||||
{@const box = geometry.boxes[index]}
|
||||
<div
|
||||
class="absolute"
|
||||
style="width: {box.width}px; height: {box.height}px; top: {box.top}px; left: {box.left}px"
|
||||
animate:flip={{ duration: 500 }}
|
||||
>
|
||||
<Thumbnail
|
||||
{asset}
|
||||
thumbnailWidth={justifiedLayoutResult.boxes[index].width || 235}
|
||||
thumbnailHeight={justifiedLayoutResult.boxes[index].height || 235}
|
||||
thumbnailWidth={box.width}
|
||||
thumbnailHeight={box.height}
|
||||
readonly={disableAssetSelect}
|
||||
publicSharedKey={sharedLink?.key}
|
||||
format={assets.length < 7 ? ThumbnailFormat.Jpeg : ThumbnailFormat.Webp}
|
||||
on:click={(e) => (isMultiSelectionMode ? selectAssetHandler(e) : viewAssetHandler(e))}
|
||||
on:select={selectAssetHandler}
|
||||
selected={selectedAssets.has(asset)}
|
||||
{showArchiveIcon}
|
||||
/>
|
||||
</div>
|
||||
{/each}
|
||||
@@ -136,5 +161,6 @@
|
||||
on:navigate-previous={navigateAssetBackward}
|
||||
on:navigate-next={navigateAssetForward}
|
||||
on:close={closeViewer}
|
||||
on:unarchived={handleUnarchivedSuccess}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
export let title: string;
|
||||
export let logo: typeof Icon;
|
||||
export let isSelected: boolean;
|
||||
export let flippedLogo = false;
|
||||
|
||||
let showMoreInformation = false;
|
||||
|
||||
@@ -25,7 +26,11 @@
|
||||
"
|
||||
>
|
||||
<div class="flex gap-4 place-items-center w-full overflow-hidden truncate">
|
||||
<svelte:component this={logo} size="1.5em" class="shrink-0" />
|
||||
<svelte:component
|
||||
this={logo}
|
||||
size="1.5em"
|
||||
class="shrink-0 {flippedLogo ? '-scale-x-100' : ''}"
|
||||
/>
|
||||
<p class="font-medium text-sm">{title}</p>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -151,6 +151,7 @@
|
||||
<SideBarButton
|
||||
title="Albums"
|
||||
logo={ImageAlbum}
|
||||
flippedLogo={true}
|
||||
isSelected={$page.route.id === '/(user)/albums'}
|
||||
>
|
||||
<svelte:fragment slot="moreInformation">
|
||||
|
||||
@@ -56,16 +56,19 @@
|
||||
<p class="font-medium text-gray-800 dark:text-immich-dark-primary">
|
||||
{album.albumName}
|
||||
</p>
|
||||
|
||||
{#await getAlbumOwnerInfo() then albumOwner}
|
||||
{#if user.email == albumOwner.email}
|
||||
<p class="text-xs text-gray-600 dark:text-immich-dark-fg">Owned</p>
|
||||
{:else}
|
||||
<p class="text-xs text-gray-600 dark:text-immich-dark-fg">
|
||||
Shared by {albumOwner.firstName}
|
||||
{albumOwner.lastName}
|
||||
</p>
|
||||
{/if}
|
||||
{/await}
|
||||
<span class="text-xs flex gap-2 dark:text-immich-dark-fg" data-testid="album-details"
|
||||
><p>{album.assetCount} items</p>
|
||||
<p>·</p>
|
||||
{#await getAlbumOwnerInfo() then albumOwner}
|
||||
{#if user.email == albumOwner.email}
|
||||
<p class="text-xs text-gray-600 dark:text-immich-dark-fg">Owned</p>
|
||||
{:else}
|
||||
<p class="text-xs text-gray-600 dark:text-immich-dark-fg">
|
||||
Shared by {albumOwner.firstName}
|
||||
{albumOwner.lastName}
|
||||
</p>
|
||||
{/if}
|
||||
{/await}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
4
web/src/lib/stores/archived-asset.store.ts
Normal file
4
web/src/lib/stores/archived-asset.store.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import { AssetResponseDto } from '@api';
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
export const archivedAsset = writable<AssetResponseDto[]>([]);
|
||||
@@ -25,13 +25,14 @@
|
||||
import { onMount } from 'svelte';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import GalleryViewer from '$lib/components/shared-components/gallery-viewer/gallery-viewer.svelte';
|
||||
import { archivedAsset } from '$lib/stores/archived-asset.store';
|
||||
|
||||
export let data: PageData;
|
||||
|
||||
onMount(async () => {
|
||||
try {
|
||||
const { data: assets } = await api.assetApi.getAllAssets(undefined, true);
|
||||
archived = assets;
|
||||
$archivedAsset = assets;
|
||||
} catch {
|
||||
handleError(Error, 'Unable to load archived assets');
|
||||
}
|
||||
@@ -54,7 +55,7 @@
|
||||
|
||||
for (const asset of deletedAssets) {
|
||||
if (asset.status == 'SUCCESS') {
|
||||
archived = archived.filter((a) => a.id != asset.id);
|
||||
$archivedAsset = $archivedAsset.filter((a) => a.id != asset.id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,7 +73,6 @@
|
||||
$: isMultiSelectionMode = selectedAssets.size > 0;
|
||||
|
||||
let selectedAssets: Set<AssetResponseDto> = new Set();
|
||||
let archived: AssetResponseDto[] = [];
|
||||
|
||||
let contextMenuPosition = { x: 0, y: 0 };
|
||||
let isShowCreateSharedLinkModal = false;
|
||||
@@ -157,7 +157,7 @@
|
||||
});
|
||||
cnt = cnt + 1;
|
||||
|
||||
archived = archived.filter((a) => a.id != asset.id);
|
||||
$archivedAsset = $archivedAsset.filter((a) => a.id != asset.id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -181,7 +181,7 @@
|
||||
|
||||
<UserPageLayout user={data.user} hideNavbar={isMultiSelectionMode}>
|
||||
<!-- Empty Message -->
|
||||
{#if archived.length === 0}
|
||||
{#if $archivedAsset.length === 0}
|
||||
<EmptyPlaceholder
|
||||
text="Archive photos and videos to hide them from your Photos view"
|
||||
alt="Empty archive"
|
||||
@@ -255,5 +255,5 @@
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
|
||||
<GalleryViewer assets={archived} bind:selectedAssets />
|
||||
<GalleryViewer assets={$archivedAsset} bind:selectedAssets viewFrom="archive-page" />
|
||||
</UserPageLayout>
|
||||
|
||||
@@ -106,6 +106,6 @@
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<GalleryViewer assets={favorites} bind:selectedAssets />
|
||||
<GalleryViewer assets={favorites} bind:selectedAssets viewFrom="favorites-page" />
|
||||
</section>
|
||||
</UserPageLayout>
|
||||
|
||||
@@ -7,7 +7,25 @@
|
||||
import ImageOffOutline from 'svelte-material-icons/ImageOffOutline.svelte';
|
||||
import SearchBar from '$lib/components/shared-components/search-bar/search-bar.svelte';
|
||||
import { afterNavigate, goto } from '$app/navigation';
|
||||
|
||||
import AlbumSelectionModal from '$lib/components/shared-components/album-selection-modal.svelte';
|
||||
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
|
||||
import ContextMenu from '$lib/components/shared-components/context-menu/context-menu.svelte';
|
||||
import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte';
|
||||
import CreateSharedLinkModal from '$lib/components/shared-components/create-share-link-modal/create-shared-link-modal.svelte';
|
||||
import {
|
||||
notificationController,
|
||||
NotificationType
|
||||
} from '$lib/components/shared-components/notification/notification';
|
||||
import { addAssetsToAlbum, bulkDownload } from '$lib/utils/asset-utils';
|
||||
import { AlbumResponseDto, api, AssetResponseDto, SharedLinkType } from '@api';
|
||||
import Close from 'svelte-material-icons/Close.svelte';
|
||||
import CloudDownloadOutline from 'svelte-material-icons/CloudDownloadOutline.svelte';
|
||||
import ArchiveArrowUpOutline from 'svelte-material-icons/ArchiveArrowUpOutline.svelte';
|
||||
import ArchiveArrowDownOutline from 'svelte-material-icons/ArchiveArrowDownOutline.svelte';
|
||||
import DeleteOutline from 'svelte-material-icons/DeleteOutline.svelte';
|
||||
import Plus from 'svelte-material-icons/Plus.svelte';
|
||||
import ShareVariantOutline from 'svelte-material-icons/ShareVariantOutline.svelte';
|
||||
import { locale } from '$lib/stores/preferences.store';
|
||||
export let data: PageData;
|
||||
|
||||
// The GalleryViewer pushes it's own history state, which causes weird
|
||||
@@ -23,14 +41,209 @@
|
||||
});
|
||||
|
||||
$: term = $page.url.searchParams.get('q') || data.term || '';
|
||||
|
||||
let selectedAssets: Set<AssetResponseDto> = new Set();
|
||||
$: isMultiSelectionMode = selectedAssets.size > 0;
|
||||
$: isAllArchived = Array.from(selectedAssets).every((asset) => asset.isArchived);
|
||||
$: isAllFavorite = Array.from(selectedAssets).every((asset) => asset.isFavorite);
|
||||
|
||||
let contextMenuPosition = { x: 0, y: 0 };
|
||||
let isShowCreateSharedLinkModal = false;
|
||||
let isShowAddMenu = false;
|
||||
let isShowAlbumPicker = false;
|
||||
let addToSharedAlbum = false;
|
||||
$: searchResultAssets = data.results.assets.items;
|
||||
|
||||
const handleShowMenu = ({ x, y }: MouseEvent) => {
|
||||
contextMenuPosition = { x, y };
|
||||
isShowAddMenu = !isShowAddMenu;
|
||||
};
|
||||
|
||||
const handleShowAlbumPicker = (shared: boolean) => {
|
||||
isShowAddMenu = false;
|
||||
isShowAlbumPicker = true;
|
||||
addToSharedAlbum = shared;
|
||||
};
|
||||
|
||||
const handleAddToNewAlbum = (event: CustomEvent) => {
|
||||
isShowAlbumPicker = false;
|
||||
|
||||
const { albumName }: { albumName: string } = event.detail;
|
||||
const assetIds = Array.from(selectedAssets).map((asset) => asset.id);
|
||||
api.albumApi.createAlbum({ albumName, assetIds }).then((response) => {
|
||||
const { id, albumName } = response.data;
|
||||
|
||||
notificationController.show({
|
||||
message: `Added ${assetIds.length} to ${albumName}`,
|
||||
type: NotificationType.Info
|
||||
});
|
||||
|
||||
clearMultiSelectAssetAssetHandler();
|
||||
|
||||
goto('/albums/' + id);
|
||||
});
|
||||
};
|
||||
|
||||
const handleAddToAlbum = async (event: CustomEvent<{ album: AlbumResponseDto }>) => {
|
||||
isShowAlbumPicker = false;
|
||||
const album = event.detail.album;
|
||||
|
||||
const assetIds = Array.from(selectedAssets).map((asset) => asset.id);
|
||||
|
||||
addAssetsToAlbum(album.id, assetIds).then(() => {
|
||||
clearMultiSelectAssetAssetHandler();
|
||||
});
|
||||
};
|
||||
|
||||
const handleDownloadFiles = async () => {
|
||||
await bulkDownload('immich', Array.from(selectedAssets), () => {
|
||||
clearMultiSelectAssetAssetHandler();
|
||||
});
|
||||
};
|
||||
|
||||
const toggleArchive = async () => {
|
||||
let cnt = 0;
|
||||
for (const asset of selectedAssets) {
|
||||
api.assetApi.updateAsset(asset.id, {
|
||||
isArchived: !isAllArchived
|
||||
});
|
||||
cnt = cnt + 1;
|
||||
|
||||
asset.isArchived = !isAllArchived;
|
||||
|
||||
searchResultAssets = searchResultAssets.map((a: AssetResponseDto) => {
|
||||
if (a.id === asset.id) {
|
||||
a = asset;
|
||||
}
|
||||
|
||||
return a;
|
||||
});
|
||||
}
|
||||
|
||||
notificationController.show({
|
||||
message: `${isAllArchived ? `Remove ${cnt} from` : `Add ${cnt} to`} archive`,
|
||||
type: NotificationType.Info
|
||||
});
|
||||
|
||||
clearMultiSelectAssetAssetHandler();
|
||||
};
|
||||
|
||||
const toggleFavorite = () => {
|
||||
isShowAddMenu = false;
|
||||
|
||||
let cnt = 0;
|
||||
for (const asset of selectedAssets) {
|
||||
api.assetApi.updateAsset(asset.id, {
|
||||
isFavorite: !isAllFavorite
|
||||
});
|
||||
cnt = cnt + 1;
|
||||
|
||||
asset.isFavorite = !isAllFavorite;
|
||||
|
||||
searchResultAssets = searchResultAssets.map((a: AssetResponseDto) => {
|
||||
if (a.id === asset.id) {
|
||||
a = asset;
|
||||
}
|
||||
return a;
|
||||
});
|
||||
}
|
||||
|
||||
notificationController.show({
|
||||
message: `${isAllFavorite ? `Remove ${cnt} from` : `Add ${cnt} to`} favorites`,
|
||||
type: NotificationType.Info
|
||||
});
|
||||
|
||||
clearMultiSelectAssetAssetHandler();
|
||||
};
|
||||
|
||||
const clearMultiSelectAssetAssetHandler = () => {
|
||||
selectedAssets = new Set();
|
||||
};
|
||||
|
||||
const deleteSelectedAssetHandler = async () => {
|
||||
try {
|
||||
if (
|
||||
window.confirm(
|
||||
`Caution! Are you sure you want to delete ${selectedAssets.size} assets? This step also deletes assets in the album(s) to which they belong. You can not undo this action!`
|
||||
)
|
||||
) {
|
||||
const { data: deletedAssets } = await api.assetApi.deleteAsset({
|
||||
ids: Array.from(selectedAssets).map((a) => a.id)
|
||||
});
|
||||
|
||||
for (const asset of deletedAssets) {
|
||||
if (asset.status == 'SUCCESS') {
|
||||
searchResultAssets = searchResultAssets.filter(
|
||||
(a: AssetResponseDto) => a.id != asset.id
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
clearMultiSelectAssetAssetHandler();
|
||||
}
|
||||
} catch (e) {
|
||||
notificationController.show({
|
||||
type: NotificationType.Error,
|
||||
message: 'Error deleting assets, check console for more details'
|
||||
});
|
||||
console.error('Error deleteSelectedAssetHandler', e);
|
||||
}
|
||||
};
|
||||
const handleCreateSharedLink = async () => {
|
||||
isShowCreateSharedLinkModal = true;
|
||||
};
|
||||
|
||||
const handleCloseSharedLinkModal = () => {
|
||||
clearMultiSelectAssetAssetHandler();
|
||||
isShowCreateSharedLinkModal = false;
|
||||
};
|
||||
</script>
|
||||
|
||||
<section>
|
||||
<ControlAppBar on:close-button-click={() => goto(previousRoute)} backIcon={ArrowLeft}>
|
||||
<div class="w-full max-w-2xl flex-1 pl-4">
|
||||
<SearchBar grayTheme={false} value={term} />
|
||||
</div>
|
||||
</ControlAppBar>
|
||||
{#if isMultiSelectionMode}
|
||||
<ControlAppBar
|
||||
on:close-button-click={clearMultiSelectAssetAssetHandler}
|
||||
backIcon={Close}
|
||||
tailwindClasses={'bg-white shadow-md'}
|
||||
>
|
||||
<svelte:fragment slot="leading">
|
||||
<p class="font-medium text-immich-primary dark:text-immich-dark-primary">
|
||||
Selected {selectedAssets.size.toLocaleString($locale)}
|
||||
</p>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="trailing">
|
||||
<CircleIconButton
|
||||
title="Share"
|
||||
logo={ShareVariantOutline}
|
||||
on:click={handleCreateSharedLink}
|
||||
/>
|
||||
|
||||
<CircleIconButton
|
||||
title={isAllArchived ? 'Unarchive' : 'Archive'}
|
||||
logo={isAllArchived ? ArchiveArrowUpOutline : ArchiveArrowDownOutline}
|
||||
on:click={toggleArchive}
|
||||
/>
|
||||
|
||||
<CircleIconButton
|
||||
title="Download"
|
||||
logo={CloudDownloadOutline}
|
||||
on:click={handleDownloadFiles}
|
||||
/>
|
||||
<CircleIconButton title="Add" logo={Plus} on:click={handleShowMenu} />
|
||||
<CircleIconButton
|
||||
title="Delete"
|
||||
logo={DeleteOutline}
|
||||
on:click={deleteSelectedAssetHandler}
|
||||
/>
|
||||
</svelte:fragment>
|
||||
</ControlAppBar>
|
||||
{:else}
|
||||
<ControlAppBar on:close-button-click={() => goto(previousRoute)} backIcon={ArrowLeft}>
|
||||
<div class="w-full max-w-2xl flex-1 pl-4">
|
||||
<SearchBar grayTheme={false} value={term} />
|
||||
</div>
|
||||
</ControlAppBar>
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
<section class="relative pt-32 mb-12 bg-immich-bg dark:bg-immich-dark-bg">
|
||||
@@ -38,7 +251,12 @@
|
||||
<section id="search-content" class="relative bg-immich-bg dark:bg-immich-dark-bg">
|
||||
{#if data.results?.assets?.items.length > 0}
|
||||
<div class="pl-4">
|
||||
<GalleryViewer assets={data.results.assets.items} disableAssetSelect />
|
||||
<GalleryViewer
|
||||
assets={searchResultAssets}
|
||||
bind:selectedAssets
|
||||
viewFrom="search-page"
|
||||
showArchiveIcon={true}
|
||||
/>
|
||||
</div>
|
||||
{:else}
|
||||
<div
|
||||
@@ -53,4 +271,35 @@
|
||||
{/if}
|
||||
</section>
|
||||
</section>
|
||||
|
||||
{#if isShowAddMenu}
|
||||
<ContextMenu {...contextMenuPosition} on:clickoutside={() => (isShowAddMenu = false)}>
|
||||
<div class="flex flex-col rounded-lg ">
|
||||
<MenuOption
|
||||
on:click={toggleFavorite}
|
||||
text={isAllFavorite ? 'Remove from favorites' : 'Add to favorites'}
|
||||
/>
|
||||
<MenuOption on:click={() => handleShowAlbumPicker(false)} text="Add to Album" />
|
||||
<MenuOption on:click={() => handleShowAlbumPicker(true)} text="Add to Shared Album" />
|
||||
</div>
|
||||
</ContextMenu>
|
||||
{/if}
|
||||
|
||||
{#if isShowAlbumPicker}
|
||||
<AlbumSelectionModal
|
||||
shared={addToSharedAlbum}
|
||||
on:newAlbum={handleAddToNewAlbum}
|
||||
on:newSharedAlbum={handleAddToNewAlbum}
|
||||
on:album={handleAddToAlbum}
|
||||
on:close={() => (isShowAlbumPicker = false)}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#if isShowCreateSharedLinkModal}
|
||||
<CreateSharedLinkModal
|
||||
sharedAssets={Array.from(selectedAssets)}
|
||||
shareType={SharedLinkType.Individual}
|
||||
on:close={handleCloseSharedLinkModal}
|
||||
/>
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
Reference in New Issue
Block a user