mirror of
https://github.com/immich-app/immich.git
synced 2026-02-02 02:04:47 -08:00
Compare commits
4 Commits
renovate/f
...
fix/databa
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
452943f342 | ||
|
|
87f389afb4 | ||
|
|
ab2036f289 | ||
|
|
9208512648 |
@@ -1,6 +1,6 @@
|
||||
[tools]
|
||||
terragrunt = "0.98.0"
|
||||
opentofu = "1.11.4"
|
||||
opentofu = "1.10.7"
|
||||
|
||||
[tasks."tg:fmt"]
|
||||
run = "terragrunt hclfmt"
|
||||
|
||||
@@ -88,7 +88,7 @@ The easiest option is to have both extensions installed during the migration:
|
||||
<details>
|
||||
<summary>Migration steps (automatic)</summary>
|
||||
1. Ensure you still have pgvecto.rs installed
|
||||
2. Install `pgvector` (`>= 0.7, < 0.9`). The easiest way to do this is on Debian/Ubuntu by adding the [PostgreSQL Apt repository][pg-apt] and then running `apt install postgresql-NN-pgvector`, where `NN` is your Postgres version (e.g., `16`)
|
||||
2. Install `pgvector` (`>= 0.7.0, < 1.0.0`). The easiest way to do this is on Debian/Ubuntu by adding the [PostgreSQL Apt repository][pg-apt] and then running `apt install postgresql-NN-pgvector`, where `NN` is your Postgres version (e.g., `16`)
|
||||
3. [Install VectorChord][vchord-install]
|
||||
4. Add `shared_preload_libraries= 'vchord.so, vectors.so'` to your `postgresql.conf`, making sure to include _both_ `vchord.so` and `vectors.so`. You may include other libraries here as well if needed
|
||||
5. Restart the Postgres database
|
||||
|
||||
@@ -15,11 +15,11 @@ config_roots = [
|
||||
|
||||
[tools]
|
||||
node = "24.13.0"
|
||||
flutter = "3.38.8"
|
||||
flutter = "3.35.7"
|
||||
pnpm = "10.28.0"
|
||||
terragrunt = "0.98.0"
|
||||
opentofu = "1.11.4"
|
||||
java = "21.0.2"
|
||||
opentofu = "1.10.7"
|
||||
java = "25.0.1"
|
||||
|
||||
[tools."github:CQLabs/homebrew-dcm"]
|
||||
version = "1.30.0"
|
||||
|
||||
3
mobile/.fvmrc
Normal file
3
mobile/.fvmrc
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"flutter": "3.35.7"
|
||||
}
|
||||
5
mobile/.gitignore
vendored
5
mobile/.gitignore
vendored
@@ -55,5 +55,8 @@ 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,9 +2,7 @@
|
||||
"dart.flutterSdkPath": ".fvm/versions/3.35.7",
|
||||
"dart.lineLength": 120,
|
||||
"[dart]": {
|
||||
"editor.rulers": [
|
||||
120
|
||||
]
|
||||
"editor.rulers": [120]
|
||||
},
|
||||
"search.exclude": {
|
||||
"**/.fvm": true
|
||||
|
||||
@@ -4,12 +4,10 @@ The Immich mobile app is a Flutter-based solution leveraging the Isar Database f
|
||||
|
||||
## Setup
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
## Translation
|
||||
|
||||
@@ -31,7 +29,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: null,
|
||||
latestVersion: ServerVersion(major: 0, minor: 0, patch: 0),
|
||||
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 regardless of if they are an admin
|
||||
// using isClientOutOfDate since that will show to users reguardless 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,52 +170,50 @@ class AppBarServerInfo extends HookConsumerWidget {
|
||||
),
|
||||
],
|
||||
),
|
||||
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,
|
||||
),
|
||||
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,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
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,7 +414,6 @@ class LoginForm extends HookConsumerWidget {
|
||||
keyboardAction: TextInputAction.next,
|
||||
keyboardType: TextInputType.url,
|
||||
autofillHints: const [AutofillHints.url],
|
||||
autoCorrect: false,
|
||||
onSubmit: (ctx, _) => ImmichForm.of(ctx).submit(),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -12,7 +12,6 @@ class ImmichTextInput extends StatefulWidget {
|
||||
final List<String>? autofillHints;
|
||||
final Widget? suffixIcon;
|
||||
final bool obscureText;
|
||||
final bool autoCorrect;
|
||||
|
||||
const ImmichTextInput({
|
||||
super.key,
|
||||
@@ -27,7 +26,6 @@ class ImmichTextInput extends StatefulWidget {
|
||||
this.autofillHints,
|
||||
this.suffixIcon,
|
||||
this.obscureText = false,
|
||||
this.autoCorrect = true,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -81,7 +79,6 @@ 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('onWebsocketConnection', () => {
|
||||
describe('onWebsocketConnectionEvent', () => {
|
||||
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,12 +143,5 @@ 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,12 +105,6 @@ 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));
|
||||
|
||||
@@ -59,6 +59,7 @@ export async function buildPostgresLaunchArguments(
|
||||
): Promise<{
|
||||
bin: string;
|
||||
args: string[];
|
||||
databaseUsername: string;
|
||||
databasePassword: string;
|
||||
databaseVersion: string;
|
||||
databaseMajorVersion?: number;
|
||||
@@ -73,6 +74,7 @@ export async function buildPostgresLaunchArguments(
|
||||
const databaseMajorVersion = databaseSemver?.major;
|
||||
|
||||
const args: string[] = [];
|
||||
let databaseUsername;
|
||||
|
||||
if (isUrlConnection) {
|
||||
if (bin !== 'pg_dump') {
|
||||
@@ -85,18 +87,20 @@ export async function buildPostgresLaunchArguments(
|
||||
// remove known bad parameters
|
||||
parsedUrl.searchParams.delete('uselibpqcompat');
|
||||
|
||||
if (options.username) {
|
||||
parsedUrl.username = options.username;
|
||||
}
|
||||
|
||||
databaseUsername = parsedUrl.username;
|
||||
url = parsedUrl.toString();
|
||||
}
|
||||
|
||||
// assume typical values if we can't parse URL or not present
|
||||
databaseUsername ??= 'postgres';
|
||||
|
||||
args.push(url);
|
||||
} else {
|
||||
databaseUsername = databaseConfig.username;
|
||||
|
||||
args.push(
|
||||
'--username',
|
||||
options.username ?? databaseConfig.username,
|
||||
databaseConfig.username,
|
||||
'--host',
|
||||
databaseConfig.host,
|
||||
'--port',
|
||||
@@ -151,6 +155,7 @@ export async function buildPostgresLaunchArguments(
|
||||
return {
|
||||
bin: `/usr/lib/postgresql/${databaseMajorVersion}/bin/${bin}`,
|
||||
args,
|
||||
databaseUsername,
|
||||
databasePassword: isUrlConnection ? new URL(databaseConfig.url).password : databaseConfig.password,
|
||||
databaseVersion,
|
||||
databaseMajorVersion,
|
||||
@@ -207,44 +212,35 @@ const SQL_DROP_CONNECTIONS = `
|
||||
AND pid <> pg_backend_pid();
|
||||
`;
|
||||
|
||||
const SQL_RESET_SCHEMA = `
|
||||
const SQL_RESET_SCHEMA = (username: string) => `
|
||||
-- re-create the default schema
|
||||
DROP SCHEMA public CASCADE;
|
||||
CREATE SCHEMA public;
|
||||
|
||||
-- restore access to schema
|
||||
GRANT ALL ON SCHEMA public TO postgres;
|
||||
GRANT ALL ON SCHEMA public TO "${username}";
|
||||
GRANT ALL ON SCHEMA public TO public;
|
||||
`;
|
||||
|
||||
async function* sql(inputStream: Readable, isPgClusterDump: boolean) {
|
||||
async function* sql(inputStream: Readable, databaseUsername: string, isPgClusterDump: boolean) {
|
||||
yield SQL_DROP_CONNECTIONS;
|
||||
yield isPgClusterDump
|
||||
? String.raw`
|
||||
? // it is likely the dump contains SQL to try to drop the currently active
|
||||
// database to ensure we have a fresh slate; if the `postgres` database exists
|
||||
// then prefer to switch before continuing otherwise this will just silently fail
|
||||
String.raw`
|
||||
\c postgres
|
||||
`
|
||||
: SQL_RESET_SCHEMA;
|
||||
: SQL_RESET_SCHEMA(databaseUsername);
|
||||
|
||||
for await (const chunk of inputStream) {
|
||||
yield chunk;
|
||||
}
|
||||
}
|
||||
|
||||
async function* sqlRollback(inputStream: Readable, isPgClusterDump: boolean) {
|
||||
async function* sqlRollback(inputStream: Readable, databaseUsername: string) {
|
||||
yield SQL_DROP_CONNECTIONS;
|
||||
|
||||
if (isPgClusterDump) {
|
||||
yield String.raw`
|
||||
-- try to create database
|
||||
-- may fail but script will continue running
|
||||
CREATE DATABASE immich;
|
||||
|
||||
-- switch to database / newly created database
|
||||
\c immich
|
||||
`;
|
||||
}
|
||||
|
||||
yield SQL_RESET_SCHEMA;
|
||||
yield SQL_RESET_SCHEMA(databaseUsername);
|
||||
|
||||
for await (const chunk of inputStream) {
|
||||
yield chunk;
|
||||
@@ -273,12 +269,11 @@ export async function restoreDatabaseBackup(
|
||||
isPgClusterDump = true;
|
||||
}
|
||||
|
||||
const { bin, args, databasePassword, databaseMajorVersion } = await buildPostgresLaunchArguments(
|
||||
const { bin, args, databaseUsername, databasePassword, databaseMajorVersion } = await buildPostgresLaunchArguments(
|
||||
{ logger, database: databaseRepository, ...pgRepos },
|
||||
'psql',
|
||||
{
|
||||
singleTransaction: !isPgClusterDump,
|
||||
username: isPgClusterDump ? 'postgres' : undefined,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -301,7 +296,7 @@ export async function restoreDatabaseBackup(
|
||||
inputStream = storage.createPlainReadStream(backupFilePath);
|
||||
}
|
||||
|
||||
const sqlStream = Readable.from(sql(inputStream, isPgClusterDump));
|
||||
const sqlStream = Readable.from(sql(inputStream, databaseUsername, isPgClusterDump));
|
||||
const psql = processRepository.spawnDuplexStream(bin, args, {
|
||||
env: {
|
||||
PATH: process.env.PATH,
|
||||
@@ -332,7 +327,7 @@ export async function restoreDatabaseBackup(
|
||||
fileStream.pipe(gunzip);
|
||||
inputStream = gunzip;
|
||||
|
||||
const sqlStream = Readable.from(sqlRollback(inputStream, isPgClusterDump));
|
||||
const sqlStream = Readable.from(sqlRollback(inputStream, databaseUsername));
|
||||
const psql = processRepository.spawnDuplexStream(bin, args, {
|
||||
env: {
|
||||
PATH: process.env.PATH,
|
||||
|
||||
@@ -22,3 +22,5 @@
|
||||
return assetViewerManager.on(events);
|
||||
});
|
||||
</script>
|
||||
|
||||
const event = name.slice(2) as keyof Events;
|
||||
|
||||
@@ -194,7 +194,9 @@
|
||||
|
||||
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.canReset}
|
||||
disabled={!editManager.hasChanges}
|
||||
class="self-start"
|
||||
shape="round"
|
||||
size="small"
|
||||
|
||||
@@ -15,7 +15,6 @@ export interface EditToolManager {
|
||||
onDeactivate: () => void;
|
||||
resetAllChanges: () => Promise<void>;
|
||||
hasChanges: boolean;
|
||||
canReset: boolean;
|
||||
edits: EditAction[];
|
||||
}
|
||||
|
||||
@@ -42,22 +41,19 @@ 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.hasUnsavedChanges) {
|
||||
if (!this.hasChanges || this.hasAppliedEdits) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -38,8 +38,7 @@ type RegionConvertParams = {
|
||||
};
|
||||
|
||||
class TransformManager implements EditToolManager {
|
||||
canReset: boolean = $derived.by(() => this.checkEdits());
|
||||
hasChanges: boolean = $state(false);
|
||||
hasChanges: boolean = $derived.by(() => this.checkEdits());
|
||||
|
||||
darkenLevel = $state(0.65);
|
||||
isInteracting = $state(false);
|
||||
@@ -57,7 +56,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 });
|
||||
previewImageSize = $derived({
|
||||
preveiwImgSize = $derived({
|
||||
width: this.cropImageSize.width * this.cropImageScale,
|
||||
height: this.cropImageSize.height * this.cropImageScale,
|
||||
});
|
||||
@@ -74,7 +73,6 @@ class TransformManager implements EditToolManager {
|
||||
edits = $derived.by(() => this.getEdits());
|
||||
|
||||
setAspectRatio(aspectRatio: string) {
|
||||
this.hasChanges = true;
|
||||
this.cropAspectRatio = aspectRatio;
|
||||
|
||||
if (!this.imgElement || !this.cropAreaEl) {
|
||||
@@ -90,8 +88,8 @@ class TransformManager implements EditToolManager {
|
||||
|
||||
checkEdits() {
|
||||
return (
|
||||
Math.abs(this.previewImageSize.width - this.region.width) > 2 ||
|
||||
Math.abs(this.previewImageSize.height - this.region.height) > 2 ||
|
||||
Math.abs(this.preveiwImgSize.width - this.region.width) > 2 ||
|
||||
Math.abs(this.preveiwImgSize.height - this.region.height) > 2 ||
|
||||
this.mirrorHorizontal ||
|
||||
this.mirrorVertical ||
|
||||
this.normalizedRotation !== 0
|
||||
@@ -100,8 +98,8 @@ class TransformManager implements EditToolManager {
|
||||
|
||||
checkCropEdits() {
|
||||
return (
|
||||
Math.abs(this.previewImageSize.width - this.region.width) > 2 ||
|
||||
Math.abs(this.previewImageSize.height - this.region.height) > 2
|
||||
Math.abs(this.preveiwImgSize.width - this.region.width) > 2 ||
|
||||
Math.abs(this.preveiwImgSize.height - this.region.height) > 2
|
||||
);
|
||||
}
|
||||
|
||||
@@ -234,12 +232,9 @@ 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';
|
||||
}
|
||||
@@ -252,8 +247,6 @@ class TransformManager implements EditToolManager {
|
||||
}
|
||||
|
||||
async rotate(angle: number) {
|
||||
this.hasChanges = true;
|
||||
|
||||
this.imageRotation += angle;
|
||||
await tick();
|
||||
this.onImageLoad();
|
||||
@@ -767,7 +760,6 @@ 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));
|
||||
|
||||
@@ -789,7 +781,6 @@ class TransformManager implements EditToolManager {
|
||||
}
|
||||
this.fadeOverlay(false);
|
||||
|
||||
this.hasChanges = true;
|
||||
const { x, y, width, height } = crop;
|
||||
const minSize = 50;
|
||||
let newRegion = { ...crop };
|
||||
|
||||
Reference in New Issue
Block a user