Compare commits

..

10 Commits

Author SHA1 Message Date
Daniel Dietzler
d1e62c3736 refactor: web stores => managers (1) 2025-04-28 13:20:07 +02:00
Alex
205260d31c chore: post release tasks (#17895) 2025-04-27 23:02:03 -05:00
Alex
3858973be5 chore(mobile): translation (#17920) 2025-04-27 23:00:40 -05:00
github-actions
02994883fe chore: version v1.132.3 2025-04-25 19:44:05 +00:00
Alex
a1f8150c30 fix: Authelia OAuth code verifier value contains invalid characters (#17886)
* fix(mobile): Authelia OAuth code verifier value contains invalid characters

* Refactor

* Refactoring with Jason

* Refactoring with Jason
2025-04-25 19:39:14 +00:00
Yaros
d85ef19bfc fix(mobile): revert get location on app start (#17882) 2025-04-25 10:38:30 -05:00
Jason Rasmussen
d0014bdf94 refactor: event manager (#17862)
* refactor: event manager

* refactor: event manager
2025-04-25 08:36:31 -04:00
Martin Mikita
e822e3eca9 docs: update MapTiler name (#17863) 2025-04-25 08:57:44 +00:00
Alex
644defa4a1 chore: post release tasks (#17867) 2025-04-25 04:14:40 +00:00
Matthew Momjian
1fe3c7b9b3 fix(docs): priorities (Capitalization) (#17866)
priorities
2025-04-25 04:07:42 +00:00
63 changed files with 429 additions and 282 deletions

6
cli/package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "@immich/cli",
"version": "2.2.64",
"version": "2.2.65",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@immich/cli",
"version": "2.2.64",
"version": "2.2.65",
"license": "GNU Affero General Public License version 3",
"dependencies": {
"chokidar": "^4.0.3",
@@ -54,7 +54,7 @@
},
"../open-api/typescript-sdk": {
"name": "@immich/sdk",
"version": "1.132.2",
"version": "1.132.3",
"dev": true,
"license": "GNU Affero General Public License version 3",
"dependencies": {

View File

@@ -1,6 +1,6 @@
{
"name": "@immich/cli",
"version": "2.2.64",
"version": "2.2.65",
"description": "Command Line Interface (CLI) for Immich",
"type": "module",
"exports": "./dist/index.js",

View File

@@ -14,14 +14,14 @@ online generators you can use.
2. Paste the link to your JSON style in either the **Light Style** or **Dark Style**. (You can add different styles which will help make the map style more appropriate depending on whether you set **Immich** to Light or Dark mode.)
3. Save your selections. Reload the map, and enjoy your custom map style!
## Use Maptiler to build a custom style
## Use MapTiler to build a custom style
Customizing the map style can be done easily using Maptiler, if you do not want to write an entire JSON document by hand.
Customizing the map style can be done easily using MapTiler, if you do not want to write an entire JSON document by hand.
1. Create a free account at https://cloud.maptiler.com
2. Once logged in, you can either create a brand new map by clicking on **New Map**, selecting a starter map, and then clicking **Customize**, OR by selecting a **Standard Map** and customizing it from there.
3. The **editor** interface is self-explanatory. You can change colors, remove visible layers, or add optional layers (e.g., administrative, topo, hydro, etc.) in the composer.
4. Once you have your map composed, click on **Save** at the top right. Give it a unique name to save it to your account.
5. Next, **Publish** your style using the **Publish** button at the top right. This will deploy it to production, which means it is able to be exposed over the Internet. Maptiler will present an interactive side-by-side map with the original and your changes prior to publication.<br/>![Maptiler Publication Settings](img/immich_map_styles_publish.webp)
6. Maptiler will warn you that changing the map will change it across all apps using the map. Since no apps are using the map yet, this is okay.
7. Clicking on the name of your new map at the top left will bring you to the item's **details** page. From here, copy the link to the JSON style under **Use vector style**. This link will automatically contain your personal API key to Maptiler.
5. Next, **Publish** your style using the **Publish** button at the top right. This will deploy it to production, which means it is able to be exposed over the Internet. MapTiler will present an interactive side-by-side map with the original and your changes prior to publication.<br/>![MapTiler Publication Settings](img/immich_map_styles_publish.webp)
6. MapTiler will warn you that changing the map will change it across all apps using the map. Since no apps are using the map yet, this is okay.
7. Clicking on the name of your new map at the top left will bring you to the item's **details** page. From here, copy the link to the JSON style under **Use vector style**. This link will automatically contain your personal API key to MapTiler.

View File

@@ -1,7 +1,7 @@
# Database Queries
:::danger
Keep in mind that mucking around in the database might set the moon on fire. Avoid modifying the database directly when possible, and always have current backups.
Keep in mind that mucking around in the database might set the Moon on fire. Avoid modifying the database directly when possible, and always have current backups.
:::
:::tip

View File

@@ -1,4 +1,8 @@
[
{
"label": "v1.132.3",
"url": "https://v1.132.3.archive.immich.app"
},
{
"label": "v1.132.2",
"url": "https://v1.132.2.archive.immich.app"

8
e2e/package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "immich-e2e",
"version": "1.132.2",
"version": "1.132.3",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "immich-e2e",
"version": "1.132.2",
"version": "1.132.3",
"license": "GNU Affero General Public License version 3",
"devDependencies": {
"@eslint/eslintrc": "^3.1.0",
@@ -44,7 +44,7 @@
},
"../cli": {
"name": "@immich/cli",
"version": "2.2.64",
"version": "2.2.65",
"dev": true,
"license": "GNU Affero General Public License version 3",
"dependencies": {
@@ -93,7 +93,7 @@
},
"../open-api/typescript-sdk": {
"name": "@immich/sdk",
"version": "1.132.2",
"version": "1.132.3",
"dev": true,
"license": "GNU Affero General Public License version 3",
"dependencies": {

View File

@@ -1,6 +1,6 @@
{
"name": "immich-e2e",
"version": "1.132.2",
"version": "1.132.3",
"description": "",
"main": "index.js",
"type": "module",

View File

@@ -25,7 +25,7 @@ test.describe('Registration', () => {
// login
await expect(page).toHaveTitle(/Login/);
await page.goto('/auth/login');
await page.goto('/auth/login?autoLaunch=0');
await page.getByLabel('Email').fill('admin@immich.app');
await page.getByLabel('Password').fill('password');
await page.getByRole('button', { name: 'Login' }).click();
@@ -59,7 +59,7 @@ test.describe('Registration', () => {
await context.clearCookies();
// login
await page.goto('/auth/login');
await page.goto('/auth/login?autoLaunch=0');
await page.getByLabel('Email').fill('user@immich.cloud');
await page.getByLabel('Password').fill('password');
await page.getByRole('button', { name: 'Login' }).click();
@@ -72,7 +72,7 @@ test.describe('Registration', () => {
await page.getByRole('button', { name: 'Change password' }).click();
// login with new password
await expect(page).toHaveURL('/auth/login');
await expect(page).toHaveURL('/auth/login?autoLaunch=0');
await page.getByLabel('Email').fill('user@immich.cloud');
await page.getByLabel('Password').fill('new-password');
await page.getByRole('button', { name: 'Login' }).click();

View File

@@ -35,8 +35,8 @@ platform :android do
task: 'bundle',
build_type: 'Release',
properties: {
"android.injected.version.code" => 196,
"android.injected.version.name" => "1.132.2",
"android.injected.version.code" => 197,
"android.injected.version.name" => "1.132.3",
}
)
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

@@ -261,9 +261,11 @@
97C146ED1CF9000F007C117D = {
CreatedOnToolsVersion = 7.3.1;
LastSwiftMigration = 1100;
ProvisioningStyle = Automatic;
};
FAC6F88F2D287C890078CB2F = {
CreatedOnToolsVersion = 16.0;
ProvisioningStyle = Automatic;
};
};
};
@@ -541,7 +543,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/RunnerProfile.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 202;
CURRENT_PROJECT_VERSION = 205;
CUSTOM_GROUP_ID = group.app.immich.share;
DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_BITCODE = NO;
@@ -685,7 +687,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 202;
CURRENT_PROJECT_VERSION = 205;
CUSTOM_GROUP_ID = group.app.immich.share;
DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_BITCODE = NO;
@@ -715,7 +717,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 202;
CURRENT_PROJECT_VERSION = 205;
CUSTOM_GROUP_ID = group.app.immich.share;
DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_BITCODE = NO;
@@ -748,7 +750,7 @@
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 202;
CURRENT_PROJECT_VERSION = 205;
CUSTOM_GROUP_ID = group.app.immich.share;
DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
@@ -769,6 +771,7 @@
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = app.alextran.immich.vdebug.ShareExtension;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SKIP_INSTALL = YES;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
SWIFT_EMIT_LOC_STRINGS = YES;
@@ -791,7 +794,7 @@
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 202;
CURRENT_PROJECT_VERSION = 205;
CUSTOM_GROUP_ID = group.app.immich.share;
DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
@@ -811,6 +814,7 @@
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = app.alextran.immich.ShareExtension;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SKIP_INSTALL = YES;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
@@ -831,7 +835,7 @@
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 202;
CURRENT_PROJECT_VERSION = 205;
CUSTOM_GROUP_ID = group.app.immich.share;
DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
@@ -851,6 +855,7 @@
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = app.alextran.immich.profile.ShareExtension;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SKIP_INSTALL = YES;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;

View File

@@ -78,7 +78,7 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.132.0</string>
<string>1.132.3</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleURLTypes</key>
@@ -93,7 +93,7 @@
</dict>
</array>
<key>CFBundleVersion</key>
<string>202</string>
<string>205</string>
<key>FLTEnableImpeller</key>
<true/>
<key>ITSAppUsesNonExemptEncryption</key>

View File

@@ -18,8 +18,11 @@ default_platform(:ios)
platform :ios do
desc "iOS Release"
lane :release do
enable_automatic_code_signing(
path: "./Runner.xcodeproj",
)
increment_version_number(
version_number: "1.132.2"
version_number: "1.132.3"
)
increment_build_number(
build_number: latest_testflight_build_number + 1,

View File

@@ -1,7 +1,6 @@
import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:geolocator/geolocator.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/user.model.dart';
import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
@@ -13,7 +12,6 @@ import 'package:immich_mobile/providers/server_info.provider.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/services/api.service.dart';
import 'package:immich_mobile/utils/image_url_builder.dart';
import 'package:immich_mobile/utils/map_utils.dart';
import 'package:immich_mobile/widgets/album/album_thumbnail_card.dart';
import 'package:immich_mobile/widgets/common/immich_app_bar.dart';
import 'package:immich_mobile/widgets/common/user_avatar.dart';
@@ -357,66 +355,51 @@ class PlacesCollectionCard extends StatelessWidget {
final widthFactor = isTablet ? 0.25 : 0.5;
final size = context.width * widthFactor - 20.0;
return FutureBuilder<(Position?, LocationPermission?)>(
future: MapUtils.checkPermAndGetLocation(
context: context,
silent: true,
return GestureDetector(
onTap: () => context.pushRoute(
PlacesCollectionRoute(
currentLocation: null,
),
),
builder: (context, snapshot) {
var position = snapshot.data?.$1;
return GestureDetector(
onTap: () => context.pushRoute(
PlacesCollectionRoute(
currentLocation: position != null
? LatLng(position.latitude, position.longitude)
: null,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
height: size,
width: size,
child: DecoratedBox(
decoration: BoxDecoration(
borderRadius: const BorderRadius.all(Radius.circular(20)),
color:
context.colorScheme.secondaryContainer.withAlpha(100),
),
child: IgnorePointer(
child: MapThumbnail(
zoom: 8,
centre: const LatLng(
21.44950,
-157.91959,
),
showAttribution: false,
themeMode: context.isDarkTheme
? ThemeMode.dark
: ThemeMode.light,
),
),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
height: size,
width: size,
child: DecoratedBox(
decoration: BoxDecoration(
borderRadius:
const BorderRadius.all(Radius.circular(20)),
color: context.colorScheme.secondaryContainer
.withAlpha(100),
),
child: IgnorePointer(
child: snapshot.connectionState ==
ConnectionState.waiting
? const Center(child: CircularProgressIndicator())
: MapThumbnail(
zoom: 8,
centre: LatLng(
position?.latitude ?? 21.44950,
position?.longitude ?? -157.91959,
),
showAttribution: false,
themeMode: context.isDarkTheme
? ThemeMode.dark
: ThemeMode.light,
),
),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
'places'.tr(),
style: context.textTheme.titleSmall?.copyWith(
color: context.colorScheme.onSurface,
fontWeight: FontWeight.w500,
),
Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
'places'.tr(),
style: context.textTheme.titleSmall?.copyWith(
color: context.colorScheme.onSurface,
fontWeight: FontWeight.w500,
),
),
),
],
),
),
);
},
],
),
);
},
);

View File

@@ -44,7 +44,7 @@ class PermissionOnboardingPage extends HookConsumerWidget {
}
}),
child: const Text(
'grant_permission',
'continue',
).tr(),
),
],

View File

@@ -207,9 +207,27 @@ class LoginForm extends HookConsumerWidget {
}
String generateRandomString(int length) {
const chars =
'AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz1234567890';
final random = Random.secure();
return base64Url
.encode(List<int>.generate(32, (i) => random.nextInt(256)));
return String.fromCharCodes(
Iterable.generate(
length,
(_) => chars.codeUnitAt(random.nextInt(chars.length)),
),
);
}
List<int> randomBytes(int length) {
final random = Random.secure();
return List<int>.generate(length, (i) => random.nextInt(256));
}
/// Per specification, the code verifier must be 43-128 characters long
/// and consist of characters [A-Z, a-z, 0-9, "-", ".", "_", "~"]
/// https://datatracker.ietf.org/doc/html/rfc7636#section-4.1
String randomCodeVerifier() {
return base64Url.encode(randomBytes(42));
}
Future<String> generatePKCECodeChallenge(String codeVerifier) async {
@@ -223,7 +241,8 @@ class LoginForm extends HookConsumerWidget {
String? oAuthServerUrl;
final state = generateRandomString(32);
final codeVerifier = generateRandomString(64);
final codeVerifier = randomCodeVerifier();
final codeChallenge = await generatePKCECodeChallenge(codeVerifier);
try {

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.132.2
- API version: 1.132.3
- Generator version: 7.8.0
- Build package: org.openapitools.codegen.languages.DartClientCodegen

View File

@@ -2,7 +2,7 @@ name: immich_mobile
description: Immich - selfhosted backup media file on mobile phone
publish_to: 'none'
version: 1.132.2+196
version: 1.132.3+197
environment:
sdk: '>=3.3.0 <4.0.0'

View File

@@ -7656,7 +7656,7 @@
"info": {
"title": "Immich",
"description": "Immich API",
"version": "1.132.2",
"version": "1.132.3",
"contact": {}
},
"tags": [],

View File

@@ -1,12 +1,12 @@
{
"name": "@immich/sdk",
"version": "1.132.2",
"version": "1.132.3",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@immich/sdk",
"version": "1.132.2",
"version": "1.132.3",
"license": "GNU Affero General Public License version 3",
"dependencies": {
"@oazapfts/runtime": "^1.0.2"

View File

@@ -1,6 +1,6 @@
{
"name": "@immich/sdk",
"version": "1.132.2",
"version": "1.132.3",
"description": "Auto-generated TypeScript SDK for the Immich API",
"type": "module",
"main": "./build/index.js",

View File

@@ -1,6 +1,6 @@
/**
* Immich
* 1.132.2
* 1.132.3
* DO NOT MODIFY - This file has been generated using oazapfts.
* See https://www.npmjs.com/package/oazapfts
*/

View File

@@ -1,12 +1,12 @@
{
"name": "immich",
"version": "1.132.2",
"version": "1.132.3",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "immich",
"version": "1.132.2",
"version": "1.132.3",
"hasInstallScript": true,
"license": "GNU Affero General Public License version 3",
"dependencies": {

View File

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

6
web/package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "immich-web",
"version": "1.132.2",
"version": "1.132.3",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "immich-web",
"version": "1.132.2",
"version": "1.132.3",
"license": "GNU Affero General Public License version 3",
"dependencies": {
"@formatjs/icu-messageformat-parser": "^2.9.8",
@@ -82,7 +82,7 @@
},
"../open-api/typescript-sdk": {
"name": "@immich/sdk",
"version": "1.132.2",
"version": "1.132.3",
"license": "GNU Affero General Public License version 3",
"dependencies": {
"@oazapfts/runtime": "^1.0.2"

View File

@@ -1,6 +1,6 @@
{
"name": "immich-web",
"version": "1.132.2",
"version": "1.132.3",
"license": "GNU Affero General Public License version 3",
"type": "module",
"scripts": {

View File

@@ -1,7 +1,6 @@
<script lang="ts">
import SelectAllAssets from '$lib/components/photos-page/actions/select-all-assets.svelte';
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
import { dragAndDropFilesStore } from '$lib/stores/drag-and-drop-files.store';
import { fileUploadHandler, openFileUploadDialog } from '$lib/utils/file-uploader';
import type { AlbumResponseDto, SharedLinkResponseDto, UserResponseDto } from '@immich/sdk';
import { AssetStore } from '$lib/stores/assets-store.svelte';
@@ -20,6 +19,7 @@
import { t } from 'svelte-i18n';
import { onDestroy } from 'svelte';
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
import { dragAndDropManager } from '$lib/managers/drag-and-drop.manager.svelte';
interface Props {
sharedLink: SharedLinkResponseDto;
@@ -38,10 +38,10 @@
const assetInteraction = new AssetInteraction();
dragAndDropFilesStore.subscribe((value) => {
if (value.isDragging && value.files.length > 0) {
handlePromiseError(fileUploadHandler(value.files, album.id));
dragAndDropFilesStore.set({ isDragging: false, files: [] });
$effect(() => {
if (dragAndDropManager.isDragging && dragAndDropManager.files.length > 0) {
handlePromiseError(fileUploadHandler(dragAndDropManager.files, album.id));
dragAndDropManager.reset();
}
});
</script>

View File

@@ -5,7 +5,6 @@
import NextAssetAction from '$lib/components/asset-viewer/actions/next-asset-action.svelte';
import PreviousAssetAction from '$lib/components/asset-viewer/actions/previous-asset-action.svelte';
import { AssetAction, ProjectionType } from '$lib/constants';
import { updateNumberOfComments } from '$lib/stores/activity.store';
import { closeEditorCofirm } from '$lib/stores/asset-editor.store';
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
import { isShowDetail } from '$lib/stores/preferences.store';
@@ -47,6 +46,7 @@
import PhotoViewer from './photo-viewer.svelte';
import SlideshowBar from './slideshow-bar.svelte';
import VideoViewer from './video-wrapper-viewer.svelte';
import { activityManager } from '$lib/managers/activity.manager.svelte';
type HasAsset = boolean;
@@ -137,12 +137,12 @@
const handleAddComment = () => {
numberOfComments++;
updateNumberOfComments(1);
activityManager.updateNumberOfComments(1);
};
const handleRemoveComment = () => {
numberOfComments--;
updateNumberOfComments(-1);
activityManager.updateNumberOfComments(-1);
};
const handleFavorite = async () => {

View File

@@ -46,7 +46,7 @@
import AlbumListItemDetails from './album-list-item-details.svelte';
import Portal from '$lib/components/shared-components/portal/portal.svelte';
import { getMetadataSearchQuery } from '$lib/utils/metadata-search';
import { isFaceEditMode } from '$lib/stores/face-edit.svelte';
import { faceManager } from '$lib/managers/face.manager.svelte';
interface Props {
asset: AssetResponseDto;
@@ -207,7 +207,7 @@
padding="1"
size="20"
buttonSize="32"
onclick={() => (isFaceEditMode.value = !isFaceEditMode.value)}
onclick={() => (faceManager.isEditMode = !faceManager.isEditMode)}
/>
{#if people.length > 0 || unassignedFaces.length > 0}

View File

@@ -1,5 +1,5 @@
<script lang="ts">
import { type DownloadProgress, downloadManager, downloadStore } from '$lib/stores/download-store.svelte';
import { downloadManager, type DownloadProgress } from '$lib/managers/download.manager.svelte';
import { locale } from '$lib/stores/preferences.store';
import { fly, slide } from 'svelte/transition';
import { getByteUnitString } from '../../utils/byte-units';
@@ -13,15 +13,15 @@
};
</script>
{#if downloadStore.isDownloading}
{#if downloadManager.isDownloading}
<div
transition:fly={{ x: -100, duration: 350 }}
class="fixed bottom-10 left-2 z-[10000] max-h-[270px] w-[315px] rounded-2xl border bg-immich-bg p-4 text-sm shadow-sm"
>
<p class="mb-2 text-xs text-gray-500">{$t('downloading').toUpperCase()}</p>
<div class="my-2 mb-2 flex max-h-[200px] flex-col overflow-y-auto text-sm">
{#each Object.keys(downloadStore.assets) as downloadKey (downloadKey)}
{@const download = downloadStore.assets[downloadKey]}
{#each Object.keys(downloadManager.assets) as downloadKey (downloadKey)}
{@const download = downloadManager.assets[downloadKey]}
<div class="mb-2 flex place-items-center" transition:slide>
<div class="w-full pr-10">
<div class="flex place-items-center justify-between gap-2 text-xs font-medium">

View File

@@ -2,7 +2,6 @@
import ImageThumbnail from '$lib/components/assets/thumbnail/image-thumbnail.svelte';
import { dialogController } from '$lib/components/shared-components/dialog/dialog';
import { notificationController } from '$lib/components/shared-components/notification/notification';
import { isFaceEditMode } from '$lib/stores/face-edit.svelte';
import { getPeopleThumbnailUrl } from '$lib/utils';
import { getAllPeople, createFace, type PersonResponseDto } from '@immich/sdk';
import { Button, Input } from '@immich/ui';
@@ -10,6 +9,7 @@
import { onMount } from 'svelte';
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
import { handleError } from '$lib/utils/handle-error';
import { faceManager } from '$lib/managers/face.manager.svelte';
interface Props {
htmlElement: HTMLImageElement | HTMLVideoElement;
@@ -140,7 +140,7 @@
};
const cancel = () => {
isFaceEditMode.value = false;
faceManager.isEditMode = false;
};
const getPeople = async () => {
@@ -303,7 +303,7 @@
} catch (error) {
handleError(error, 'Error tagging face');
} finally {
isFaceEditMode.value = false;
faceManager.isEditMode = false;
}
};
</script>

View File

@@ -20,7 +20,7 @@
import { handleError } from '$lib/utils/handle-error';
import FaceEditor from '$lib/components/asset-viewer/face-editor/face-editor.svelte';
import { photoViewerImgElement } from '$lib/stores/assets-store.svelte';
import { isFaceEditMode } from '$lib/stores/face-edit.svelte';
import { faceManager } from '$lib/managers/face.manager.svelte';
interface Props {
asset: AssetResponseDto;
@@ -109,7 +109,7 @@
};
$effect(() => {
if (isFaceEditMode.value && $photoZoomState.currentZoom > 1) {
if (faceManager.isEditMode && $photoZoomState.currentZoom > 1) {
zoomToggle();
}
});
@@ -235,7 +235,7 @@
{/each}
</div>
{#if isFaceEditMode.value}
{#if faceManager.isEditMode}
<FaceEditor htmlElement={$photoViewerImgElement} {containerWidth} {containerHeight} assetId={asset.id} />
{/if}
{/if}

View File

@@ -9,8 +9,8 @@
import type { SwipeCustomEvent } from 'svelte-gestures';
import { fade } from 'svelte/transition';
import { t } from 'svelte-i18n';
import { isFaceEditMode } from '$lib/stores/face-edit.svelte';
import FaceEditor from '$lib/components/asset-viewer/face-editor/face-editor.svelte';
import { faceManager } from '$lib/managers/face.manager.svelte';
interface Props {
assetId: string;
@@ -94,7 +94,7 @@
let containerHeight = $state(0);
$effect(() => {
if (isFaceEditMode.value) {
if (faceManager.isEditMode) {
videoPlayer?.pause();
}
});
@@ -141,7 +141,7 @@
</div>
{/if}
{#if isFaceEditMode.value}
{#if faceManager.isEditMode}
<FaceEditor htmlElement={videoPlayer} {containerWidth} {containerHeight} {assetId} />
{/if}
</div>

View File

@@ -2,7 +2,6 @@
import { goto } from '$app/navigation';
import type { Action } from '$lib/components/asset-viewer/actions/action';
import { AppRoute, AssetAction } from '$lib/constants';
import { dragAndDropFilesStore } from '$lib/stores/drag-and-drop-files.store';
import { getKey, handlePromiseError } from '$lib/utils';
import { downloadArchive } from '$lib/utils/asset-utils';
import { fileUploadHandler, openFileUploadDialog } from '$lib/utils/file-uploader';
@@ -22,6 +21,7 @@
import type { Viewport } from '$lib/stores/assets-store.svelte';
import { t } from 'svelte-i18n';
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
import { dragAndDropManager } from '$lib/managers/drag-and-drop.manager.svelte';
interface Props {
sharedLink: SharedLinkResponseDto;
@@ -35,10 +35,10 @@
let assets = $derived(sharedLink.assets);
dragAndDropFilesStore.subscribe((value) => {
if (value.isDragging && value.files.length > 0) {
handlePromiseError(handleUploadAssets(value.files));
dragAndDropFilesStore.set({ isDragging: false, files: [] });
$effect(() => {
if (dragAndDropManager.isDragging && dragAndDropManager.files.length > 0) {
handlePromiseError(handleUploadAssets(dragAndDropManager.files));
dragAndDropManager.reset();
}
});

View File

@@ -6,7 +6,7 @@
type Padding,
} from '$lib/components/elements/buttons/circle-icon-button.svelte';
import ContextMenu from '$lib/components/shared-components/context-menu/context-menu.svelte';
import { optionClickCallbackStore, selectedIdStore } from '$lib/stores/context-menu.store';
import { contextMenuManager } from '$lib/managers/context-menu.manager.svelte';
import {
getContextMenuPositionFromBoundingRect,
getContextMenuPositionFromEvent,
@@ -97,7 +97,7 @@
}
focusButton();
isOpen = false;
$selectedIdStore = undefined;
contextMenuManager.selectedId = undefined;
};
const handleOptionClick = () => {
@@ -124,7 +124,7 @@
$effect(() => {
if (isOpen) {
$optionClickCallbackStore = handleOptionClick;
contextMenuManager.optionClickCallback = handleOptionClick;
}
});
</script>
@@ -139,8 +139,8 @@
isOpen,
onEscape,
openDropdown,
selectedId: $selectedIdStore,
selectionChanged: (id) => ($selectedIdStore = id),
selectedId: contextMenuManager.selectedId,
selectionChanged: (id) => (contextMenuManager.selectedId = id),
}}
onresize={onResize}
{...restProps}
@@ -178,7 +178,7 @@
<ContextMenu
{...contextMenuPosition}
{direction}
ariaActiveDescendant={$selectedIdStore}
ariaActiveDescendant={contextMenuManager.selectedId}
ariaLabelledBy={buttonId}
bind:menuElement={menuContainer}
id={menuId}

View File

@@ -1,9 +1,9 @@
<script lang="ts">
import Icon from '$lib/components/elements/icon.svelte';
import { generateId } from '$lib/utils/generate-id';
import { optionClickCallbackStore, selectedIdStore } from '$lib/stores/context-menu.store';
import type { Shortcut } from '$lib/actions/shortcut';
import { shortcutLabel as computeShortcutLabel, shortcut as bindShortcut } from '$lib/actions/shortcut';
import { contextMenuManager } from '$lib/managers/context-menu.manager.svelte';
interface Props {
text: string;
@@ -29,10 +29,10 @@
let id: string = generateId();
let isActive = $derived($selectedIdStore === id);
let isActive = $derived(contextMenuManager.selectedId === id);
const handleClick = () => {
$optionClickCallbackStore?.();
contextMenuManager.optionClickCallback?.();
onClick();
};
@@ -51,8 +51,8 @@
<li
{id}
onclick={handleClick}
onmouseover={() => ($selectedIdStore = id)}
onmouseleave={() => ($selectedIdStore = undefined)}
onmouseover={() => (contextMenuManager.selectedId = id)}
onmouseleave={() => (contextMenuManager.selectedId = undefined)}
class="w-full p-4 text-left text-sm font-medium {textColor} focus:outline-none focus:ring-2 focus:ring-inset cursor-pointer border-gray-200 flex gap-2 items-center {isActive
? activeColor
: 'bg-slate-100'}"

View File

@@ -4,7 +4,7 @@
import { shortcuts } from '$lib/actions/shortcut';
import { generateId } from '$lib/utils/generate-id';
import { contextMenuNavigation } from '$lib/actions/context-menu-navigation';
import { optionClickCallbackStore, selectedIdStore } from '$lib/stores/context-menu.store';
import { contextMenuManager } from '$lib/managers/context-menu.manager.svelte';
interface Props {
title: string;
@@ -60,7 +60,7 @@
if (isOpen && menuContainer) {
triggerElement = document.activeElement as HTMLElement;
menuContainer.focus();
$optionClickCallbackStore = closeContextMenu;
contextMenuManager.optionClickCallback = closeContextMenu;
}
});
@@ -77,8 +77,8 @@
closeDropdown: closeContextMenu,
container: menuContainer,
isOpen,
selectedId: $selectedIdStore,
selectionChanged: (id) => ($selectedIdStore = id),
selectedId: contextMenuManager.selectedId,
selectionChanged: (id) => (contextMenuManager.selectedId = id),
}}
use:shortcuts={[
{
@@ -96,7 +96,7 @@
{direction}
{x}
{y}
ariaActiveDescendant={$selectedIdStore}
ariaActiveDescendant={contextMenuManager.selectedId}
ariaLabel={title}
bind:menuElement={menuContainer}
id={menuId}

View File

@@ -1,7 +1,7 @@
<script lang="ts">
import { page } from '$app/state';
import { shouldIgnoreEvent } from '$lib/actions/shortcut';
import { dragAndDropFilesStore } from '$lib/stores/drag-and-drop-files.store';
import { dragAndDropManager } from '$lib/managers/drag-and-drop.manager.svelte';
import { fileUploadHandler } from '$lib/utils/file-uploader';
import { isAlbumsRoute, isSharedLinkRoute } from '$lib/utils/navigation';
import { t } from 'svelte-i18n';
@@ -124,7 +124,8 @@
const filesArray: File[] = Array.from<File>(files);
if (isShare) {
dragAndDropFilesStore.set({ isDragging: true, files: filesArray });
dragAndDropManager.isDragging = true;
dragAndDropManager.files = filesArray;
} else {
await fileUploadHandler(filesArray, albumId);
}

View File

@@ -10,11 +10,13 @@
import ImmichLogo from '$lib/components/shared-components/immich-logo.svelte';
import SearchBar from '$lib/components/shared-components/search-bar/search-bar.svelte';
import { AppRoute } from '$lib/constants';
import { authManager } from '$lib/managers/auth-manager.svelte';
import { mobileDevice } from '$lib/stores/mobile-device.svelte';
import { featureFlags } from '$lib/stores/server-config.store';
import { sidebarStore } from '$lib/stores/sidebar.svelte';
import { user } from '$lib/stores/user.store';
import { userInteraction } from '$lib/stores/user.svelte';
import { handleLogout } from '$lib/utils/auth';
import { getAboutInfo, logout, type ServerAboutResponseDto } from '@immich/sdk';
import { getAboutInfo, type ServerAboutResponseDto } from '@immich/sdk';
import { Button, IconButton } from '@immich/ui';
import { mdiHelpCircleOutline, mdiMagnify, mdiMenu, mdiTrayArrowUp } from '@mdi/js';
import { onMount } from 'svelte';
@@ -23,8 +25,6 @@
import ThemeButton from '../theme-button.svelte';
import UserAvatar from '../user-avatar.svelte';
import AccountInfoPanel from './account-info-panel.svelte';
import { sidebarStore } from '$lib/stores/sidebar.svelte';
import { mobileDevice } from '$lib/stores/mobile-device.svelte';
interface Props {
showUploadButton?: boolean;
@@ -38,11 +38,6 @@
let shouldShowHelpPanel = $state(false);
let innerWidth: number = $state(0);
const onLogout = async () => {
const { redirectUri } = await logout();
await handleLogout(redirectUri);
};
let info: ServerAboutResponseDto | undefined = $state();
onMount(async () => {
@@ -183,7 +178,7 @@
{/if}
{#if shouldShowAccountInfoPanel}
<AccountInfoPanel {onLogout} />
<AccountInfoPanel onLogout={() => authManager.logout()} />
{/if}
</div>
</section>

View File

@@ -0,0 +1,17 @@
class ActivityManager {
#numberOfComments = $state<number>(0);
get numberOfComments() {
return this.#numberOfComments;
}
set numberOfComments(number: number) {
this.#numberOfComments = number;
}
updateNumberOfComments(addOrRemove: 1 | -1) {
this.#numberOfComments += addOrRemove;
}
}
export const activityManager = new ActivityManager();

View File

@@ -0,0 +1,33 @@
import { goto } from '$app/navigation';
import { AppRoute } from '$lib/constants';
import { eventManager } from '$lib/managers/event.manager.svelte';
import { logout } from '@immich/sdk';
class AuthManager {
async logout() {
let redirectUri;
try {
const response = await logout();
if (response.redirectUri) {
redirectUri = response.redirectUri;
}
} catch (error) {
console.log('Error logging out:', error);
}
redirectUri = redirectUri ?? AppRoute.AUTH_LOGIN;
try {
if (redirectUri.startsWith('/')) {
await goto(redirectUri);
} else {
globalThis.location.href = redirectUri;
}
} finally {
eventManager.emit('auth.logout');
}
}
}
export const authManager = new AuthManager();

View File

@@ -0,0 +1,22 @@
class ContextMenuManager {
#selectedId = $state<string | undefined>(undefined);
#optionClickCallback = $state<(() => void) | undefined>(undefined);
get selectedId() {
return this.#selectedId;
}
set selectedId(id: string | undefined) {
this.#selectedId = id;
}
get optionClickCallback() {
return this.#optionClickCallback;
}
set optionClickCallback(callback: (() => void) | undefined) {
this.#optionClickCallback = callback;
}
}
export const contextMenuManager = new ContextMenuManager();

View File

@@ -0,0 +1,47 @@
export interface DownloadProgress {
progress: number;
total: number;
percentage: number;
abort: AbortController | null;
}
class DownloadManager {
#assets = $state<Record<string, DownloadProgress>>({});
#isDownloading = $derived(Object.keys(this.#assets).length > 0);
#update(key: string, value: Partial<DownloadProgress>) {
if (!this.#assets[key]) {
this.#assets[key] = { progress: 0, total: 0, percentage: 0, abort: null };
}
const item = this.#assets[key];
Object.assign(item, value);
item.percentage = Math.min(Math.floor((item.progress / item.total) * 100), 100);
}
get assets() {
return this.#assets;
}
get isDownloading() {
return this.#isDownloading;
}
add(key: string, total: number, abort?: AbortController) {
this.#update(key, { total, abort });
}
clear(key: string) {
delete this.#assets[key];
}
update(key: string, progress: number, total?: number) {
const download: Partial<DownloadProgress> = { progress };
if (total !== undefined) {
download.total = total;
}
this.#update(key, download);
}
}
export const downloadManager = new DownloadManager();

View File

@@ -0,0 +1,27 @@
class DragAndDropManager {
#isDragging = $state<boolean>(false);
#files = $state<File[]>([]);
get isDragging() {
return this.#isDragging;
}
get files() {
return this.#files;
}
set isDragging(isDragging: boolean) {
this.#isDragging = isDragging;
}
set files(files: File[]) {
this.#files = files;
}
reset() {
this.#isDragging = false;
this.#files = [];
}
}
export const dragAndDropManager = new DragAndDropManager();

View File

@@ -0,0 +1,54 @@
type Listener<EventMap extends Record<string, unknown[]>, K extends keyof EventMap> = (...params: EventMap[K]) => void;
class EventManager<EventMap extends Record<string, unknown[]>> {
private listeners: {
[K in keyof EventMap]?: {
listener: Listener<EventMap, K>;
once?: boolean;
}[];
} = {};
on<T extends keyof EventMap>(key: T, listener: (...params: EventMap[T]) => void) {
return this.addListener(key, listener, false);
}
once<T extends keyof EventMap>(key: T, listener: (...params: EventMap[T]) => void) {
return this.addListener(key, listener, true);
}
off<K extends keyof EventMap>(key: K, listener: Listener<EventMap, K>) {
if (this.listeners[key]) {
this.listeners[key] = this.listeners[key].filter((item) => item.listener !== listener);
}
return this;
}
emit<T extends keyof EventMap>(key: T, ...params: EventMap[T]) {
if (!this.listeners[key]) {
return;
}
for (const { listener } of this.listeners[key]) {
listener(...params);
}
// remove one time listeners
this.listeners[key] = this.listeners[key].filter((item) => !item.once);
}
private addListener<T extends keyof EventMap>(key: T, listener: (...params: EventMap[T]) => void, once: boolean) {
if (!this.listeners[key]) {
this.listeners[key] = [];
}
this.listeners[key].push({ listener, once });
return this;
}
}
export const eventManager = new EventManager<{
'user.login': [];
'auth.logout': [];
}>();

View File

@@ -0,0 +1,13 @@
class FaceManager {
#isEditMode = $state(false);
get isEditMode() {
return this.#isEditMode;
}
set isEditMode(isEditMode: boolean) {
this.#isEditMode = isEditMode;
}
}
export const faceManager = new FaceManager();

View File

@@ -1,11 +0,0 @@
import { writable } from 'svelte/store';
export const numberOfComments = writable<number>(0);
export const setNumberOfComments = (number: number) => {
numberOfComments.set(number);
};
export const updateNumberOfComments = (addOrRemove: 1 | -1) => {
numberOfComments.update((n) => n + addOrRemove);
};

View File

@@ -1,6 +0,0 @@
import { writable } from 'svelte/store';
const selectedIdStore = writable<string | undefined>(undefined);
const optionClickCallbackStore = writable<(() => void) | undefined>(undefined);
export { optionClickCallbackStore, selectedIdStore };

View File

@@ -1,51 +0,0 @@
export interface DownloadProgress {
progress: number;
total: number;
percentage: number;
abort: AbortController | null;
}
class DownloadStore {
assets = $state<Record<string, DownloadProgress>>({});
isDownloading = $derived(Object.keys(this.assets).length > 0);
#update(key: string, value: Partial<DownloadProgress> | null) {
if (value === null) {
delete this.assets[key];
return;
}
if (!this.assets[key]) {
this.assets[key] = { progress: 0, total: 0, percentage: 0, abort: null };
}
const item = this.assets[key];
Object.assign(item, value);
item.percentage = Math.min(Math.floor((item.progress / item.total) * 100), 100);
}
add(key: string, total: number, abort?: AbortController) {
this.#update(key, { total, abort });
}
clear(key: string) {
this.#update(key, null);
}
update(key: string, progress: number, total?: number) {
const download: Partial<DownloadProgress> = { progress };
if (total !== undefined) {
download.total = total;
}
this.#update(key, download);
}
}
export const downloadStore = new DownloadStore();
export const downloadManager = {
add: (key: string, total: number, abort?: AbortController) => downloadStore.add(key, total, abort),
clear: (key: string) => downloadStore.clear(key),
update: (key: string, progress: number, total?: number) => downloadStore.update(key, progress, total),
};

View File

@@ -1,7 +0,0 @@
//store to track the state of the drag and drop and the files
import { writable } from 'svelte/store';
export const dragAndDropFilesStore = writable({
isDragging: false as boolean,
files: [] as File[],
});

View File

@@ -1 +0,0 @@
export const isFaceEditMode = $state({ value: false });

View File

@@ -1,3 +1,4 @@
import { eventManager } from '$lib/managers/event.manager.svelte';
import {
getAssetsByOriginalPath,
getUniqueOriginalPaths,
@@ -16,6 +17,10 @@ class FoldersStore {
uniquePaths = $state<string[]>([]);
assets = $state<AssetCache>({});
constructor() {
eventManager.on('auth.logout', () => this.clearCache());
}
async fetchUniquePaths() {
if (this.initialized) {
return;

View File

@@ -1,3 +1,4 @@
import { eventManager } from '$lib/managers/event.manager.svelte';
import { asLocalTimeISO } from '$lib/utils/date-time';
import {
type AssetResponseDto,
@@ -24,6 +25,10 @@ export type MemoryAsset = MemoryIndex & {
};
class MemoryStoreSvelte {
constructor() {
eventManager.on('auth.logout', () => this.clearCache());
}
memories = $state<MemoryResponseDto[]>([]);
private initialized = false;
private memoryAssets = $derived.by(() => {

View File

@@ -1,7 +1,13 @@
import { eventManager } from '$lib/managers/event.manager.svelte';
class SearchStore {
savedSearchTerms = $state<string[]>([]);
isSearchEnabled = $state(false);
constructor() {
eventManager.on('auth.logout', () => this.clearCache());
}
clearCache() {
this.savedSearchTerms = [];
this.isSearchEnabled = false;

View File

@@ -1,3 +1,4 @@
import { eventManager } from '$lib/managers/event.manager.svelte';
import { purchaseStore } from '$lib/stores/purchase.store';
import { type UserAdminResponseDto, type UserPreferencesResponseDto } from '@immich/sdk';
import { writable } from 'svelte/store';
@@ -14,3 +15,5 @@ export const resetSavedUser = () => {
preferences.set(undefined as unknown as UserPreferencesResponseDto);
purchaseStore.setPurchaseStatus(false);
};
eventManager.on('auth.logout', () => resetSavedUser());

View File

@@ -1,3 +1,4 @@
import { eventManager } from '$lib/managers/event.manager.svelte';
import type {
AlbumResponseDto,
ServerAboutResponseDto,
@@ -19,8 +20,10 @@ const defaultUserInteraction: UserInteractions = {
serverInfo: undefined,
};
export const resetUserInteraction = () => {
export const userInteraction = $state<UserInteractions>(defaultUserInteraction);
const reset = () => {
Object.assign(userInteraction, defaultUserInteraction);
};
export const userInteraction = $state<UserInteractions>(defaultUserInteraction);
eventManager.on('auth.logout', () => reset());

View File

@@ -1,5 +1,4 @@
import { AppRoute } from '$lib/constants';
import { handleLogout } from '$lib/utils/auth';
import { authManager } from '$lib/managers/auth-manager.svelte';
import { createEventEmitter } from '$lib/utils/eventemitter';
import type { AssetResponseDto, ServerVersionResponseDto } from '@immich/sdk';
import { io, type Socket } from 'socket.io-client';
@@ -50,7 +49,7 @@ websocket
.on('disconnect', () => websocketStore.connected.set(false))
.on('on_server_version', (serverVersion) => websocketStore.serverVersion.set(serverVersion))
.on('on_new_release', (releaseVersion) => websocketStore.release.set(releaseVersion))
.on('on_session_delete', () => handleLogout(AppRoute.AUTH_LOGIN))
.on('on_session_delete', () => authManager.logout())
.on('connect_error', (e) => console.log('Websocket Connect Error', e));
export const openWebsocketConnection = () => {

View File

@@ -3,9 +3,9 @@ import FormatBoldMessage from '$lib/components/i18n/format-bold-message.svelte';
import type { InterpolationValues } from '$lib/components/i18n/format-message';
import { NotificationType, notificationController } from '$lib/components/shared-components/notification/notification';
import { AppRoute } from '$lib/constants';
import { downloadManager } from '$lib/managers/download.manager.svelte';
import type { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
import { assetsSnapshot, isSelectingAllAssets, type AssetStore } from '$lib/stores/assets-store.svelte';
import { downloadManager } from '$lib/stores/download-store.svelte';
import { preferences } from '$lib/stores/user.store';
import { downloadRequest, getKey, withError } from '$lib/utils';
import { createAlbum } from '$lib/utils/album-utils';

View File

@@ -1,11 +1,7 @@
import { browser } from '$app/environment';
import { goto } from '$app/navigation';
import { foldersStore } from '$lib/stores/folders.svelte';
import { memoryStore } from '$lib/stores/memory.store.svelte';
import { purchaseStore } from '$lib/stores/purchase.store';
import { searchStore } from '$lib/stores/search.svelte';
import { preferences as preferences$, resetSavedUser, user as user$ } from '$lib/stores/user.store';
import { resetUserInteraction, userInteraction } from '$lib/stores/user.svelte';
import { preferences as preferences$, user as user$ } from '$lib/stores/user.store';
import { userInteraction } from '$lib/stores/user.svelte';
import { getAboutInfo, getMyPreferences, getMyUser, getStorage } from '@immich/sdk';
import { redirect } from '@sveltejs/kit';
import { DateTime } from 'luxon';
@@ -91,19 +87,3 @@ export const getAccountAge = (): number => {
return Number(accountAge);
};
export const handleLogout = async (redirectUri: string) => {
try {
if (redirectUri.startsWith('/')) {
await goto(redirectUri);
} else {
globalThis.location.href = redirectUri;
}
} finally {
resetSavedUser();
resetUserInteraction();
foldersStore.clearCache();
memoryStore.clearCache();
searchStore.clearCache();
}
};

View File

@@ -34,7 +34,6 @@
} from '$lib/components/shared-components/notification/notification';
import UserAvatar from '$lib/components/shared-components/user-avatar.svelte';
import { AppRoute, AlbumPageViewMode } from '$lib/constants';
import { numberOfComments, setNumberOfComments, updateNumberOfComments } from '$lib/stores/activity.store';
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
import { AssetStore } from '$lib/stores/assets-store.svelte';
import { SlideshowNavigation, SlideshowState, slideshowStore } from '$lib/stores/slideshow.store';
@@ -87,6 +86,7 @@
import { confirmAlbumDelete } from '$lib/utils/album-utils';
import TagAction from '$lib/components/photos-page/actions/tag-action.svelte';
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
import { activityManager } from '$lib/managers/activity.manager.svelte';
interface Props {
data: PageData;
@@ -191,7 +191,7 @@
const getNumberOfComments = async () => {
try {
const { comments } = await getActivityStatistics({ albumId: album.id });
setNumberOfComments(comments);
activityManager.numberOfComments = comments;
} catch (error) {
handleError(error, $t('errors.cant_get_number_of_comments'));
}
@@ -398,7 +398,7 @@
let albumId = $derived(album.id);
$effect(() => {
if (!album.isActivityEnabled && $numberOfComments === 0) {
if (!album.isActivityEnabled && activityManager.numberOfComments === 0) {
isShowActivity = false;
}
});
@@ -420,7 +420,9 @@
let isOwned = $derived($user.id == album.ownerId);
let showActivityStatus = $derived(
album.albumUsers.length > 0 && !$showAssetViewer && (album.isActivityEnabled || $numberOfComments > 0),
album.albumUsers.length > 0 &&
!$showAssetViewer &&
(album.isActivityEnabled || activityManager.numberOfComments > 0),
);
let isEditor = $derived(
album.albumUsers.find(({ user: { id } }) => id === $user.id)?.role === AlbumUserRole.Editor ||
@@ -712,7 +714,7 @@
<ActivityStatus
disabled={!album.isActivityEnabled}
{isLiked}
numberOfComments={$numberOfComments}
numberOfComments={activityManager.numberOfComments}
onFavorite={handleFavorite}
onOpenActivityTab={handleOpenAndCloseActivityTab}
/>
@@ -735,8 +737,8 @@
albumId={album.id}
{isLiked}
bind:reactions
onAddComment={() => updateNumberOfComments(1)}
onDeleteComment={() => updateNumberOfComments(-1)}
onAddComment={() => activityManager.updateNumberOfComments(1)}
onDeleteComment={() => activityManager.updateNumberOfComments(-1)}
onDeleteLike={() => (isLiked = null)}
onClose={handleOpenAndCloseActivityTab}
/>

View File

@@ -20,10 +20,10 @@
import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte';
import EmptyPlaceholder from '$lib/components/shared-components/empty-placeholder.svelte';
import { AssetAction } from '$lib/constants';
import { faceManager } from '$lib/managers/face.manager.svelte';
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
import { AssetStore } from '$lib/stores/assets-store.svelte';
import { isFaceEditMode } from '$lib/stores/face-edit.svelte';
import { preferences, user } from '$lib/stores/user.store';
import {
updateStackedAssetInTimeline,
@@ -76,7 +76,7 @@
};
beforeNavigate(() => {
isFaceEditMode.value = false;
faceManager.isEditMode = false;
});
</script>

View File

@@ -7,7 +7,6 @@
NotificationType,
notificationController,
} from '$lib/components/shared-components/notification/notification';
import { downloadManager } from '$lib/stores/download-store.svelte';
import { locale } from '$lib/stores/preferences.store';
import { copyToClipboard } from '$lib/utils';
import { downloadBlob } from '$lib/utils/asset-utils';
@@ -17,6 +16,7 @@
import { mdiCheckAll, mdiContentCopy, mdiDownload, mdiRefresh, mdiWrench } from '@mdi/js';
import { t } from 'svelte-i18n';
import type { PageData } from './$types';
import { downloadManager } from '$lib/managers/download.manager.svelte';
interface Props {
data: PageData;

View File

@@ -22,7 +22,6 @@
import SettingAccordionState from '$lib/components/shared-components/settings/setting-accordion-state.svelte';
import SettingAccordion from '$lib/components/shared-components/settings/setting-accordion.svelte';
import { QueryParameter } from '$lib/constants';
import { downloadManager } from '$lib/stores/download-store.svelte';
import { featureFlags } from '$lib/stores/server-config.store';
import { copyToClipboard } from '$lib/utils';
import { downloadBlob } from '$lib/utils/asset-utils';
@@ -53,6 +52,7 @@
import type { Component } from 'svelte';
import type { SettingsComponentProps } from '$lib/components/admin-page/settings/admin-settings';
import SearchBar from '$lib/components/elements/search-bar.svelte';
import { downloadManager } from '$lib/managers/download.manager.svelte';
interface Props {
data: PageData;

View File

@@ -1,9 +1,8 @@
<script lang="ts">
import { goto } from '$app/navigation';
import AuthPageLayout from '$lib/components/layouts/AuthPageLayout.svelte';
import { AppRoute } from '$lib/constants';
import { resetSavedUser, user } from '$lib/stores/user.store';
import { logout, updateMyUser } from '@immich/sdk';
import { authManager } from '$lib/managers/auth-manager.svelte';
import { user } from '$lib/stores/user.store';
import { updateMyUser } from '@immich/sdk';
import { Alert, Button, Field, HelperText, PasswordInput, Stack, Text } from '@immich/ui';
import { t } from 'svelte-i18n';
import type { PageData } from './$types';
@@ -25,9 +24,7 @@
}
await updateMyUser({ userUpdateMeDto: { password } });
await goto(AppRoute.AUTH_LOGIN);
resetSavedUser();
await logout();
await authManager.logout();
};
</script>