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

View File

@@ -1,6 +1,6 @@
{ {
"name": "@immich/cli", "name": "@immich/cli",
"version": "2.2.64", "version": "2.2.65",
"description": "Command Line Interface (CLI) for Immich", "description": "Command Line Interface (CLI) for Immich",
"type": "module", "type": "module",
"exports": "./dist/index.js", "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.) 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! 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 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. 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. 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. 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) 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. 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. 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 # Database Queries
:::danger :::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 :::tip

View File

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

8
e2e/package-lock.json generated
View File

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

View File

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

View File

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

View File

@@ -35,8 +35,8 @@ platform :android do
task: 'bundle', task: 'bundle',
build_type: 'Release', build_type: 'Release',
properties: { properties: {
"android.injected.version.code" => 196, "android.injected.version.code" => 197,
"android.injected.version.name" => "1.132.2", "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') 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 = { 97C146ED1CF9000F007C117D = {
CreatedOnToolsVersion = 7.3.1; CreatedOnToolsVersion = 7.3.1;
LastSwiftMigration = 1100; LastSwiftMigration = 1100;
ProvisioningStyle = Automatic;
}; };
FAC6F88F2D287C890078CB2F = { FAC6F88F2D287C890078CB2F = {
CreatedOnToolsVersion = 16.0; CreatedOnToolsVersion = 16.0;
ProvisioningStyle = Automatic;
}; };
}; };
}; };
@@ -541,7 +543,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/RunnerProfile.entitlements; CODE_SIGN_ENTITLEMENTS = Runner/RunnerProfile.entitlements;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 202; CURRENT_PROJECT_VERSION = 205;
CUSTOM_GROUP_ID = group.app.immich.share; CUSTOM_GROUP_ID = group.app.immich.share;
DEVELOPMENT_TEAM = 2F67MQ8R79; DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
@@ -685,7 +687,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 202; CURRENT_PROJECT_VERSION = 205;
CUSTOM_GROUP_ID = group.app.immich.share; CUSTOM_GROUP_ID = group.app.immich.share;
DEVELOPMENT_TEAM = 2F67MQ8R79; DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
@@ -715,7 +717,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 202; CURRENT_PROJECT_VERSION = 205;
CUSTOM_GROUP_ID = group.app.immich.share; CUSTOM_GROUP_ID = group.app.immich.share;
DEVELOPMENT_TEAM = 2F67MQ8R79; DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
@@ -748,7 +750,7 @@
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 202; CURRENT_PROJECT_VERSION = 205;
CUSTOM_GROUP_ID = group.app.immich.share; CUSTOM_GROUP_ID = group.app.immich.share;
DEVELOPMENT_TEAM = 2F67MQ8R79; DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_USER_SCRIPT_SANDBOXING = YES; ENABLE_USER_SCRIPT_SANDBOXING = YES;
@@ -769,6 +771,7 @@
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = app.alextran.immich.vdebug.ShareExtension; PRODUCT_BUNDLE_IDENTIFIER = app.alextran.immich.vdebug.ShareExtension;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SKIP_INSTALL = YES; SKIP_INSTALL = YES;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_EMIT_LOC_STRINGS = YES;
@@ -791,7 +794,7 @@
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 202; CURRENT_PROJECT_VERSION = 205;
CUSTOM_GROUP_ID = group.app.immich.share; CUSTOM_GROUP_ID = group.app.immich.share;
DEVELOPMENT_TEAM = 2F67MQ8R79; DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_USER_SCRIPT_SANDBOXING = YES; ENABLE_USER_SCRIPT_SANDBOXING = YES;
@@ -811,6 +814,7 @@
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = app.alextran.immich.ShareExtension; PRODUCT_BUNDLE_IDENTIFIER = app.alextran.immich.ShareExtension;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SKIP_INSTALL = YES; SKIP_INSTALL = YES;
SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
@@ -831,7 +835,7 @@
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 202; CURRENT_PROJECT_VERSION = 205;
CUSTOM_GROUP_ID = group.app.immich.share; CUSTOM_GROUP_ID = group.app.immich.share;
DEVELOPMENT_TEAM = 2F67MQ8R79; DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_USER_SCRIPT_SANDBOXING = YES; ENABLE_USER_SCRIPT_SANDBOXING = YES;
@@ -851,6 +855,7 @@
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = app.alextran.immich.profile.ShareExtension; PRODUCT_BUNDLE_IDENTIFIER = app.alextran.immich.profile.ShareExtension;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SKIP_INSTALL = YES; SKIP_INSTALL = YES;
SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;

