Compare commits

..

17 Commits

Author SHA1 Message Date
Alex Tran
736b968eab merge main 2024-04-22 06:52:30 -05:00
Alex Tran
d5d8426bda Merge branch 'main' of github.com:immich-app/immich into web/automation-ui 2024-04-22 06:51:57 -05:00
Alex
be4a783845 fix(web): wrong month on timeline scrollbar cursor (#8996)
* fix(web): wrong month on timeline scrollbar cursor

* revert unnesessary change
2024-04-22 06:22:59 -05:00
Mert
c30cd3b378 chore: test more formats in e2e (#9001) 2024-04-22 01:35:27 -04:00
TruongSinh Tran-Nguyen
0d3cc28f45 feat(web): support 360 video (equirectangular) (#8762)
* [web]: support 360 video

* lint

* lint

* fix typing

---------

Co-authored-by: Alex <alex.tran1502@gmail.com>
2024-04-21 19:14:54 +00:00
Conner Hnatiuk
f004487be0 fix(web): trash page now auto refreshes (#8978)
* fix(web): the trash page now auto refreshes when restore all or empty trash is clicked. Also shows number of assets affected.

* formatting
2024-04-21 14:07:17 -05:00
clementdelestre
21231d53a5 feat(mobile): add i18n in multiselect-grid and update translation (en and fr) (#8993)
* add i18n in multiselect grid (en-fr)

* add FR translations from (haptic feedback)

* revert settings
2024-04-21 13:26:19 -05:00
Daniel Dietzler
a99862120d feat: mobile label for renovate pull requests (#8991)
mobile lable for renovate pull requests
2024-04-21 13:11:03 -05:00
shenlong
776023b149 dep(mobile): upgrade gradle (#8409)
* dep(mobile): upgrade gradle

* chore(deps): update kotlin & guava

* build: change java version and flutter test version

---------

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2024-04-20 23:07:32 -05:00
martin
7d4187962a feat(web): new look option for slideshow (#8924)
feat: new look option for slideshow
2024-04-20 23:06:49 -05:00
Jason Rasmussen
a93534fc3c refactor(server): session interface types (#8977) 2024-04-20 23:45:55 -04:00
Alex
cef84f6ced chore(mobile): override appbundle on PlayStore before getting released (#8960) 2024-04-20 19:56:03 -05:00
Alex
7e92ef9428 Merge branch 'main' of github.com:immich-app/immich into web/automation-ui 2024-04-17 11:13:14 +02:00
Alex
372fae20d9 light theme 2024-04-08 07:37:25 +02:00
Alex
16543a233b basic ui complete 2024-04-07 20:37:26 -05:00
Alex
73b961f5fa colors 2024-04-07 19:38:23 -05:00
Alex
e0d15c96f1 feat(web): workflow automation ui 2024-04-07 19:18:49 -05:00
41 changed files with 484 additions and 93 deletions

View File

@@ -37,15 +37,15 @@ jobs:
- uses: actions/setup-java@v4
with:
distribution: "zulu"
java-version: "11.0.21+9"
cache: "gradle"
distribution: 'zulu'
java-version: '17'
cache: 'gradle'
- name: Setup Flutter SDK
uses: subosito/flutter-action@v2
with:
channel: "stable"
flutter-version: "3.19.3"
channel: 'stable'
flutter-version: '3.19.3'
cache: true
- name: Create the Keystore

View File

@@ -208,7 +208,7 @@ jobs:
uses: subosito/flutter-action@v2
with:
channel: 'stable'
flutter-version: '3.16.9'
flutter-version: '3.19.3'
- name: Run tests
working-directory: ./mobile
run: flutter test -j 1

View File

@@ -572,6 +572,22 @@ describe('/asset', () => {
}
const tests = [
{
input: 'formats/avif/8bit-sRGB.avif',
expected: {
type: AssetTypeEnum.Image,
originalFileName: '8bit-sRGB.avif',
resized: true,
exifInfo: {
description: '',
exifImageHeight: 1080,
exifImageWidth: 1617,
fileSizeInByte: 862_424,
latitude: null,
longitude: null,
},
},
},
{
input: 'formats/jpg/el_torcal_rocks.jpg',
expected: {
@@ -596,6 +612,22 @@ describe('/asset', () => {
},
},
},
{
input: 'formats/jxl/8bit-sRGB.jxl',
expected: {
type: AssetTypeEnum.Image,
originalFileName: '8bit-sRGB.jxl',
resized: true,
exifInfo: {
description: '',
exifImageHeight: 1080,
exifImageWidth: 1440,
fileSizeInByte: 1_780_777,
latitude: null,
longitude: null,
},
},
},
{
input: 'formats/heic/IMG_2682.heic',
expected: {
@@ -681,6 +713,80 @@ describe('/asset', () => {
},
},
},
{
input: 'formats/raw/Panasonic/DMC-GH4/4_3.rw2',
expected: {
type: AssetTypeEnum.Image,
originalFileName: '4_3.rw2',
resized: true,
fileCreatedAt: '2018-05-10T08:42:37.842Z',
exifInfo: {
make: 'Panasonic',
model: 'DMC-GH4',
exifImageHeight: 3456,
exifImageWidth: 4608,
exposureTime: '1/100',
fNumber: 3.2,
focalLength: 35,
iso: 400,
fileSizeInByte: 19_587_072,
dateTimeOriginal: '2018-05-10T08:42:37.842Z',
latitude: null,
longitude: null,
orientation: '1',
},
},
},
{
input: 'formats/raw/Sony/ILCE-6300/12bit-compressed-(3_2).arw',
expected: {
type: AssetTypeEnum.Image,
originalFileName: '12bit-compressed-(3_2).arw',
resized: true,
fileCreatedAt: '2016-09-27T10:51:44.000Z',
exifInfo: {
make: 'SONY',
model: 'ILCE-6300',
exifImageHeight: 4024,
exifImageWidth: 6048,
exposureTime: '1/320',
fNumber: 8,
focalLength: 97,
iso: 100,
lensModel: 'E PZ 18-105mm F4 G OSS',
fileSizeInByte: 25_001_984,
dateTimeOriginal: '2016-09-27T10:51:44.000Z',
latitude: null,
longitude: null,
orientation: '1',
},
},
},
{
input: 'formats/raw/Sony/ILCE-7M2/14bit-uncompressed-(3_2).arw',
expected: {
type: AssetTypeEnum.Image,
originalFileName: '14bit-uncompressed-(3_2).arw',
resized: true,
fileCreatedAt: '2016-01-08T15:08:01.000Z',
exifInfo: {
make: 'SONY',
model: 'ILCE-7M2',
exifImageHeight: 4024,
exifImageWidth: 6048,
exposureTime: '1.3',
fNumber: 22,
focalLength: 25,
iso: 100,
lensModel: 'E 25mm F2',
fileSizeInByte: 49_512_448,
dateTimeOriginal: '2016-01-08T15:08:01.000Z',
latitude: null,
longitude: null,
orientation: '1',
},
},
},
];
for (const { input, expected } of tests) {

View File

@@ -1,14 +1,14 @@
plugins {
id "com.android.application"
id "kotlin-android"
id "dev.flutter.flutter-gradle-plugin"
id "kotlin-kapt"
}
def localProperties = new Properties()
def localPropertiesFile = rootProject.file('local.properties')
if (localPropertiesFile.exists()) {
localPropertiesFile.withReader('UTF-8') { reader ->
localProperties.load(reader)
}
}
def flutterRoot = localProperties.getProperty('flutter.sdk')
if (flutterRoot == null) {
throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
localPropertiesFile.withInputStream { localProperties.load(it) }
}
def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
@@ -21,18 +21,12 @@ if (flutterVersionName == null) {
flutterVersionName = '1.0'
}
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()
def keystorePropertiesFile = rootProject.file('key.properties')
if (keystorePropertiesFile.exists()) {
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
keystorePropertiesFile.withInputStream { keystoreProperties.load(it) }
}
android {
compileSdkVersion 34
@@ -50,7 +44,6 @@ android {
}
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId "app.alextran.immich"
minSdkVersion 26
targetSdkVersion 33
@@ -88,6 +81,13 @@ flutter {
}
dependencies {
def kotlin_version = '1.9.23'
def kotlin_coroutines_version = '1.8.0'
def work_version = '2.9.0'
def concurrent_version = '1.1.0'
def guava_version = '33.1.0-android'
def glide_version = '4.16.0'
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlin_coroutines_version"
implementation "androidx.work:work-runtime-ktx:$work_version"

View File

@@ -276,7 +276,7 @@ class BackupWorker(ctx: Context, params: WorkerParameters) : ListenableWorker(ct
private const val NOTIFICATION_CHANNEL_ERROR_ID = "immich/backgroundServiceError"
private const val NOTIFICATION_DEFAULT_TITLE = "Immich"
private const val NOTIFICATION_ID = 1
private const val NOTIFICATION_ERROR_ID = 2
private const val NOTIFICATION_ERROR_ID = 2
private const val NOTIFICATION_DETAIL_ID = 3
private const val ONE_MINUTE = 60000L
@@ -304,7 +304,7 @@ class BackupWorker(ctx: Context, params: WorkerParameters) : ListenableWorker(ct
val workInfoList = workInfoFuture.get(1000, TimeUnit.MILLISECONDS)
if (workInfoList != null) {
for (workInfo in workInfoList) {
if (workInfo.getState() == WorkInfo.State.ENQUEUED) {
if (workInfo.state == WorkInfo.State.ENQUEUED) {
val workRequest = buildWorkRequest(requireWifi, requireCharging)
wm.enqueueUniqueWork(TASK_NAME_BACKUP, ExistingWorkPolicy.REPLACE, workRequest)
Log.d(TAG, "updateBackupWorker updated BackupWorker constraints")
@@ -346,7 +346,7 @@ class BackupWorker(ctx: Context, params: WorkerParameters) : ListenableWorker(ct
.setRequiresBatteryNotLow(true)
.setRequiresCharging(requireCharging)
.build();
val work = OneTimeWorkRequest.Builder(BackupWorker::class.java)
.setConstraints(constraints)
.setBackoffCriteria(BackoffPolicy.EXPONENTIAL, ONE_MINUTE, TimeUnit.MILLISECONDS)
@@ -359,4 +359,4 @@ class BackupWorker(ctx: Context, params: WorkerParameters) : ListenableWorker(ct
}
}
private const val TAG = "BackupWorker"
private const val TAG = "BackupWorker"

View File

@@ -1,21 +1,3 @@
buildscript {
ext.kotlin_version = '1.8.20'
ext.kotlin_coroutines_version = '1.7.1'
ext.work_version = '2.7.1'
ext.concurrent_version = '1.1.0'
ext.guava_version = '33.0.0-android'
ext.glide_version = '4.14.2'
repositories {
google()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:7.4.2'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
allprojects {
repositories {
google()
@@ -34,3 +16,7 @@ subprojects {
tasks.register("clean", Delete) {
delete rootProject.buildDir
}
tasks.named('wrapper') {
distributionType = Wrapper.DistributionType.ALL
}

View File

@@ -35,7 +35,7 @@ platform :android do
task: 'bundle',
build_type: 'Release',
properties: {
"android.injected.version.code" => 134,
"android.injected.version.code" => 136,
"android.injected.version.name" => "1.102.3",
}
)

View File

@@ -5,17 +5,17 @@
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000425">
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000261">
</testcase>
<testcase classname="fastlane.lanes" name="1: bundleRelease" time="63.658719">
<testcase classname="fastlane.lanes" name="1: bundleRelease" time="32.48099">
</testcase>
<testcase classname="fastlane.lanes" name="2: upload_to_play_store" time="36.312519">
<testcase classname="fastlane.lanes" name="2: upload_to_play_store" time="30.236974">
</testcase>

View File

@@ -1,6 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.3-all.zip
distributionSha256Sum=6001aba9b2204d26fa25a5800bb9382cf3ee01ccb78fe77317b2872336eb2f80
distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.4-all.zip
distributionSha256Sum=fe696c020f241a5f69c30f763c5a7f38eec54b490db19cd2b0962dda420d7d12

View File

@@ -1,11 +1,26 @@
include ':app'
pluginManagement {
def flutterSdkPath = {
def properties = new Properties()
file("local.properties").withInputStream { properties.load(it) }
def flutterSdkPath = properties.getProperty("flutter.sdk")
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
return flutterSdkPath
}()
def localPropertiesFile = new File(rootProject.projectDir, "local.properties")
def properties = new Properties()
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
assert localPropertiesFile.exists()
localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) }
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
def flutterSdkPath = properties.getProperty("flutter.sdk")
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle"
plugins {
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
id "com.android.application" version "7.4.2" apply false
id "org.jetbrains.kotlin.android" version "1.9.23" apply false
id "org.jetbrains.kotlin.kapt" version "1.9.23" apply false
}
include ":app"

View File

@@ -296,6 +296,7 @@
"motion_photos_page_title": "Motion Photos",
"multiselect_grid_edit_date_time_err_read_only": "Cannot edit date of read only asset(s), skipping",
"multiselect_grid_edit_gps_err_read_only": "Cannot edit location of read only asset(s), skipping",
"no_assets_to_show" : "No assets to show",
"notification_permission_dialog_cancel": "Cancel",
"notification_permission_dialog_content": "To enable notifications, go to Settings and select allow.",
"notification_permission_dialog_settings": "Settings",

View File

@@ -296,6 +296,7 @@
"motion_photos_page_title": "Photos avec mouvement",
"multiselect_grid_edit_date_time_err_read_only": "Impossible de modifier la date d'un élément d'actif en lecture seule.",
"multiselect_grid_edit_gps_err_read_only": "Impossible de modifier l'emplacement d'un élément en lecture seule.",
"no_assets_to_show" : "Aucun élément à afficher",
"notification_permission_dialog_cancel": "Annuler",
"notification_permission_dialog_content": "Pour activer les notifications, allez dans Paramètres et sélectionnez Autoriser.",
"notification_permission_dialog_settings": "Paramètres",
@@ -509,5 +510,7 @@
"version_announcement_overlay_title": "Nouvelle version serveur disponible \uD83C\uDF89",
"viewer_remove_from_stack": "Retirer de la pile",
"viewer_stack_use_as_main_asset": "Utiliser comme élément principal",
"viewer_unstack": "Désempiler"
"viewer_unstack": "Désempiler",
"haptic_feedback_title": "Retour haptique",
"haptic_feedback_switch": "Activer le retour haptique"
}

View File

@@ -63,7 +63,7 @@ class MultiselectGrid extends HookConsumerWidget {
const Center(child: ImmichLoadingIndicator());
Widget buildEmptyIndicator() =>
emptyIndicator ?? const Center(child: Text("No assets to show"));
emptyIndicator ?? Center(child: const Text("no_assets_to_show").tr());
@override
Widget build(BuildContext context, WidgetRef ref) {

View File

@@ -1804,5 +1804,5 @@ packages:
source: hosted
version: "3.1.2"
sdks:
dart: ">=3.2.0 <4.0.0"
dart: ">=3.3.0 <4.0.0"
flutter: ">=3.16.0"

View File

@@ -2,10 +2,10 @@ name: immich_mobile
description: Immich - selfhosted backup media file on mobile phone
publish_to: 'none'
version: 1.102.3+134
version: 1.102.3+136
environment:
sdk: '>=3.0.0 <4.0.0'
sdk: '>=3.3.0 <4.0.0'
dependencies:
flutter:

View File

@@ -27,7 +27,8 @@
"matchFileNames": ["mobile/**"],
"groupName": "mobile",
"matchUpdateTypes": ["minor", "patch"],
"schedule": "on tuesday"
"schedule": "on tuesday",
"addLabels": ["📱mobile"]
},
{
"groupName": "exiftool",

View File

@@ -2,10 +2,12 @@ import { SessionEntity } from 'src/entities/session.entity';
export const ISessionRepository = 'ISessionRepository';
type E = SessionEntity;
export interface ISessionRepository {
create(dto: Partial<SessionEntity>): Promise<SessionEntity>;
update(dto: Partial<SessionEntity>): Promise<SessionEntity>;
create<T extends Partial<E>>(dto: T): Promise<T>;
update<T extends Partial<E>>(dto: T): Promise<T>;
delete(id: string): Promise<void>;
getByToken(token: string): Promise<SessionEntity | null>;
getByUserId(userId: string): Promise<SessionEntity[]>;
getByToken(token: string): Promise<E | null>;
getByUserId(userId: string): Promise<E[]>;
}

View File

@@ -31,12 +31,12 @@ export class SessionRepository implements ISessionRepository {
});
}
create(session: Partial<SessionEntity>): Promise<SessionEntity> {
return this.repository.save(session);
create<T extends Partial<SessionEntity>>(dto: T): Promise<T & { id: string }> {
return this.repository.save(dto);
}
update(session: Partial<SessionEntity>): Promise<SessionEntity> {
return this.repository.save(session);
update<T extends Partial<SessionEntity>>(dto: T): Promise<T> {
return this.repository.save(dto);
}
@GenerateSql({ params: [DummyValue.UUID] })

View File

@@ -3,8 +3,8 @@ import { Mocked, vitest } from 'vitest';
export const newSessionRepositoryMock = (): Mocked<ISessionRepository> => {
return {
create: vitest.fn(),
update: vitest.fn(),
create: vitest.fn() as any,
update: vitest.fn() as any,
delete: vitest.fn(),
getByToken: vitest.fn(),
getByUserId: vitest.fn(),

18
web/package-lock.json generated
View File

@@ -12,6 +12,8 @@
"@immich/sdk": "file:../open-api/typescript-sdk",
"@mdi/js": "^7.4.47",
"@photo-sphere-viewer/core": "^5.7.1",
"@photo-sphere-viewer/equirectangular-video-adapter": "^5.7.2",
"@photo-sphere-viewer/video-plugin": "^5.7.2",
"@zoom-image/svelte": "^0.2.6",
"buffer": "^6.0.3",
"copy-image-clipboard": "^2.1.2",
@@ -1590,6 +1592,22 @@
"three": "^0.161.0"
}
},
"node_modules/@photo-sphere-viewer/equirectangular-video-adapter": {
"version": "5.7.2",
"resolved": "https://registry.npmjs.org/@photo-sphere-viewer/equirectangular-video-adapter/-/equirectangular-video-adapter-5.7.2.tgz",
"integrity": "sha512-cAaot52nPqa2p77Xp1humRvuxRIa8cqbZ/XRhA8kBToFLT1Ugh9YBcDD7pM/358JtAjicUbLpT7Ioap9iEigxQ==",
"peerDependencies": {
"@photo-sphere-viewer/core": "5.7.2"
}
},
"node_modules/@photo-sphere-viewer/video-plugin": {
"version": "5.7.2",
"resolved": "https://registry.npmjs.org/@photo-sphere-viewer/video-plugin/-/video-plugin-5.7.2.tgz",
"integrity": "sha512-vrPV9RCr4HsYiORkto1unDPeUkbN2kbyogvNUoLiQ78M4xkPOqoKxtfxCxTYoM+7gECwNL9VTF81+okck498qA==",
"peerDependencies": {
"@photo-sphere-viewer/core": "5.7.2"
}
},
"node_modules/@polka/url": {
"version": "1.0.0-next.24",
"resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.24.tgz",

View File

@@ -61,6 +61,8 @@
"@immich/sdk": "file:../open-api/typescript-sdk",
"@mdi/js": "^7.4.47",
"@photo-sphere-viewer/core": "^5.7.1",
"@photo-sphere-viewer/equirectangular-video-adapter": "^5.7.2",
"@photo-sphere-viewer/video-plugin": "^5.7.2",
"@zoom-image/svelte": "^0.2.6",
"buffer": "^6.0.3",
"copy-image-clipboard": "^2.1.2",

View File

@@ -50,7 +50,7 @@
import PanoramaViewer from './panorama-viewer.svelte';
import PhotoViewer from './photo-viewer.svelte';
import SlideshowBar from './slideshow-bar.svelte';
import VideoViewer from './video-viewer.svelte';
import VideoViewer from './video-wrapper-viewer.svelte';
export let assetStore: AssetStore | null = null;
export let asset: AssetResponseDto;
@@ -622,6 +622,7 @@
{:else}
<VideoViewer
assetId={previewStackedAsset.id}
projectionType={previewStackedAsset.exifInfo?.projectionType}
on:close={closeViewer}
on:onVideoEnded={() => navigateAsset()}
on:onVideoStarted={handleVideoStarted}
@@ -642,6 +643,7 @@
{#if shouldPlayMotionPhoto && asset.livePhotoVideoId}
<VideoViewer
assetId={asset.livePhotoVideoId}
projectionType={asset.exifInfo?.projectionType}
on:close={closeViewer}
on:onVideoEnded={() => (shouldPlayMotionPhoto = false)}
/>
@@ -655,6 +657,7 @@
{:else}
<VideoViewer
assetId={asset.id}
projectionType={asset.exifInfo?.projectionType}
on:close={closeViewer}
on:onVideoEnded={() => navigateAsset()}
on:onVideoStarted={handleVideoStarted}

View File

@@ -1,22 +1,39 @@
<script lang="ts">
import { serveFile, type AssetResponseDto } from '@immich/sdk';
import { serveFile, type AssetResponseDto, AssetTypeEnum } from '@immich/sdk';
import { fade } from 'svelte/transition';
import LoadingSpinner from '../shared-components/loading-spinner.svelte';
import { getKey } from '$lib/utils';
export let asset: AssetResponseDto;
import type { AdapterConstructor, PluginConstructor } from '@photo-sphere-viewer/core';
export let asset: Pick<AssetResponseDto, 'id' | 'type'>;
const photoSphereConfigs =
asset.type === AssetTypeEnum.Video
? ([
import('@photo-sphere-viewer/equirectangular-video-adapter').then(
({ EquirectangularVideoAdapter }) => EquirectangularVideoAdapter,
),
import('@photo-sphere-viewer/video-plugin').then(({ VideoPlugin }) => [VideoPlugin]),
true,
import('@photo-sphere-viewer/video-plugin/index.css'),
] as [PromiseLike<AdapterConstructor>, Promise<PluginConstructor[]>, true, unknown])
: ([undefined, [], false] as [undefined, [], false]);
const loadAssetData = async () => {
const data = await serveFile({ id: asset.id, isWeb: false, isThumb: false, key: getKey() });
return URL.createObjectURL(data);
const url = URL.createObjectURL(data);
if (asset.type === AssetTypeEnum.Video) {
return { source: url };
}
return url;
};
</script>
<div transition:fade={{ duration: 150 }} class="flex h-full select-none place-content-center place-items-center">
<!-- the photo sphere viewer is quite large, so lazy load it in parallel with loading the data -->
{#await Promise.all([loadAssetData(), import('./photo-sphere-viewer-adapter.svelte')])}
{#await Promise.all([loadAssetData(), import('./photo-sphere-viewer-adapter.svelte'), ...photoSphereConfigs])}
<LoadingSpinner />
{:then [data, module]}
<svelte:component this={module.default} panorama={data} />
{:then [data, module, adapter, plugins, navbar]}
<svelte:component this={module.default} panorama={data} plugins={plugins ?? undefined} {navbar} {adapter} />
{:catch}
Failed to load asset
{/await}

View File

@@ -1,17 +1,32 @@
<script lang="ts">
import { Viewer } from '@photo-sphere-viewer/core';
import {
Viewer,
EquirectangularAdapter,
type PluginConstructor,
type AdapterConstructor,
} from '@photo-sphere-viewer/core';
import '@photo-sphere-viewer/core/index.css';
import { onDestroy, onMount } from 'svelte';
export let panorama: string;
export let panorama: string | { source: string };
export let adapter: AdapterConstructor | [AdapterConstructor, unknown] = EquirectangularAdapter;
export let plugins: (PluginConstructor | [PluginConstructor, unknown])[] = [];
export let navbar = false;
let container: HTMLDivElement;
let viewer: Viewer;
onMount(() => {
viewer = new Viewer({
adapter,
plugins,
container,
panorama,
navbar: false,
touchmoveTwoFingers: true,
mousewheelCtrlKey: false,
navbar,
maxFov: 180,
fisheye: true,
});
});

View File

@@ -14,7 +14,7 @@
import LoadingSpinner from '../shared-components/loading-spinner.svelte';
import { NotificationType, notificationController } from '../shared-components/notification/notification';
import { getAltText } from '$lib/utils/thumbnail-util';
import { slideshowLookCssMapping, SlideshowState, slideshowStore } from '$lib/stores/slideshow.store';
import { SlideshowLook, slideshowLookCssMapping, SlideshowState, slideshowStore } from '$lib/stores/slideshow.store';
const { slideshowState, slideshowLook } = slideshowStore;
@@ -150,15 +150,24 @@
<div
bind:this={element}
transition:fade={{ duration: haveFadeTransition ? 150 : 0 }}
class="flex h-full select-none place-content-center place-items-center"
class="relative h-full select-none"
>
{#if !imageLoaded}
<LoadingSpinner />
<div class="flex h-full items-center justify-center">
<LoadingSpinner />
</div>
{:else}
<div bind:this={imgElement} class="h-full w-full">
<div bind:this={imgElement} class="h-full w-full" transition:fade={{ duration: haveFadeTransition ? 150 : 0 }}>
{#if $slideshowState !== SlideshowState.None && $slideshowLook === SlideshowLook.BlurredBackground}
<img
src={assetData}
alt={getAltText(asset)}
class="absolute top-0 left-0 -z-10 object-cover h-full w-full blur-lg"
draggable="false"
/>
{/if}
<img
bind:this={$photoViewer}
transition:fade={{ duration: haveFadeTransition ? 150 : 0 }}
src={assetData}
alt={getAltText(asset)}
class="h-full w-full {$slideshowState === SlideshowState.None

View File

@@ -0,0 +1,15 @@
<script lang="ts">
import { AssetTypeEnum } from '@immich/sdk';
import { ProjectionType } from '$lib/constants';
import VideoNativeViewer from '$lib/components/asset-viewer/video-native-viewer.svelte';
import PanoramaViewer from '$lib/components/asset-viewer/panorama-viewer.svelte';
export let assetId: string;
export let projectionType: string | null | undefined;
</script>
{#if projectionType === ProjectionType.EQUIRECTANGULAR}
<PanoramaViewer asset={{ id: assetId, type: AssetTypeEnum.Video }} />
{:else}
<VideoNativeViewer {assetId} on:onVideoEnded on:onVideoStarted />
{/if}

View File

@@ -43,7 +43,7 @@
'text-immich-primary dark:text-immich-dark-primary enabled:dark:hover:bg-immich-dark-primary/10 enabled:hover:bg-immich-primary/10',
'light-red': 'bg-[#F9DEDC] text-[#410E0B] enabled:hover:bg-red-50',
red: 'bg-red-500 text-white enabled:hover:bg-red-400',
green: 'bg-green-500 text-gray-800 enabled:hover:bg-green-400/90',
green: 'bg-green-600 text-white enabled:hover:bg-green-400/90',
gray: 'bg-gray-500 dark:bg-gray-200 enabled:hover:bg-gray-500/75 enabled:dark:hover:bg-gray-200/80 text-white dark:text-immich-dark-gray',
'transparent-gray':
'dark:text-immich-dark-fg enabled:hover:bg-immich-primary/5 enabled:hover:text-gray-700 enabled:hover:dark:text-immich-dark-fg enabled:dark:hover:bg-immich-dark-primary/25',

View File

@@ -83,6 +83,7 @@
hoverLabel = new Date(attr).toLocaleString($locale, {
month: 'short',
year: 'numeric',
timeZone: 'UTC',
});
};

View File

@@ -15,6 +15,7 @@
mdiMagnify,
mdiMap,
mdiTrashCanOutline,
mdiTuneVariant,
} from '@mdi/js';
import LoadingSpinner from '../loading-spinner.svelte';
import StatusBox from '../status-box.svelte';
@@ -145,6 +146,12 @@
</svelte:fragment>
</SideBarLink>
{/if}
<div class="text-xs transition-all duration-200 dark:text-immich-dark-fg">
<p class="hidden p-6 group-hover:sm:block md:block">AUTOMATION</p>
<hr class="mx-4 mb-[31px] mt-8 block group-hover:sm:hidden md:hidden" />
</div>
<SideBarLink title="Workflows" routeId="/(user)/workflows" icon={mdiTuneVariant} />
</nav>
<!-- Status Box -->

View File

@@ -4,7 +4,14 @@
SettingInputFieldType,
} from '$lib/components/shared-components/settings/setting-input-field.svelte';
import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte';
import { mdiArrowDownThin, mdiArrowUpThin, mdiFitToPageOutline, mdiFitToScreenOutline, mdiShuffle } from '@mdi/js';
import {
mdiArrowDownThin,
mdiArrowUpThin,
mdiFitToPageOutline,
mdiFitToScreenOutline,
mdiPanorama,
mdiShuffle,
} from '@mdi/js';
import { SlideshowLook, SlideshowNavigation, slideshowStore } from '../stores/slideshow.store';
import Button from './elements/buttons/button.svelte';
import type { RenderedOption } from './elements/dropdown.svelte';
@@ -23,6 +30,7 @@
const lookOptions: Record<SlideshowLook, RenderedOption> = {
[SlideshowLook.Contain]: { icon: mdiFitToScreenOutline, title: 'Contain' },
[SlideshowLook.Cover]: { icon: mdiFitToPageOutline, title: 'Cover' },
[SlideshowLook.BlurredBackground]: { icon: mdiPanorama, title: 'Blurred background' },
};
const handleToggle = <Type extends SlideshowNavigation | SlideshowLook>(

View File

@@ -0,0 +1,15 @@
<script lang="ts">
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
import { mdiPencil, mdiTrashCan } from '@mdi/js';
</script>
<div
class="bg-zinc-300 dark:bg-zinc-800 text-black dark:text-white min-h-[60px] grid grid-cols-[15%_70%_15%] place-items-center place-content-center rounded-2xl mx-4 mt-2 p-2"
>
<p class="col-start-2 col-span-1">Add to album "RANDOM"</p>
<div class="col-start-3 col-span-1 flex gap-2 justify-self-end">
<CircleIconButton size="20" padding="2" title="Remove rule" icon={mdiTrashCan} />
<CircleIconButton size="20" padding="2" title="Edit rule" icon={mdiPencil} />
</div>
</div>

View File

@@ -0,0 +1,15 @@
<script lang="ts">
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
import { mdiPencil, mdiTrashCan } from '@mdi/js';
</script>
<div
class="bg-zinc-200 text-black dark:bg-zinc-700 dark:text-white min-h-[60px] grid grid-cols-[15%_70%_15%] place-items-center place-content-center rounded-2xl mx-4 mt-2 p-2"
>
<p class="col-start-2 col-span-1">And has Alex and Henry and Nate</p>
<div class="col-start-3 col-span-1 flex gap-2 justify-self-end">
<CircleIconButton size="20" padding="2" title="Remove rule" icon={mdiTrashCan} />
<CircleIconButton size="20" padding="2" title="Edit rule" icon={mdiPencil} />
</div>
</div>

View File

@@ -0,0 +1,5 @@
<div
class="bg-immich-primary text-white dark:bg-immich-dark-primary dark:text-black min-h-[60px] flex place-items-center place-content-center mx-4 mt-2 rounded-2xl"
>
When an asset is uploaded
</div>

View File

@@ -0,0 +1,10 @@
<script lang="ts">
// export let onSelect: () => void;
</script>
<div
class="rounded-3xl mr-2 my-4 min-h-[60px] grid grid-cols-[32px_1fr] gap-2 place-items-center p-4 place-content-center bg-gray-100 hover:bg-gray-200 dark:bg-gray-900 hover:dark:bg-gray-800"
>
<div class="w-4 h-4 rounded-full bg-green-400"></div>
<p class="dark:text-gray-300">Add this photo to every albums and notify everybody about this glorious asset</p>
</div>

View File

@@ -0,0 +1,82 @@
<script lang="ts">
import Button from '$lib/components/elements/buttons/button.svelte';
import Icon from '$lib/components/elements/icon.svelte';
import WorkflowActionCard from '$lib/components/workflow-page/editor/workflow-action-card.svelte';
import WorkflowRuleCard from '$lib/components/workflow-page/editor/workflow-rule-card.svelte';
import WorkflowTriggerCard from '$lib/components/workflow-page/editor/workflow-trigger-card.svelte';
import { mdiPlus } from '@mdi/js';
</script>
<section class="h-full overflow-scroll">
<div
id="workflow-control-bar"
class="sticky top-0 flex justify-between place-items-center border-b border-gray-200 dark:border-gray-800 p-4 bg-zinc-50 dark:bg-zinc-900 z-20"
>
<p class="uppercase text-lg dark:text-white font-medium">
Add this photo to every albums and notify everybody about this glorious asset
</p>
<div class="flex gap-2">
<Button size="sm" color="red">Discard</Button>
<Button size="sm">Disable</Button>
<Button size="sm" color="green">Save</Button>
</div>
</div>
<div id="workflows-selection">
<!-- TRIGGER BLOCK -->
<div class="translate-y-3">
<p class="pl-4 text-xs dark:text-gray-300">TRIGGER</p>
<WorkflowTriggerCard />
</div>
<!-- VISUAL CONNECTOR -->
<div class="relative w-full grid grid-cols-3 place-items-center place-content-center z-10">
<p class="col-start-1 col-span-1 justify-self-start self-end pl-4 pb-6 text-xs dark:text-gray-300">RULES</p>
<div class="col-start-2 col-span-1 flex flex-col place-items-center">
<div
class="rounded-full border-[6px] border-immich-primary dark:border-immich-dark-primary h-[20px] w-[20px] bg-white translate-y-1"
></div>
<div
class="h-[60px] w-[5px] bg-white bg-gradient-to-b from-immich-primary dark:from-immich-dark-primary via-zinc-300 dark:via-gray-600 dark:to-zinc-700"
></div>
<div
class="rounded-full border-[6px] border-zinc-200 dark:border-gray-700 h-[20px] w-[20px] bg-white -translate-y-1"
></div>
</div>
</div>
<!-- RULES BLOCK -->
<div id="rule-block" class="-translate-y-6">
<WorkflowRuleCard />
<WorkflowRuleCard />
<div
class="border-2 dark:border-gray-700 dark:text-white min-h-[60px] flex place-items-center place-content-center rounded-2xl mx-4 mt-2"
>
<span><Icon path={mdiPlus} /></span> ADD RULE
</div>
</div>
<!-- VISUAL CONNECTOR -->
<div class="relative w-full grid grid-cols-3 place-items-center place-content-center -translate-y-9 z-10">
<p class="col-start-1 col-span-1 justify-self-start self-end pl-4 pb-6 text-xs dark:text-gray-300">ACTIONS</p>
<div class="col-start-2 col-span-1 flex flex-col place-items-center">
<div class="rounded-full border-[6px] border-gray-900 h-[20px] w-[20px] bg-white translate-y-1"></div>
<div class="h-[60px] w-[5px] bg-white bg-gradient-to-b from-gray-900 via-indigo-800 to-gray-700"></div>
<div class="rounded-full border-[6px] border-gray-700 h-[20px] w-[20px] bg-white -translate-y-1"></div>
</div>
</div>
<!-- ACTION BLOCK -->
<div id="action-block" class="-translate-y-14">
<WorkflowActionCard />
<WorkflowActionCard />
<div
class="border-2 dark:border-gray-500 dark:text-white min-h-[60px] flex place-items-center place-content-center rounded-2xl mx-4 mt-2"
>
<span><Icon path={mdiPlus} /></span> ADD ACTION
</div>
</div>
</div>
</section>

View File

@@ -0,0 +1,17 @@
<script lang="ts">
import Button from '$lib/components/elements/buttons/button.svelte';
import WorkflowCard from '$lib/components/workflow-page/workflow-card.svelte';
</script>
<section id="workflow-list" class="border-r border-gray-200 dark:border-gray-800 h-full relative overflow-scroll pr-2">
<div class="sticky top-0 dark:bg-immich-dark-bg flex justify-between place-items-center pr-2 py-4 bg-immich-bg">
<p class="text-xs dark:text-white">CURRENT WORKFLOWS</p>
<Button size="sm">New Workflow</Button>
</div>
<div>
{#each Array.from({ length: 50 }) as _}
<WorkflowCard />
{/each}
</div>
</section>

View File

@@ -16,11 +16,13 @@ export enum SlideshowNavigation {
export enum SlideshowLook {
Contain = 'contain',
Cover = 'cover',
BlurredBackground = 'blurred-background',
}
export const slideshowLookCssMapping: Record<SlideshowLook, string> = {
[SlideshowLook.Contain]: 'object-contain',
[SlideshowLook.Cover]: 'object-cover',
[SlideshowLook.BlurredBackground]: 'object-contain',
};
function createSlideshowStore() {

View File

@@ -39,8 +39,12 @@
try {
await emptyTrash();
const deletedAssetIds = assetStore.assets.map((a) => a.id);
const numberOfAssets = deletedAssetIds.length;
assetStore.removeAssets(deletedAssetIds);
notificationController.show({
message: `Empty trash initiated. Refresh the page to see the changes`,
message: `Permanently deleted ${numberOfAssets} ${numberOfAssets == 1 ? 'asset' : 'assets'}`,
type: NotificationType.Info,
});
} catch (error) {
@@ -52,8 +56,12 @@
try {
await restoreTrash();
const restoredAssetIds = assetStore.assets.map((a) => a.id);
const numberOfAssets = restoredAssetIds.length;
assetStore.removeAssets(restoredAssetIds);
notificationController.show({
message: `Restore trash initiated. Refresh the page to see the changes`,
message: `Restored ${numberOfAssets} ${numberOfAssets == 1 ? 'asset' : 'assets'}`,
type: NotificationType.Info,
});
} catch (error) {

View File

@@ -0,0 +1,15 @@
<script lang="ts">
import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte';
import WorkflowEditor from '$lib/components/workflow-page/workflow-editor.svelte';
import WorkflowList from '$lib/components/workflow-page/workflow-list.svelte';
import type { PageData } from './$types';
export let data: PageData;
</script>
<UserPageLayout title={data.meta.title} scrollbar={false}>
<section class="grid grid-cols-[25%_1fr] h-full">
<WorkflowList />
<WorkflowEditor />
</section>
</UserPageLayout>

View File

@@ -0,0 +1,12 @@
import { authenticate } from '$lib/utils/auth';
import type { PageLoad } from './$types';
export const load = (async () => {
await authenticate();
return {
meta: {
title: 'Workflows',
},
};
}) satisfies PageLoad;