Compare commits

...

11 Commits

Author SHA1 Message Date
Alex The Bot
3d251f51fc Version v1.54.1 2023-04-23 02:36:09 +00:00
Alex
57704522cd feat(web): smaller thumbnails on timeline mobile (#2316) 2023-04-22 21:13:15 -05:00
faupau
787926c111 feat(web): show asset count in sharing tab and album viewer (#2311)
* show asset count in sharing tab

* add asset count to album-viewer

* remove duplicate font size

* fix test

---------

Co-authored-by: faupau03 <paul.paffe@gmx.net>
Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
2023-04-22 13:38:45 -05:00
Alex Tran
08b424b3df update gitignore 2023-04-22 13:31:42 -05:00
Jason Rasmussen
736a946101 fix(web): justified layout (#2313) 2023-04-21 16:24:25 -05:00
Alex
6f6f847ee2 feat(web): Add action button to search result page (#2303)
* feat(web): Add action button to search result page

* fix test

* rename

* pr feedback

* better condition

* fix test
2023-04-21 10:10:08 -05:00
Fynn Petersen-Frey
13be271df7 chore(Android): update gradle, kotlin, configure glide (#2306) 2023-04-21 05:31:44 -05:00
Jason Rasmussen
b423852fad chore: flip album icon (#2300) 2023-04-20 13:23:03 -05:00
Alex
fe3d6b870a feat(web): add button to archive and unarchive in detail viewer (#2296) 2023-04-20 09:09:27 -05:00
Alex
14be63039f chore(doc): update FAQ for Photo Stream on iOS (#2295) 2023-04-19 18:54:19 -05:00
Alex
d339d4c8dd post release note 2023-04-18 14:38:46 -05:00
40 changed files with 487 additions and 98 deletions

View File

@@ -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.

View File

@@ -1,3 +1,5 @@
*.zip
*.onnx
upload/
venv/
__pycache__/

View File

@@ -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"
}

View File

@@ -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>

View File

@@ -0,0 +1,7 @@
package app.alextran.immich
import com.bumptech.glide.annotation.GlideModule
import com.bumptech.glide.module.AppGlideModule
@GlideModule
class AppGlideModule : AppGlideModule()

View File

@@ -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"
}
}

View File

@@ -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')

View File

@@ -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.

View File

@@ -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>

View File

@@ -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

View File

@@ -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;

View File

@@ -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>

View File

@@ -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,

View File

@@ -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&apos;&#10;/usr/local/Cellar/fastlane/2.212.1/libexec/gems/fastlane-2.212.1/fastlane/lib/fastlane/runner.rb:255:in `block in execute_action&apos;&#10;/usr/local/Cellar/fastlane/2.212.1/libexec/gems/fastlane-2.212.1/fastlane/lib/fastlane/runner.rb:229:in `chdir&apos;&#10;/usr/local/Cellar/fastlane/2.212.1/libexec/gems/fastlane-2.212.1/fastlane/lib/fastlane/runner.rb:229:in `execute_action&apos;&#10;/usr/local/Cellar/fastlane/2.212.1/libexec/gems/fastlane-2.212.1/fastlane/lib/fastlane/runner.rb:157:in `trigger_action_by_name&apos;&#10;/usr/local/Cellar/fastlane/2.212.1/libexec/gems/fastlane-2.212.1/fastlane/lib/fastlane/fast_file.rb:159:in `method_missing&apos;&#10;Fastfile:30:in `block (2 levels) in parsing_binding&apos;&#10;/usr/local/Cellar/fastlane/2.212.1/libexec/gems/fastlane-2.212.1/fastlane/lib/fastlane/lane.rb:33:in `call&apos;&#10;/usr/local/Cellar/fastlane/2.212.1/libexec/gems/fastlane-2.212.1/fastlane/lib/fastlane/runner.rb:49:in `block in execute&apos;&#10;/usr/local/Cellar/fastlane/2.212.1/libexec/gems/fastlane-2.212.1/fastlane/lib/fastlane/runner.rb:45:in `chdir&apos;&#10;/usr/local/Cellar/fastlane/2.212.1/libexec/gems/fastlane-2.212.1/fastlane/lib/fastlane/runner.rb:45:in `execute&apos;&#10;/usr/local/Cellar/fastlane/2.212.1/libexec/gems/fastlane-2.212.1/fastlane/lib/fastlane/lane_manager.rb:47:in `cruise_lane&apos;&#10;/usr/local/Cellar/fastlane/2.212.1/libexec/gems/fastlane-2.212.1/fastlane/lib/fastlane/command_line_handler.rb:36:in `handle&apos;&#10;/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&apos;&#10;/usr/local/Cellar/fastlane/2.212.1/libexec/gems/commander-4.6.0/lib/commander/command.rb:187:in `call&apos;&#10;/usr/local/Cellar/fastlane/2.212.1/libexec/gems/commander-4.6.0/lib/commander/command.rb:157:in `run&apos;&#10;/usr/local/Cellar/fastlane/2.212.1/libexec/gems/commander-4.6.0/lib/commander/runner.rb:444:in `run_active_command&apos;&#10;/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!&apos;&#10;/usr/local/Cellar/fastlane/2.212.1/libexec/gems/commander-4.6.0/lib/commander/delegates.rb:18:in `run!&apos;&#10;/usr/local/Cellar/fastlane/2.212.1/libexec/gems/fastlane-2.212.1/fastlane/lib/fastlane/commands_generator.rb:354:in `run&apos;&#10;/usr/local/Cellar/fastlane/2.212.1/libexec/gems/fastlane-2.212.1/fastlane/lib/fastlane/commands_generator.rb:43:in `start&apos;&#10;/usr/local/Cellar/fastlane/2.212.1/libexec/gems/fastlane-2.212.1/fastlane/lib/fastlane/cli_tools_distributor.rb:123:in `take_off&apos;&#10;/usr/local/Cellar/fastlane/2.212.1/libexec/gems/fastlane-2.212.1/bin/fastlane:23:in `&lt;top (required)&gt;&apos;&#10;/usr/local/Cellar/fastlane/2.212.1/libexec/bin/fastlane:25:in `load&apos;&#10;/usr/local/Cellar/fastlane/2.212.1/libexec/bin/fastlane:25:in `&lt;main&gt;&apos;&#10;&#10;Error uploading ipa file: &#10; [Application Loader Error Output]: Error uploading &apos;/var/folders/lp/myp2frzj00g93mcbnz6cd8900000gn/T/279a4279-ff7e-48d3-b6fd-1ee020a5b0a9.ipa&apos;.
<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&apos;&#10;/opt/homebrew/Cellar/fastlane/2.212.0/libexec/gems/fastlane-2.212.0/fastlane/lib/fastlane/runner.rb:255:in `block in execute_action&apos;&#10;/opt/homebrew/Cellar/fastlane/2.212.0/libexec/gems/fastlane-2.212.0/fastlane/lib/fastlane/runner.rb:229:in `chdir&apos;&#10;/opt/homebrew/Cellar/fastlane/2.212.0/libexec/gems/fastlane-2.212.0/fastlane/lib/fastlane/runner.rb:229:in `execute_action&apos;&#10;/opt/homebrew/Cellar/fastlane/2.212.0/libexec/gems/fastlane-2.212.0/fastlane/lib/fastlane/runner.rb:157:in `trigger_action_by_name&apos;&#10;/opt/homebrew/Cellar/fastlane/2.212.0/libexec/gems/fastlane-2.212.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.212.0/libexec/gems/fastlane-2.212.0/fastlane/lib/fastlane/lane.rb:33:in `call&apos;&#10;/opt/homebrew/Cellar/fastlane/2.212.0/libexec/gems/fastlane-2.212.0/fastlane/lib/fastlane/runner.rb:49:in `block in execute&apos;&#10;/opt/homebrew/Cellar/fastlane/2.212.0/libexec/gems/fastlane-2.212.0/fastlane/lib/fastlane/runner.rb:45:in `chdir&apos;&#10;/opt/homebrew/Cellar/fastlane/2.212.0/libexec/gems/fastlane-2.212.0/fastlane/lib/fastlane/runner.rb:45:in `execute&apos;&#10;/opt/homebrew/Cellar/fastlane/2.212.0/libexec/gems/fastlane-2.212.0/fastlane/lib/fastlane/lane_manager.rb:47:in `cruise_lane&apos;&#10;/opt/homebrew/Cellar/fastlane/2.212.0/libexec/gems/fastlane-2.212.0/fastlane/lib/fastlane/command_line_handler.rb:36:in `handle&apos;&#10;/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&apos;&#10;/opt/homebrew/Cellar/fastlane/2.212.0/libexec/gems/commander-4.6.0/lib/commander/command.rb:187:in `call&apos;&#10;/opt/homebrew/Cellar/fastlane/2.212.0/libexec/gems/commander-4.6.0/lib/commander/command.rb:157:in `run&apos;&#10;/opt/homebrew/Cellar/fastlane/2.212.0/libexec/gems/commander-4.6.0/lib/commander/runner.rb:444:in `run_active_command&apos;&#10;/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!&apos;&#10;/opt/homebrew/Cellar/fastlane/2.212.0/libexec/gems/commander-4.6.0/lib/commander/delegates.rb:18:in `run!&apos;&#10;/opt/homebrew/Cellar/fastlane/2.212.0/libexec/gems/fastlane-2.212.0/fastlane/lib/fastlane/commands_generator.rb:354:in `run&apos;&#10;/opt/homebrew/Cellar/fastlane/2.212.0/libexec/gems/fastlane-2.212.0/fastlane/lib/fastlane/commands_generator.rb:43:in `start&apos;&#10;/opt/homebrew/Cellar/fastlane/2.212.0/libexec/gems/fastlane-2.212.0/fastlane/lib/fastlane/cli_tools_distributor.rb:123:in `take_off&apos;&#10;/opt/homebrew/Cellar/fastlane/2.212.0/libexec/gems/fastlane-2.212.0/bin/fastlane:23:in `&lt;top (required)&gt;&apos;&#10;/opt/homebrew/Cellar/fastlane/2.212.0/libexec/bin/fastlane:25:in `load&apos;&#10;/opt/homebrew/Cellar/fastlane/2.212.0/libexec/bin/fastlane:25:in `&lt;main&gt;&apos;&#10;&#10;Error uploading ipa file: &#10; [Application Loader Error Output]: Error uploading &apos;/var/folders/_5/z27flzxx02j89sxdq8f1rwqc0000gn/T/77c4b65b-64d8-4545-8f0b-74399639f049.ipa&apos;.
&#10;[Application Loader Error Output]: Unable to upload archive. Failed to get authorization for username &apos;alex.tran1502@gmail.com&apos; and password. (
&#10;[Application Loader Error Output]: The call to the altool completed with a non-zero exit status: 1. This indicates a failure." />

View File

@@ -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

View File

@@ -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:

View File

@@ -3414,7 +3414,7 @@
"info": {
"title": "Immich",
"description": "Immich API",
"version": "1.54.0",
"version": "1.54.1",
"contact": {}
},
"tags": [],

View File

@@ -1,6 +1,6 @@
{
"name": "immich",
"version": "1.54.0",
"version": "1.54.1",
"lockfileVersion": 2,
"requires": true,
"packages": {

View File

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

View File

@@ -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).

View File

@@ -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).

View File

@@ -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).

View File

@@ -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).

View File

@@ -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).

View File

@@ -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">

View File

@@ -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

View File

@@ -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>

View File

@@ -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}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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}

View File

@@ -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>

View File

@@ -151,6 +151,7 @@
<SideBarButton
title="Albums"
logo={ImageAlbum}
flippedLogo={true}
isSelected={$page.route.id === '/(user)/albums'}
>
<svelte:fragment slot="moreInformation">

View File

@@ -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>

View File

@@ -0,0 +1,4 @@
import { AssetResponseDto } from '@api';
import { writable } from 'svelte/store';
export const archivedAsset = writable<AssetResponseDto[]>([]);

View File

@@ -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>

View File

@@ -106,6 +106,6 @@
/>
{/if}
<GalleryViewer assets={favorites} bind:selectedAssets />
<GalleryViewer assets={favorites} bind:selectedAssets viewFrom="favorites-page" />
</section>
</UserPageLayout>

View File

@@ -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>