View File

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

View File

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

View File

@@ -1,7 +1,6 @@
import 'package:auto_route/auto_route.dart'; import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:geolocator/geolocator.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/user.model.dart'; import 'package:immich_mobile/domain/models/user.model.dart';
import 'package:immich_mobile/extensions/asyncvalue_extensions.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/routing/router.dart';
import 'package:immich_mobile/services/api.service.dart'; import 'package:immich_mobile/services/api.service.dart';
import 'package:immich_mobile/utils/image_url_builder.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/album/album_thumbnail_card.dart';
import 'package:immich_mobile/widgets/common/immich_app_bar.dart'; import 'package:immich_mobile/widgets/common/immich_app_bar.dart';
import 'package:immich_mobile/widgets/common/user_avatar.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 widthFactor = isTablet ? 0.25 : 0.5;
final size = context.width * widthFactor - 20.0; final size = context.width * widthFactor - 20.0;
return FutureBuilder<(Position?, LocationPermission?)>( return GestureDetector(
future: MapUtils.checkPermAndGetLocation( onTap: () => context.pushRoute(
context: context, PlacesCollectionRoute(
silent: true, currentLocation: null,
),
), ),
builder: (context, snapshot) { child: Column(
var position = snapshot.data?.$1; crossAxisAlignment: CrossAxisAlignment.start,
return GestureDetector( children: [
onTap: () => context.pushRoute( SizedBox(
PlacesCollectionRoute( height: size,
currentLocation: position != null width: size,
? LatLng(position.latitude, position.longitude) child: DecoratedBox(
: null, 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( Padding(
crossAxisAlignment: CrossAxisAlignment.start, padding: const EdgeInsets.all(8.0),
children: [ child: Text(
SizedBox( 'places'.tr(),
height: size, style: context.textTheme.titleSmall?.copyWith(
width: size, color: context.colorScheme.onSurface,
child: DecoratedBox( fontWeight: FontWeight.w500,
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,
),
),
),
],
), ),
); ],
}, ),
); );
}, },
); );

View File

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

View File

@@ -207,9 +207,27 @@ class LoginForm extends HookConsumerWidget {
} }
String generateRandomString(int length) { String generateRandomString(int length) {
const chars =
'AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz1234567890';
final random = Random.secure(); final random = Random.secure();
return base64Url return String.fromCharCodes(
.encode(List<int>.generate(32, (i) => random.nextInt(256))); 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 { Future<String> generatePKCECodeChallenge(String codeVerifier) async {
@@ -223,7 +241,8 @@ class LoginForm extends HookConsumerWidget {
String? oAuthServerUrl; String? oAuthServerUrl;
final state = generateRandomString(32); final state = generateRandomString(32);
final codeVerifier = generateRandomString(64);
final codeVerifier = randomCodeVerifier();
final codeChallenge = await generatePKCECodeChallenge(codeVerifier); final codeChallenge = await generatePKCECodeChallenge(codeVerifier);
try { try {

View File

@@ -3,7 +3,7 @@ Immich API
This Dart package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project: 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 - Generator version: 7.8.0
- Build package: org.openapitools.codegen.languages.DartClientCodegen - 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 description: Immich - selfhosted backup media file on mobile phone
publish_to: 'none' publish_to: 'none'
version: 1.132.2+196 version: 1.132.3+197
environment: environment:
sdk: '>=3.3.0 <4.0.0' sdk: '>=3.3.0 <4.0.0'

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

6
web/package-lock.json generated
View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
<script lang="ts"> <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 { locale } from '$lib/stores/preferences.store';
import { fly, slide } from 'svelte/transition'; import { fly, slide } from 'svelte/transition';
import { getByteUnitString } from '../../utils/byte-units'; import { getByteUnitString } from '../../utils/byte-units';
@@ -13,15 +13,15 @@
}; };
</script> </script>
{#if downloadStore.isDownloading} {#if downloadManager.isDownloading}
<div <div
transition:fly={{ x: -100, duration: 350 }} 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" 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> <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"> <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)} {#each Object.keys(downloadManager.assets) as downloadKey (downloadKey)}
{@const download = downloadStore.assets[downloadKey]} {@const download = downloadManager.assets[downloadKey]}
<div class="mb-2 flex place-items-center" transition:slide> <div class="mb-2 flex place-items-center" transition:slide>
<div class="w-full pr-10"> <div class="w-full pr-10">
<div class="flex place-items-center justify-between gap-2 text-xs font-medium"> <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 ImageThumbnail from '$lib/components/assets/thumbnail/image-thumbnail.svelte';
import { dialogController } from '$lib/components/shared-components/dialog/dialog'; import { dialogController } from '$lib/components/shared-components/dialog/dialog';
import { notificationController } from '$lib/components/shared-components/notification/notification'; import { notificationController } from '$lib/components/shared-components/notification/notification';
import { isFaceEditMode } from '$lib/stores/face-edit.svelte';
import { getPeopleThumbnailUrl } from '$lib/utils'; import { getPeopleThumbnailUrl } from '$lib/utils';
import { getAllPeople, createFace, type PersonResponseDto } from '@immich/sdk'; import { getAllPeople, createFace, type PersonResponseDto } from '@immich/sdk';
import { Button, Input } from '@immich/ui'; import { Button, Input } from '@immich/ui';
@@ -10,6 +9,7 @@
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { assetViewingStore } from '$lib/stores/asset-viewing.store'; import { assetViewingStore } from '$lib/stores/asset-viewing.store';
import { handleError } from '$lib/utils/handle-error'; import { handleError } from '$lib/utils/handle-error';
import { faceManager } from '$lib/managers/face.manager.svelte';
interface Props { interface Props {
htmlElement: HTMLImageElement | HTMLVideoElement; htmlElement: HTMLImageElement | HTMLVideoElement;
@@ -140,7 +140,7 @@
}; };
const cancel = () => { const cancel = () => {
isFaceEditMode.value = false; faceManager.isEditMode = false;
}; };
const getPeople = async () => { const getPeople = async () => {
@@ -303,7 +303,7 @@
} catch (error) { } catch (error) {
handleError(error, 'Error tagging face'); handleError(error, 'Error tagging face');
} finally { } finally {
isFaceEditMode.value = false; faceManager.isEditMode = false;
} }
}; };
</script> </script>

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,9 +1,9 @@
<script lang="ts"> <script lang="ts">
import Icon from '$lib/components/elements/icon.svelte'; import Icon from '$lib/components/elements/icon.svelte';
import { generateId } from '$lib/utils/generate-id'; import { generateId } from '$lib/utils/generate-id';
import { optionClickCallbackStore, selectedIdStore } from '$lib/stores/context-menu.store';
import type { Shortcut } from '$lib/actions/shortcut'; import type { Shortcut } from '$lib/actions/shortcut';
import { shortcutLabel as computeShortcutLabel, shortcut as bindShortcut } 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 { interface Props {
text: string; text: string;
@@ -29,10 +29,10 @@
let id: string = generateId(); let id: string = generateId();
let isActive = $derived($selectedIdStore === id); let isActive = $derived(contextMenuManager.selectedId === id);
const handleClick = () => { const handleClick = () => {
$optionClickCallbackStore?.(); contextMenuManager.optionClickCallback?.();
onClick(); onClick();
}; };
@@ -51,8 +51,8 @@
<li <li
{id} {id}
onclick={handleClick} onclick={handleClick}
onmouseover={() => ($selectedIdStore = id)} onmouseover={() => (contextMenuManager.selectedId = id)}
onmouseleave={() => ($selectedIdStore = undefined)} 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 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 ? activeColor
: 'bg-slate-100'}" : 'bg-slate-100'}"

View File

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

View File

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

View File

@@ -10,11 +10,13 @@
import ImmichLogo from '$lib/components/shared-components/immich-logo.svelte'; import ImmichLogo from '$lib/components/shared-components/immich-logo.svelte';
import SearchBar from '$lib/components/shared-components/search-bar/search-bar.svelte'; import SearchBar from '$lib/components/shared-components/search-bar/search-bar.svelte';
import { AppRoute } from '$lib/constants'; 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 { featureFlags } from '$lib/stores/server-config.store';
import { sidebarStore } from '$lib/stores/sidebar.svelte';
import { user } from '$lib/stores/user.store'; import { user } from '$lib/stores/user.store';
import { userInteraction } from '$lib/stores/user.svelte'; import { userInteraction } from '$lib/stores/user.svelte';
import { handleLogout } from '$lib/utils/auth'; import { getAboutInfo, type ServerAboutResponseDto } from '@immich/sdk';
import { getAboutInfo, logout, type ServerAboutResponseDto } from '@immich/sdk';
import { Button, IconButton } from '@immich/ui'; import { Button, IconButton } from '@immich/ui';
import { mdiHelpCircleOutline, mdiMagnify, mdiMenu, mdiTrayArrowUp } from '@mdi/js'; import { mdiHelpCircleOutline, mdiMagnify, mdiMenu, mdiTrayArrowUp } from '@mdi/js';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
@@ -23,8 +25,6 @@
import ThemeButton from '../theme-button.svelte'; import ThemeButton from '../theme-button.svelte';
import UserAvatar from '../user-avatar.svelte'; import UserAvatar from '../user-avatar.svelte';
import AccountInfoPanel from './account-info-panel.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 { interface Props {
showUploadButton?: boolean; showUploadButton?: boolean;
@@ -38,11 +38,6 @@
let shouldShowHelpPanel = $state(false); let shouldShowHelpPanel = $state(false);
let innerWidth: number = $state(0); let innerWidth: number = $state(0);
const onLogout = async () => {
const { redirectUri } = await logout();
await handleLogout(redirectUri);
};
let info: ServerAboutResponseDto | undefined = $state(); let info: ServerAboutResponseDto | undefined = $state();
onMount(async () => { onMount(async () => {
@@ -183,7 +178,7 @@
{/if} {/if}
{#if shouldShowAccountInfoPanel} {#if shouldShowAccountInfoPanel}
<AccountInfoPanel {onLogout} /> <AccountInfoPanel onLogout={() => authManager.logout()} />
{/if} {/if}
</div> </div>
</section> </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 { import {
getAssetsByOriginalPath, getAssetsByOriginalPath,
getUniqueOriginalPaths, getUniqueOriginalPaths,
@@ -16,6 +17,10 @@ class FoldersStore {
uniquePaths = $state<string[]>([]); uniquePaths = $state<string[]>([]);
assets = $state<AssetCache>({}); assets = $state<AssetCache>({});
constructor() {
eventManager.on('auth.logout', () => this.clearCache());
}
async fetchUniquePaths() { async fetchUniquePaths() {
if (this.initialized) { if (this.initialized) {
return; return;

View File

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

View File

@@ -1,7 +1,13 @@
import { eventManager } from '$lib/managers/event.manager.svelte';
class SearchStore { class SearchStore {
savedSearchTerms = $state<string[]>([]); savedSearchTerms = $state<string[]>([]);
isSearchEnabled = $state(false); isSearchEnabled = $state(false);
constructor() {
eventManager.on('auth.logout', () => this.clearCache());
}
clearCache() { clearCache() {
this.savedSearchTerms = []; this.savedSearchTerms = [];
this.isSearchEnabled = false; 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 { purchaseStore } from '$lib/stores/purchase.store';
import { type UserAdminResponseDto, type UserPreferencesResponseDto } from '@immich/sdk'; import { type UserAdminResponseDto, type UserPreferencesResponseDto } from '@immich/sdk';
import { writable } from 'svelte/store'; import { writable } from 'svelte/store';
@@ -14,3 +15,5 @@ export const resetSavedUser = () => {
preferences.set(undefined as unknown as UserPreferencesResponseDto); preferences.set(undefined as unknown as UserPreferencesResponseDto);
purchaseStore.setPurchaseStatus(false); purchaseStore.setPurchaseStatus(false);
}; };
eventManager.on('auth.logout', () => resetSavedUser());

View File

@@ -1,3 +1,4 @@
import { eventManager } from '$lib/managers/event.manager.svelte';
import type { import type {
AlbumResponseDto, AlbumResponseDto,
ServerAboutResponseDto, ServerAboutResponseDto,
@@ -19,8 +20,10 @@ const defaultUserInteraction: UserInteractions = {
serverInfo: undefined, serverInfo: undefined,
}; };
export const resetUserInteraction = () => { export const userInteraction = $state<UserInteractions>(defaultUserInteraction);
const reset = () => {
Object.assign(userInteraction, defaultUserInteraction); 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 { authManager } from '$lib/managers/auth-manager.svelte';
import { handleLogout } from '$lib/utils/auth';
import { createEventEmitter } from '$lib/utils/eventemitter'; import { createEventEmitter } from '$lib/utils/eventemitter';
import type { AssetResponseDto, ServerVersionResponseDto } from '@immich/sdk'; import type { AssetResponseDto, ServerVersionResponseDto } from '@immich/sdk';
import { io, type Socket } from 'socket.io-client'; import { io, type Socket } from 'socket.io-client';
@@ -50,7 +49,7 @@ websocket
.on('disconnect', () => websocketStore.connected.set(false)) .on('disconnect', () => websocketStore.connected.set(false))
.on('on_server_version', (serverVersion) => websocketStore.serverVersion.set(serverVersion)) .on('on_server_version', (serverVersion) => websocketStore.serverVersion.set(serverVersion))
.on('on_new_release', (releaseVersion) => websocketStore.release.set(releaseVersion)) .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)); .on('connect_error', (e) => console.log('Websocket Connect Error', e));
export const openWebsocketConnection = () => { 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 type { InterpolationValues } from '$lib/components/i18n/format-message';
import { NotificationType, notificationController } from '$lib/components/shared-components/notification/notification'; import { NotificationType, notificationController } from '$lib/components/shared-components/notification/notification';
import { AppRoute } from '$lib/constants'; import { AppRoute } from '$lib/constants';
import { downloadManager } from '$lib/managers/download.manager.svelte';
import type { AssetInteraction } from '$lib/stores/asset-interaction.svelte'; import type { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
import { assetsSnapshot, isSelectingAllAssets, type AssetStore } from '$lib/stores/assets-store.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 { preferences } from '$lib/stores/user.store';
import { downloadRequest, getKey, withError } from '$lib/utils'; import { downloadRequest, getKey, withError } from '$lib/utils';
import { createAlbum } from '$lib/utils/album-utils'; import { createAlbum } from '$lib/utils/album-utils';

View File

@@ -1,11 +1,7 @@
import { browser } from '$app/environment'; 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 { purchaseStore } from '$lib/stores/purchase.store';
import { searchStore } from '$lib/stores/search.svelte'; import { preferences as preferences$, user as user$ } from '$lib/stores/user.store';
import { preferences as preferences$, resetSavedUser, user as user$ } from '$lib/stores/user.store'; import { userInteraction } from '$lib/stores/user.svelte';
import { resetUserInteraction, userInteraction } from '$lib/stores/user.svelte';
import { getAboutInfo, getMyPreferences, getMyUser, getStorage } from '@immich/sdk'; import { getAboutInfo, getMyPreferences, getMyUser, getStorage } from '@immich/sdk';
import { redirect } from '@sveltejs/kit'; import { redirect } from '@sveltejs/kit';
import { DateTime } from 'luxon'; import { DateTime } from 'luxon';
@@ -91,19 +87,3 @@ export const getAccountAge = (): number => {
return Number(accountAge); 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'; } from '$lib/components/shared-components/notification/notification';
import UserAvatar from '$lib/components/shared-components/user-avatar.svelte'; import UserAvatar from '$lib/components/shared-components/user-avatar.svelte';
import { AppRoute, AlbumPageViewMode } from '$lib/constants'; import { AppRoute, AlbumPageViewMode } from '$lib/constants';
import { numberOfComments, setNumberOfComments, updateNumberOfComments } from '$lib/stores/activity.store';
import { assetViewingStore } from '$lib/stores/asset-viewing.store'; import { assetViewingStore } from '$lib/stores/asset-viewing.store';
import { AssetStore } from '$lib/stores/assets-store.svelte'; import { AssetStore } from '$lib/stores/assets-store.svelte';
import { SlideshowNavigation, SlideshowState, slideshowStore } from '$lib/stores/slideshow.store'; import { SlideshowNavigation, SlideshowState, slideshowStore } from '$lib/stores/slideshow.store';
@@ -87,6 +86,7 @@
import { confirmAlbumDelete } from '$lib/utils/album-utils'; import { confirmAlbumDelete } from '$lib/utils/album-utils';
import TagAction from '$lib/components/photos-page/actions/tag-action.svelte'; import TagAction from '$lib/components/photos-page/actions/tag-action.svelte';
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte'; import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
import { activityManager } from '$lib/managers/activity.manager.svelte';
interface Props { interface Props {
data: PageData; data: PageData;
@@ -191,7 +191,7 @@
const getNumberOfComments = async () => { const getNumberOfComments = async () => {
try { try {
const { comments } = await getActivityStatistics({ albumId: album.id }); const { comments } = await getActivityStatistics({ albumId: album.id });
setNumberOfComments(comments); activityManager.numberOfComments = comments;
} catch (error) { } catch (error) {
handleError(error, $t('errors.cant_get_number_of_comments')); handleError(error, $t('errors.cant_get_number_of_comments'));
} }
@@ -398,7 +398,7 @@
let albumId = $derived(album.id); let albumId = $derived(album.id);
$effect(() => { $effect(() => {
if (!album.isActivityEnabled && $numberOfComments === 0) { if (!album.isActivityEnabled && activityManager.numberOfComments === 0) {
isShowActivity = false; isShowActivity = false;
} }
}); });
@@ -420,7 +420,9 @@
let isOwned = $derived($user.id == album.ownerId); let isOwned = $derived($user.id == album.ownerId);
let showActivityStatus = $derived( 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( let isEditor = $derived(
album.albumUsers.find(({ user: { id } }) => id === $user.id)?.role === AlbumUserRole.Editor || album.albumUsers.find(({ user: { id } }) => id === $user.id)?.role === AlbumUserRole.Editor ||
@@ -712,7 +714,7 @@
<ActivityStatus <ActivityStatus
disabled={!album.isActivityEnabled} disabled={!album.isActivityEnabled}
{isLiked} {isLiked}
numberOfComments={$numberOfComments} numberOfComments={activityManager.numberOfComments}
onFavorite={handleFavorite} onFavorite={handleFavorite}
onOpenActivityTab={handleOpenAndCloseActivityTab} onOpenActivityTab={handleOpenAndCloseActivityTab}
/> />
@@ -735,8 +737,8 @@
albumId={album.id} albumId={album.id}
{isLiked} {isLiked}
bind:reactions bind:reactions
onAddComment={() => updateNumberOfComments(1)} onAddComment={() => activityManager.updateNumberOfComments(1)}
onDeleteComment={() => updateNumberOfComments(-1)} onDeleteComment={() => activityManager.updateNumberOfComments(-1)}
onDeleteLike={() => (isLiked = null)} onDeleteLike={() => (isLiked = null)}
onClose={handleOpenAndCloseActivityTab} onClose={handleOpenAndCloseActivityTab}
/> />

View File

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

View File

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

View File

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

View File

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