mirror of
https://github.com/immich-app/immich.git
synced 2026-01-31 09:14:47 -08:00
Compare commits
7 Commits
feat/share
...
refactor/p
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2aab5c28a1 | ||
|
|
855817514c | ||
|
|
d5ad35ea52 | ||
|
|
e63213d774 | ||
|
|
0be1ffade6 | ||
|
|
1a04caee29 | ||
|
|
3ace578fc0 |
@@ -1,6 +1,6 @@
|
||||
[tools]
|
||||
terragrunt = "0.98.0"
|
||||
opentofu = "1.10.7"
|
||||
opentofu = "1.11.4"
|
||||
|
||||
[tasks."tg:fmt"]
|
||||
run = "terragrunt hclfmt"
|
||||
|
||||
@@ -18,8 +18,8 @@ node = "24.13.0"
|
||||
flutter = "3.35.7"
|
||||
pnpm = "10.28.0"
|
||||
terragrunt = "0.98.0"
|
||||
opentofu = "1.10.7"
|
||||
java = "25.0.1"
|
||||
opentofu = "1.11.4"
|
||||
java = "21.0.2"
|
||||
|
||||
[tools."github:CQLabs/homebrew-dcm"]
|
||||
version = "1.30.0"
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"flutter": "3.35.7"
|
||||
}
|
||||
5
mobile/.gitignore
vendored
5
mobile/.gitignore
vendored
@@ -55,8 +55,5 @@ default.isar
|
||||
default.isar.lock
|
||||
libisar.so
|
||||
|
||||
# FVM Version
|
||||
.fvm/
|
||||
|
||||
# Translation file
|
||||
lib/generated/
|
||||
lib/generated/
|
||||
|
||||
4
mobile/.vscode/settings.json
vendored
4
mobile/.vscode/settings.json
vendored
@@ -2,7 +2,9 @@
|
||||
"dart.flutterSdkPath": ".fvm/versions/3.35.7",
|
||||
"dart.lineLength": 120,
|
||||
"[dart]": {
|
||||
"editor.rulers": [120]
|
||||
"editor.rulers": [
|
||||
120
|
||||
]
|
||||
},
|
||||
"search.exclude": {
|
||||
"**/.fvm": true
|
||||
|
||||
@@ -4,10 +4,12 @@ The Immich mobile app is a Flutter-based solution leveraging the Isar Database f
|
||||
|
||||
## Setup
|
||||
|
||||
1. Setup Flutter toolchain using FVM.
|
||||
2. Run `flutter pub get` to install the dependencies.
|
||||
3. Run `make translation` to generate the translation file.
|
||||
4. Run `fvm flutter run` to start the app.
|
||||
1. [Install mise](https://mise.jdx.dev/installing-mise.html).
|
||||
2. Change to the immich directory and trust the mise config with `mise trust`.
|
||||
3. Install tools with mise: `mise install`.
|
||||
4. Run `flutter pub get` to install the dependencies.
|
||||
5. Run `make translation` to generate the translation file.
|
||||
6. Run `flutter run` to start the app.
|
||||
|
||||
## Translation
|
||||
|
||||
@@ -29,7 +31,7 @@ dcm analyze lib
|
||||
```
|
||||
|
||||
[DCM](https://dcm.dev/) is a vendor tool that needs to be downloaded manually to run locally.
|
||||
Immich was provided an open source license.
|
||||
Immich was provided an open source license.
|
||||
To use it, it is important that you do not have an active free tier license (can be verified with `dcm license`).
|
||||
If you have write-access to the Immich repository directly, running dcm in your clone should just work.
|
||||
If you are working on a clone of a fork, you need to connect to the main Immich repository as remote first:
|
||||
|
||||
@@ -20,7 +20,7 @@ enum VersionStatus {
|
||||
|
||||
class ServerInfo {
|
||||
final ServerVersion serverVersion;
|
||||
final ServerVersion latestVersion;
|
||||
final ServerVersion? latestVersion;
|
||||
final ServerFeatures serverFeatures;
|
||||
final ServerConfig serverConfig;
|
||||
final ServerDiskInfo serverDiskInfo;
|
||||
|
||||
@@ -15,7 +15,7 @@ class ServerInfoNotifier extends StateNotifier<ServerInfo> {
|
||||
: super(
|
||||
const ServerInfo(
|
||||
serverVersion: ServerVersion(major: 0, minor: 0, patch: 0),
|
||||
latestVersion: ServerVersion(major: 0, minor: 0, patch: 0),
|
||||
latestVersion: null,
|
||||
serverFeatures: ServerFeatures(map: true, trash: true, oauthEnabled: false, passwordLogin: true),
|
||||
serverConfig: ServerConfig(
|
||||
trashDays: 30,
|
||||
@@ -43,7 +43,7 @@ class ServerInfoNotifier extends StateNotifier<ServerInfo> {
|
||||
try {
|
||||
final serverVersion = await _serverInfoService.getServerVersion();
|
||||
|
||||
// using isClientOutOfDate since that will show to users reguardless of if they are an admin
|
||||
// using isClientOutOfDate since that will show to users regardless of if they are an admin
|
||||
if (serverVersion == null) {
|
||||
state = state.copyWith(versionStatus: VersionStatus.error);
|
||||
return;
|
||||
@@ -76,7 +76,7 @@ class ServerInfoNotifier extends StateNotifier<ServerInfo> {
|
||||
state = state.copyWith(versionStatus: VersionStatus.upToDate);
|
||||
}
|
||||
|
||||
handleReleaseInfo(ServerVersion serverVersion, ServerVersion latestVersion) {
|
||||
handleReleaseInfo(ServerVersion serverVersion, ServerVersion? latestVersion) {
|
||||
// Update local server version
|
||||
_checkServerVersionMismatch(serverVersion, latestVersion: latestVersion);
|
||||
}
|
||||
|
||||
@@ -170,50 +170,52 @@ class AppBarServerInfo extends HookConsumerWidget {
|
||||
),
|
||||
],
|
||||
),
|
||||
const Padding(padding: EdgeInsets.symmetric(horizontal: 10), child: Divider(thickness: 1)),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(left: 10.0),
|
||||
child: Row(
|
||||
children: [
|
||||
if (serverInfoState.versionStatus == VersionStatus.serverOutOfDate)
|
||||
const Padding(
|
||||
padding: EdgeInsets.only(right: 5.0),
|
||||
child: Icon(Icons.info, color: Color.fromARGB(255, 243, 188, 106), size: 12),
|
||||
if (serverInfoState.latestVersion != null) ...[
|
||||
const Padding(padding: EdgeInsets.symmetric(horizontal: 10), child: Divider(thickness: 1)),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(left: 10.0),
|
||||
child: Row(
|
||||
children: [
|
||||
if (serverInfoState.versionStatus == VersionStatus.serverOutOfDate)
|
||||
const Padding(
|
||||
padding: EdgeInsets.only(right: 5.0),
|
||||
child: Icon(Icons.info, color: Color.fromARGB(255, 243, 188, 106), size: 12),
|
||||
),
|
||||
Text(
|
||||
"latest_version".tr(),
|
||||
style: TextStyle(
|
||||
fontSize: titleFontSize,
|
||||
color: context.textTheme.labelSmall?.color,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
"latest_version".tr(),
|
||||
style: TextStyle(
|
||||
fontSize: titleFontSize,
|
||||
color: context.textTheme.labelSmall?.color,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
flex: 0,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(right: 10.0),
|
||||
child: Text(
|
||||
serverInfoState.latestVersion.major > 0
|
||||
? "${serverInfoState.latestVersion.major}.${serverInfoState.latestVersion.minor}.${serverInfoState.latestVersion.patch}"
|
||||
: "--",
|
||||
style: TextStyle(
|
||||
fontSize: contentFontSize,
|
||||
color: context.colorScheme.onSurfaceSecondary,
|
||||
fontWeight: FontWeight.bold,
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Expanded(
|
||||
flex: 0,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(right: 10.0),
|
||||
child: Text(
|
||||
serverInfoState.latestVersion!.major > 0
|
||||
? "${serverInfoState.latestVersion!.major}.${serverInfoState.latestVersion!.minor}.${serverInfoState.latestVersion!.patch}"
|
||||
: "--",
|
||||
style: TextStyle(
|
||||
fontSize: contentFontSize,
|
||||
color: context.colorScheme.onSurfaceSecondary,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -414,6 +414,7 @@ class LoginForm extends HookConsumerWidget {
|
||||
keyboardAction: TextInputAction.next,
|
||||
keyboardType: TextInputType.url,
|
||||
autofillHints: const [AutofillHints.url],
|
||||
autoCorrect: false,
|
||||
onSubmit: (ctx, _) => ImmichForm.of(ctx).submit(),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -12,6 +12,7 @@ class ImmichTextInput extends StatefulWidget {
|
||||
final List<String>? autofillHints;
|
||||
final Widget? suffixIcon;
|
||||
final bool obscureText;
|
||||
final bool autoCorrect;
|
||||
|
||||
const ImmichTextInput({
|
||||
super.key,
|
||||
@@ -26,6 +27,7 @@ class ImmichTextInput extends StatefulWidget {
|
||||
this.autofillHints,
|
||||
this.suffixIcon,
|
||||
this.obscureText = false,
|
||||
this.autoCorrect = true,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -79,6 +81,7 @@ class _ImmichTextInputState extends State<ImmichTextInput> {
|
||||
validator: _validateInput,
|
||||
keyboardType: widget.keyboardType,
|
||||
textInputAction: widget.keyboardAction,
|
||||
autocorrect: widget.autoCorrect,
|
||||
autofillHints: widget.autofillHints,
|
||||
onTap: () => setState(() => _error = null),
|
||||
onTapOutside: (_) => _focusNode.unfocus(),
|
||||
|
||||
@@ -130,7 +130,7 @@ describe(VersionService.name, () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('onWebsocketConnectionEvent', () => {
|
||||
describe('onWebsocketConnection', () => {
|
||||
it('should send on_server_version client event', async () => {
|
||||
await sut.onWebsocketConnection({ userId: '42' });
|
||||
expect(mocks.websocket.clientSend).toHaveBeenCalledWith('on_server_version', '42', expect.any(SemVer));
|
||||
@@ -143,5 +143,12 @@ describe(VersionService.name, () => {
|
||||
expect(mocks.websocket.clientSend).toHaveBeenCalledWith('on_server_version', '42', expect.any(SemVer));
|
||||
expect(mocks.websocket.clientSend).toHaveBeenCalledWith('on_new_release', '42', expect.any(Object));
|
||||
});
|
||||
|
||||
it('should not send a release notification when the version check is disabled', async () => {
|
||||
mocks.systemMetadata.get.mockResolvedValueOnce({ newVersionCheck: { enabled: false } });
|
||||
await sut.onWebsocketConnection({ userId: '42' });
|
||||
expect(mocks.websocket.clientSend).toHaveBeenCalledWith('on_server_version', '42', expect.any(SemVer));
|
||||
expect(mocks.websocket.clientSend).not.toHaveBeenCalledWith('on_new_release', '42', expect.any(Object));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -105,6 +105,12 @@ export class VersionService extends BaseService {
|
||||
@OnEvent({ name: 'WebsocketConnect' })
|
||||
async onWebsocketConnection({ userId }: ArgOf<'WebsocketConnect'>) {
|
||||
this.websocketRepository.clientSend('on_server_version', userId, serverVersion);
|
||||
|
||||
const { newVersionCheck } = await this.getConfig({ withCache: true });
|
||||
if (!newVersionCheck.enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const metadata = await this.systemMetadataRepository.get(SystemMetadataKey.VersionCheckState);
|
||||
if (metadata) {
|
||||
this.websocketRepository.clientSend('on_new_release', userId, asNotification(metadata));
|
||||
|
||||
@@ -194,9 +194,7 @@
|
||||
|
||||
const closeEditor = async () => {
|
||||
if (editManager.hasAppliedEdits) {
|
||||
console.log(asset);
|
||||
const refreshedAsset = await getAssetInfo({ id: asset.id });
|
||||
console.log(refreshedAsset);
|
||||
onAssetChange?.(refreshedAsset);
|
||||
assetViewingStore.setAsset(refreshedAsset);
|
||||
}
|
||||
|
||||
@@ -75,7 +75,7 @@
|
||||
<Button
|
||||
variant="outline"
|
||||
onclick={() => editManager.resetAllChanges()}
|
||||
disabled={!editManager.hasChanges}
|
||||
disabled={!editManager.canReset}
|
||||
class="self-start"
|
||||
shape="round"
|
||||
size="small"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { purchaseStore } from '$lib/stores/purchase.store';
|
||||
import { authManager } from '$lib/managers/auth-manager.svelte';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { activateProduct, getActivationKey } from '$lib/utils/license-utils';
|
||||
import { Button, Heading, LoadingSpinner } from '@immich/ui';
|
||||
@@ -26,7 +26,7 @@
|
||||
await activateProduct(productKey, activationKey);
|
||||
|
||||
onActivate();
|
||||
purchaseStore.setPurchaseStatus(true);
|
||||
authManager.isPurchased = true;
|
||||
} catch (error) {
|
||||
handleError(error, $t('purchase_failed_activation'));
|
||||
} finally {
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
import { goto } from '$app/navigation';
|
||||
import { OpenQueryParam } from '$lib/constants';
|
||||
import Portal from '$lib/elements/Portal.svelte';
|
||||
import { authManager } from '$lib/managers/auth-manager.svelte';
|
||||
import PurchaseModal from '$lib/modals/PurchaseModal.svelte';
|
||||
import { Route } from '$lib/route';
|
||||
import { purchaseStore } from '$lib/stores/purchase.store';
|
||||
import { preferences } from '$lib/stores/user.store';
|
||||
import { getAccountAge } from '$lib/utils/auth';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
@@ -22,8 +22,6 @@
|
||||
|
||||
let showBuyButton = $state(getButtonVisibility());
|
||||
|
||||
const { isPurchased } = purchaseStore;
|
||||
|
||||
const openPurchaseModal = async () => {
|
||||
await modalManager.show(PurchaseModal);
|
||||
showMessage = false;
|
||||
@@ -72,7 +70,7 @@
|
||||
</script>
|
||||
|
||||
<div class="license-status ps-4 text-sm">
|
||||
{#if $isPurchased && $preferences.purchase.showSupportBadge}
|
||||
{#if authManager.isPurchased && $preferences.purchase.showSupportBadge}
|
||||
<button
|
||||
onclick={() => goto(Route.userSettings({ isOpen: OpenQueryParam.PURCHASE_SETTINGS }))}
|
||||
class="w-full mt-2"
|
||||
@@ -80,7 +78,7 @@
|
||||
>
|
||||
<SupporterBadge size="small" effect="always" />
|
||||
</button>
|
||||
{:else if !$isPurchased && showBuyButton && getAccountAge() > 14}
|
||||
{:else if !authManager.isPurchased && showBuyButton && getAccountAge() > 14}
|
||||
<button
|
||||
type="button"
|
||||
onclick={openPurchaseModal}
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
import PurchaseContent from '$lib/components/shared-components/purchasing/purchase-content.svelte';
|
||||
import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte';
|
||||
import { dateFormats } from '$lib/constants';
|
||||
import { authManager } from '$lib/managers/auth-manager.svelte';
|
||||
import { locale } from '$lib/stores/preferences.store';
|
||||
import { purchaseStore } from '$lib/stores/purchase.store';
|
||||
import { preferences, user } from '$lib/stores/user.store';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { setSupportBadgeVisibility } from '$lib/utils/purchase-utils';
|
||||
@@ -22,7 +22,6 @@
|
||||
import { mdiKey } from '@mdi/js';
|
||||
import { onMount } from 'svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
const { isPurchased } = purchaseStore;
|
||||
|
||||
let isServerProduct = $state(false);
|
||||
let serverPurchaseInfo: LicenseResponseDto | null = $state(null);
|
||||
@@ -53,7 +52,7 @@
|
||||
};
|
||||
|
||||
onMount(async () => {
|
||||
if (!$isPurchased) {
|
||||
if (!authManager.isPurchased) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -73,7 +72,7 @@
|
||||
}
|
||||
|
||||
await deleteIndividualProductKey();
|
||||
purchaseStore.setPurchaseStatus(false);
|
||||
authManager.isPurchased = false;
|
||||
} catch (error) {
|
||||
handleError(error, $t('errors.failed_to_remove_product_key'));
|
||||
}
|
||||
@@ -92,21 +91,21 @@
|
||||
}
|
||||
|
||||
await deleteServerProductKey();
|
||||
purchaseStore.setPurchaseStatus(false);
|
||||
authManager.isPurchased = false;
|
||||
} catch (error) {
|
||||
handleError(error, $t('errors.failed_to_remove_product_key'));
|
||||
}
|
||||
};
|
||||
|
||||
const onProductActivated = async () => {
|
||||
purchaseStore.setPurchaseStatus(true);
|
||||
authManager.isPurchased = true;
|
||||
await checkPurchaseInfo();
|
||||
};
|
||||
</script>
|
||||
|
||||
<section class="my-4">
|
||||
<div in:fade={{ duration: 500 }}>
|
||||
{#if $isPurchased}
|
||||
{#if authManager.isPurchased}
|
||||
<!-- BADGE TOGGLE -->
|
||||
<div class="mb-4">
|
||||
<SettingSwitch
|
||||
|
||||
@@ -3,12 +3,31 @@ import { page } from '$app/state';
|
||||
import { eventManager } from '$lib/managers/event-manager.svelte';
|
||||
import { Route } from '$lib/route';
|
||||
import { isSharedLinkRoute } from '$lib/utils/navigation';
|
||||
import { logout } from '@immich/sdk';
|
||||
import { getAboutInfo, logout, type UserAdminResponseDto } from '@immich/sdk';
|
||||
|
||||
class AuthManager {
|
||||
isPurchased = $state(false);
|
||||
isSharedLink = $derived(isSharedLinkRoute(page.route?.id));
|
||||
params = $derived(this.isSharedLink ? { key: page.params.key, slug: page.params.slug } : {});
|
||||
|
||||
constructor() {
|
||||
eventManager.on({
|
||||
AuthUserLoaded: (user) => this.onAuthUserLoaded(user),
|
||||
});
|
||||
}
|
||||
|
||||
private async onAuthUserLoaded(user: UserAdminResponseDto) {
|
||||
if (user.license?.activatedAt) {
|
||||
authManager.isPurchased = true;
|
||||
return;
|
||||
}
|
||||
|
||||
const serverInfo = await getAboutInfo().catch(() => undefined);
|
||||
if (serverInfo?.licensed) {
|
||||
authManager.isPurchased = true;
|
||||
}
|
||||
}
|
||||
|
||||
async logout() {
|
||||
let redirectUri;
|
||||
|
||||
@@ -30,6 +49,7 @@ class AuthManager {
|
||||
globalThis.location.href = redirectUri;
|
||||
}
|
||||
} finally {
|
||||
this.isPurchased = false;
|
||||
eventManager.emit('AuthLogout');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ export interface EditToolManager {
|
||||
onDeactivate: () => void;
|
||||
resetAllChanges: () => Promise<void>;
|
||||
hasChanges: boolean;
|
||||
canReset: boolean;
|
||||
edits: EditAction[];
|
||||
}
|
||||
|
||||
@@ -41,19 +42,22 @@ export class EditManager {
|
||||
|
||||
currentAsset = $state<AssetResponseDto | null>(null);
|
||||
selectedTool = $state<EditTool | null>(null);
|
||||
hasChanges = $derived(this.tools.some((t) => t.manager.hasChanges));
|
||||
|
||||
// used to disable multiple confirm dialogs and mouse events while one is open
|
||||
isShowingConfirmDialog = $state(false);
|
||||
isApplyingEdits = $state(false);
|
||||
hasAppliedEdits = $state(false);
|
||||
|
||||
hasUnsavedChanges = $derived(this.tools.some((t) => t.manager.hasChanges) && !this.hasAppliedEdits);
|
||||
canReset = $derived(this.tools.some((t) => t.manager.canReset));
|
||||
|
||||
async closeConfirm(): Promise<boolean> {
|
||||
// Prevent multiple dialogs (usually happens with rapid escape key presses)
|
||||
if (this.isShowingConfirmDialog) {
|
||||
return false;
|
||||
}
|
||||
if (!this.hasChanges || this.hasAppliedEdits) {
|
||||
|
||||
if (!this.hasUnsavedChanges) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -38,7 +38,8 @@ type RegionConvertParams = {
|
||||
};
|
||||
|
||||
class TransformManager implements EditToolManager {
|
||||
hasChanges: boolean = $derived.by(() => this.checkEdits());
|
||||
canReset: boolean = $derived.by(() => this.checkEdits());
|
||||
hasChanges: boolean = $state(false);
|
||||
|
||||
darkenLevel = $state(0.65);
|
||||
isInteracting = $state(false);
|
||||
@@ -56,7 +57,7 @@ class TransformManager implements EditToolManager {
|
||||
cropAspectRatio = $state('free');
|
||||
originalImageSize = $state<ImageDimensions>({ width: 1000, height: 1000 });
|
||||
region = $state({ x: 0, y: 0, width: 100, height: 100 });
|
||||
preveiwImgSize = $derived({
|
||||
previewImageSize = $derived({
|
||||
width: this.cropImageSize.width * this.cropImageScale,
|
||||
height: this.cropImageSize.height * this.cropImageScale,
|
||||
});
|
||||
@@ -73,6 +74,7 @@ class TransformManager implements EditToolManager {
|
||||
edits = $derived.by(() => this.getEdits());
|
||||
|
||||
setAspectRatio(aspectRatio: string) {
|
||||
this.hasChanges = true;
|
||||
this.cropAspectRatio = aspectRatio;
|
||||
|
||||
if (!this.imgElement || !this.cropAreaEl) {
|
||||
@@ -88,8 +90,8 @@ class TransformManager implements EditToolManager {
|
||||
|
||||
checkEdits() {
|
||||
return (
|
||||
Math.abs(this.preveiwImgSize.width - this.region.width) > 2 ||
|
||||
Math.abs(this.preveiwImgSize.height - this.region.height) > 2 ||
|
||||
Math.abs(this.previewImageSize.width - this.region.width) > 2 ||
|
||||
Math.abs(this.previewImageSize.height - this.region.height) > 2 ||
|
||||
this.mirrorHorizontal ||
|
||||
this.mirrorVertical ||
|
||||
this.normalizedRotation !== 0
|
||||
@@ -98,8 +100,8 @@ class TransformManager implements EditToolManager {
|
||||
|
||||
checkCropEdits() {
|
||||
return (
|
||||
Math.abs(this.preveiwImgSize.width - this.region.width) > 2 ||
|
||||
Math.abs(this.preveiwImgSize.height - this.region.height) > 2
|
||||
Math.abs(this.previewImageSize.width - this.region.width) > 2 ||
|
||||
Math.abs(this.previewImageSize.height - this.region.height) > 2
|
||||
);
|
||||
}
|
||||
|
||||
@@ -232,9 +234,12 @@ class TransformManager implements EditToolManager {
|
||||
this.originalImageSize = { width: 1000, height: 1000 };
|
||||
this.cropImageScale = 1;
|
||||
this.cropAspectRatio = 'free';
|
||||
this.hasChanges = false;
|
||||
}
|
||||
|
||||
mirror(axis: 'horizontal' | 'vertical') {
|
||||
this.hasChanges = true;
|
||||
|
||||
if (this.imageRotation % 180 !== 0) {
|
||||
axis = axis === 'horizontal' ? 'vertical' : 'horizontal';
|
||||
}
|
||||
@@ -247,6 +252,8 @@ class TransformManager implements EditToolManager {
|
||||
}
|
||||
|
||||
async rotate(angle: number) {
|
||||
this.hasChanges = true;
|
||||
|
||||
this.imageRotation += angle;
|
||||
await tick();
|
||||
this.onImageLoad();
|
||||
@@ -760,6 +767,7 @@ class TransformManager implements EditToolManager {
|
||||
return;
|
||||
}
|
||||
|
||||
this.hasChanges = true;
|
||||
const newX = Math.max(0, Math.min(mouseX - this.dragOffset.x, cropArea.clientWidth - this.region.width));
|
||||
const newY = Math.max(0, Math.min(mouseY - this.dragOffset.y, cropArea.clientHeight - this.region.height));
|
||||
|
||||
@@ -781,6 +789,7 @@ class TransformManager implements EditToolManager {
|
||||
}
|
||||
this.fadeOverlay(false);
|
||||
|
||||
this.hasChanges = true;
|
||||
const { x, y, width, height } = crop;
|
||||
const minSize = 50;
|
||||
let newRegion = { ...crop };
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
import { readonly, writable } from 'svelte/store';
|
||||
|
||||
function createPurchaseStore() {
|
||||
const isPurcharsed = writable(false);
|
||||
|
||||
function setPurchaseStatus(status: boolean) {
|
||||
isPurcharsed.set(status);
|
||||
}
|
||||
|
||||
return {
|
||||
isPurchased: readonly(isPurcharsed),
|
||||
setPurchaseStatus,
|
||||
};
|
||||
}
|
||||
|
||||
export const purchaseStore = createPurchaseStore();
|
||||
@@ -1,5 +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';
|
||||
|
||||
@@ -13,7 +12,6 @@ export const preferences = writable<UserPreferencesResponseDto>();
|
||||
export const resetSavedUser = () => {
|
||||
user.set(undefined as unknown as UserAdminResponseDto);
|
||||
preferences.set(undefined as unknown as UserPreferencesResponseDto);
|
||||
purchaseStore.setPurchaseStatus(false);
|
||||
};
|
||||
|
||||
eventManager.on({
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { browser } from '$app/environment';
|
||||
import { eventManager } from '$lib/managers/event-manager.svelte';
|
||||
import { Route } from '$lib/route';
|
||||
import { purchaseStore } from '$lib/stores/purchase.store';
|
||||
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 { getMyPreferences, getMyUser, getStorage } from '@immich/sdk';
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
import { DateTime } from 'luxon';
|
||||
import { get } from 'svelte/store';
|
||||
@@ -18,19 +17,12 @@ export const loadUser = async () => {
|
||||
try {
|
||||
let user = get(user$);
|
||||
let preferences = get(preferences$);
|
||||
let serverInfo;
|
||||
|
||||
if ((!user || !preferences) && hasAuthCookie()) {
|
||||
[user, preferences, serverInfo] = await Promise.all([getMyUser(), getMyPreferences(), getAboutInfo()]);
|
||||
[user, preferences] = await Promise.all([getMyUser(), getMyPreferences()]);
|
||||
user$.set(user);
|
||||
preferences$.set(preferences);
|
||||
|
||||
eventManager.emit('AuthUserLoaded', user);
|
||||
|
||||
// Check for license status
|
||||
if (serverInfo.licensed || user.license?.activatedAt) {
|
||||
purchaseStore.setPurchaseStatus(true);
|
||||
}
|
||||
}
|
||||
return user;
|
||||
} catch {
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
import LicenseActivationSuccess from '$lib/components/shared-components/purchasing/purchase-activation-success.svelte';
|
||||
import LicenseContent from '$lib/components/shared-components/purchasing/purchase-content.svelte';
|
||||
import SupporterBadge from '$lib/components/shared-components/side-bar/supporter-badge.svelte';
|
||||
import { authManager } from '$lib/managers/auth-manager.svelte';
|
||||
import { Route } from '$lib/route';
|
||||
import { purchaseStore } from '$lib/stores/purchase.store';
|
||||
import { Alert, Container, Stack } from '@immich/ui';
|
||||
import { mdiAlertCircleOutline } from '@mdi/js';
|
||||
import { t } from 'svelte-i18n';
|
||||
@@ -17,17 +17,16 @@
|
||||
|
||||
let { data }: Props = $props();
|
||||
let showLicenseActivated = $state(false);
|
||||
const { isPurchased } = purchaseStore;
|
||||
</script>
|
||||
|
||||
<UserPageLayout title={$t('buy')}>
|
||||
<UserPageLayout title={data.meta.title}>
|
||||
<Container size="medium" center>
|
||||
<Stack gap={4} class="mt-4">
|
||||
{#if data.isActivated === false}
|
||||
<Alert icon={mdiAlertCircleOutline} color="danger" title={$t('purchase_failed_activation')} />
|
||||
{/if}
|
||||
|
||||
{#if $isPurchased}
|
||||
{#if authManager.isPurchased}
|
||||
<SupporterBadge logoSize="lg" centered />
|
||||
{/if}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { purchaseStore } from '$lib/stores/purchase.store';
|
||||
import { authManager } from '$lib/managers/auth-manager.svelte';
|
||||
import { authenticate } from '$lib/utils/auth';
|
||||
import { getFormatter } from '$lib/utils/i18n';
|
||||
import { activateProduct, getActivationKey } from '$lib/utils/license-utils';
|
||||
@@ -21,7 +21,7 @@ export const load = (async ({ url }) => {
|
||||
const response = await activateProduct(licenseKey, activationKey);
|
||||
if (response.activatedAt !== '') {
|
||||
isActivated = true;
|
||||
purchaseStore.setPurchaseStatus(true);
|
||||
authManager.isPurchased = true;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
|
||||
Reference in New Issue
Block a user