diff --git a/package-lock.json b/package-lock.json index 22401649..f89f0a31 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1606,9 +1606,9 @@ ] }, "node_modules/@sveltejs/acorn-typescript": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.6.tgz", - "integrity": "sha512-4awhxtMh4cx9blePWl10HRHj8Iivtqj+2QdDCSMDzxG+XKa9+VCNupQuCuvzEhYPzZSrX+0gC+0lHA/0fFKKQQ==", + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.10.tgz", + "integrity": "sha512-4WfKk68eTih+MiJD4fSbxN7E8kVBmTMPWHUPYjvl2N0rMs53YLTT8/YjKU5Dtnz5LqDjl7LEw4U7lXR2W3J5WA==", "dev": true, "license": "MIT", "peerDependencies": { @@ -2399,9 +2399,9 @@ } }, "node_modules/devalue": { - "version": "5.6.4", - "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.6.4.tgz", - "integrity": "sha512-Gp6rDldRsFh/7XuouDbxMH3Mx8GMCcgzIb1pDTvNyn8pZGQ22u+Wa+lGV9dQCltFQ7uVw0MhRyb8XDskNFOReA==", + "version": "5.8.1", + "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.8.1.tgz", + "integrity": "sha512-4CXDYRBGqN+57wVJkuXBYmpAVUSg3L6JAQa/DFqm238G73E1wuyc/JhGQJzN7vUf/CMphYau2zXbfWzDR5aTEw==", "dev": true, "license": "MIT" }, @@ -2639,13 +2639,21 @@ } }, "node_modules/esrap": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/esrap/-/esrap-2.2.3.tgz", - "integrity": "sha512-8fOS+GIGCQZl/ZIlhl59htOlms6U8NvX6ZYgYHpRU/b6tVSh3uHkOHZikl3D4cMbYM0JlpBe+p/BkZEi8J9XIQ==", + "version": "2.2.9", + "resolved": "https://registry.npmjs.org/esrap/-/esrap-2.2.9.tgz", + "integrity": "sha512-4KijP+NxCWthMCUC3qHbE6n4vCjqgJS1uAYKhuT/GWfFTf1Qyive2TgOjep+gzbSzRfnNyaN/UU9YmdOt8Eg0A==", "dev": true, "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15" + }, + "peerDependencies": { + "@typescript-eslint/types": "^8.2.0" + }, + "peerDependenciesMeta": { + "@typescript-eslint/types": { + "optional": true + } } }, "node_modules/esrecurse": { @@ -3276,9 +3284,9 @@ "license": "MIT" }, "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", "dev": true, "funding": [ { @@ -3461,9 +3469,9 @@ } }, "node_modules/postcss": { - "version": "8.5.6", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", - "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", "dev": true, "funding": [ { @@ -3481,7 +3489,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.11", + "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -3956,24 +3964,24 @@ } }, "node_modules/svelte": { - "version": "5.53.7", - "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.53.7.tgz", - "integrity": "sha512-uxck1KI7JWtlfP3H6HOWi/94soAl23jsGJkBzN2BAWcQng0+lTrRNhxActFqORgnO9BHVd1hKJhG+ljRuIUWfQ==", + "version": "5.55.9", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.55.9.tgz", + "integrity": "sha512-fTjjT8cHLDwigcu2j3pv7Jq04LklXevPB8uBgyHNiTXv+RMNvVnrjS4UEYrLMkhuq1vpCodHjiW+z/95SDs/fg==", "dev": true, "license": "MIT", "dependencies": { "@jridgewell/remapping": "^2.3.4", "@jridgewell/sourcemap-codec": "^1.5.0", - "@sveltejs/acorn-typescript": "^1.0.5", + "@sveltejs/acorn-typescript": "^1.0.10", "@types/estree": "^1.0.5", "@types/trusted-types": "^2.0.7", "acorn": "^8.12.1", "aria-query": "5.3.1", "axobject-query": "^4.1.0", "clsx": "^2.1.1", - "devalue": "^5.6.3", + "devalue": "^5.8.1", "esm-env": "^1.2.1", - "esrap": "^2.2.2", + "esrap": "^2.2.9", "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", @@ -4483,9 +4491,9 @@ } }, "node_modules/ws": { - "version": "8.19.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", - "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", + "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", "license": "MIT", "optional": true, "engines": { diff --git a/src/App.svelte b/src/App.svelte index ac63b2a3..d3a0bcf0 100644 --- a/src/App.svelte +++ b/src/App.svelte @@ -2,4 +2,4 @@ import MainPage from "$/components/MainPage.svelte"; - + \ No newline at end of file diff --git a/src/components/LabelDesigner.svelte b/src/components/LabelDesigner.svelte index fc92ab80..8648b1cc 100644 --- a/src/components/LabelDesigner.svelte +++ b/src/components/LabelDesigner.svelte @@ -1,4 +1,7 @@ @@ -130,6 +158,17 @@ {/each} + + +
+ + +
{/if} {#if $printerMeta} @@ -279,7 +318,7 @@ class="btn btn-primary" disabled={$connectionState === "connecting" || (!featureSupport.capacitorBle && !featureSupport.webBluetooth && !featureSupport.webSerial)} - onclick={onConnectClicked}> + onclick={() => onConnectClicked(false)}> {/if} @@ -296,4 +335,4 @@ width: 100vw; max-width: 300px; } - + \ No newline at end of file diff --git a/src/components/designer-controls/GenericObjectParamsControls.svelte b/src/components/designer-controls/GenericObjectParamsControls.svelte index c785cd55..6fc644ad 100644 --- a/src/components/designer-controls/GenericObjectParamsControls.svelte +++ b/src/components/designer-controls/GenericObjectParamsControls.svelte @@ -4,7 +4,7 @@ import { appConfig } from "$/stores"; import MdIcon from "$/components/basic/MdIcon.svelte"; import ObjectPositionControls from "$/components/designer-controls/ObjectPositionControls.svelte"; - + import { Toasts } from "$/utils/toasts"; interface Props { selectedObject: fabric.FabricObject; @@ -14,6 +14,22 @@ let { selectedObject, editRevision, valueUpdated }: Props = $props(); + const saveQuickTransform = () => { + appConfig.update((cfg) => ({ + ...cfg, + pdfQuickTransform: { + scaleX: selectedObject.scaleX ?? 1, + scaleY: selectedObject.scaleY ?? 1, + left: selectedObject.left ?? 0, + top: selectedObject.top ?? 0, + angle: selectedObject.angle ?? 0, + originX: selectedObject.originX, + originY: selectedObject.originY, + } + })); + Toasts.message("Saved as Quick Transform for incoming PDFs"); + }; + const putToCenterV = () => { selectedObject.canvas!.centerObjectV(selectedObject); valueUpdated(); @@ -114,10 +130,14 @@ + + {/if} + \ No newline at end of file diff --git a/src/stores.ts b/src/stores.ts index 0a6614ef..ce9fff39 100644 --- a/src/stores.ts +++ b/src/stores.ts @@ -52,7 +52,10 @@ export const csvData = writablePersisted("csv_params", CsvParamsSchem userFonts.subscribe(FileUtils.loadFonts); -export const automation = readable( +// src/stores.ts + +// 1. Change `readable` to `writable` +export const automation = writable( (() => { try { return LocalStoragePersistence.loadAutomation() ?? undefined; @@ -63,6 +66,11 @@ export const automation = readable( })(), ); +// 2. Automatically save to local storage whenever it changes +automation.subscribe((value) => { + LocalStoragePersistence.saveAutomation(value); +}); + export const refreshRfidInfo = () => { const client = get(printerClient); diff --git a/src/types.ts b/src/types.ts index d7381168..469e9abb 100644 --- a/src/types.ts +++ b/src/types.ts @@ -100,6 +100,16 @@ export const AutomationPropsSchema = z.object({ startPrint: z.enum(["after_connect", "immediately"]).optional(), }); +export const PdfQuickTransformSchema = z.object({ + scaleX: z.number(), + scaleY: z.number(), + left: z.number(), + top: z.number(), + angle: z.number(), + originX: z.string(), + originY: z.string(), +}); + export const AppConfigSchema = z.object({ /** Keep image aspect ration when using "fit" button */ fitMode: z.enum(["stretch", "ratio_min", "ratio_max"]), @@ -107,6 +117,15 @@ export const AppConfigSchema = z.object({ iconListMode: z.enum(["user", "pack", "both"]), packetIntervalMs: z.number().gte(0).optional(), gridEnabled: z.boolean().optional(), + pdfQuickTransform: z.object({ + scaleX: z.number(), + scaleY: z.number(), + left: z.number(), + top: z.number(), + angle: z.number(), + originX: z.string(), + originY: z.string() + }).optional() }); export const UserIconSchema = z.object({ diff --git a/standalone-apps/capacitor/android/app/src/main/AndroidManifest.xml b/standalone-apps/capacitor/android/app/src/main/AndroidManifest.xml index 9a35dec1..f84c6254 100644 --- a/standalone-apps/capacitor/android/app/src/main/AndroidManifest.xml +++ b/standalone-apps/capacitor/android/app/src/main/AndroidManifest.xml @@ -14,10 +14,30 @@ android:theme="@style/AppTheme.NoActionBarLaunch" android:launchMode="singleTask" android:exported="true"> + + + + + + + + + + + + + + + + + + + + - - - - - - - - - + - + \ No newline at end of file diff --git a/standalone-apps/capacitor/android/app/src/main/java/ru/mmote/niimblues/MainActivity.java b/standalone-apps/capacitor/android/app/src/main/java/ru/mmote/niimblues/MainActivity.java index 181e21f6..407f68fb 100644 --- a/standalone-apps/capacitor/android/app/src/main/java/ru/mmote/niimblues/MainActivity.java +++ b/standalone-apps/capacitor/android/app/src/main/java/ru/mmote/niimblues/MainActivity.java @@ -1,5 +1,12 @@ package ru.mmote.niimblues; +import android.os.Bundle; import com.getcapacitor.BridgeActivity; -public class MainActivity extends BridgeActivity {} +public class MainActivity extends BridgeActivity { + @Override + public void onCreate(Bundle savedInstanceState) { + registerPlugin(PdfIntentPlugin.class); + super.onCreate(savedInstanceState); + } +} \ No newline at end of file diff --git a/standalone-apps/capacitor/android/app/src/main/java/ru/mmote/niimblues/PdfIntentPlugin.java b/standalone-apps/capacitor/android/app/src/main/java/ru/mmote/niimblues/PdfIntentPlugin.java new file mode 100644 index 00000000..b10bf97f --- /dev/null +++ b/standalone-apps/capacitor/android/app/src/main/java/ru/mmote/niimblues/PdfIntentPlugin.java @@ -0,0 +1,118 @@ +package ru.mmote.niimblues; + +import android.content.Intent; +import android.graphics.Bitmap; +import android.graphics.Color; +import android.graphics.pdf.PdfRenderer; +import android.net.Uri; +import android.os.ParcelFileDescriptor; +import android.util.Base64; +import android.util.Log; + +import com.getcapacitor.JSObject; +import com.getcapacitor.Plugin; +import com.getcapacitor.PluginCall; +import com.getcapacitor.PluginMethod; +import com.getcapacitor.annotation.CapacitorPlugin; + +import java.io.ByteArrayOutputStream; + +@CapacitorPlugin(name = "PdfIntent") +public class PdfIntentPlugin extends Plugin { + + // Cache the image here if Android processes it before Svelte is fully loaded + private String cachedBase64Image = null; + + @PluginMethod + public void checkIntent(PluginCall call) { + // 1. If we already caught a PDF while Svelte was booting, return it now! + if (cachedBase64Image != null) { + JSObject ret = new JSObject(); + ret.put("image", cachedBase64Image); + call.resolve(ret); + cachedBase64Image = null; // Clear cache after delivering + return; + } + + // 2. Otherwise, check the Android intent directly + Intent intent = getActivity().getIntent(); + processAndResolve(intent, call, null); + } + + @Override + protected void handleOnNewIntent(Intent intent) { + super.handleOnNewIntent(intent); + getActivity().setIntent(intent); + processAndResolve(intent, null, "onPdfReceived"); + } + + private void processAndResolve(Intent intent, PluginCall call, String eventName) { + if (intent == null) { + if (call != null) call.resolve(new JSObject()); + return; + } + + String action = intent.getAction(); + Uri pdfUri = null; + + if (Intent.ACTION_SEND.equals(action)) { + pdfUri = intent.getParcelableExtra(Intent.EXTRA_STREAM); + } else if (Intent.ACTION_VIEW.equals(action)) { + pdfUri = intent.getData(); + } + + if (pdfUri == null) { + if (call != null) call.resolve(new JSObject()); + return; + } + + Uri finalUri = pdfUri; + + new Thread(() -> { + try { + ParcelFileDescriptor fd = getContext().getContentResolver().openFileDescriptor(finalUri, "r"); + if (fd == null) throw new Exception("Android returned null FD."); + + PdfRenderer renderer = new PdfRenderer(fd); + if (renderer.getPageCount() == 0) throw new Exception("0 pages."); + + PdfRenderer.Page page = renderer.openPage(0); + + int width = 800; + int height = (int) (width * ((float) page.getHeight() / page.getWidth())); + Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); + + bitmap.eraseColor(Color.WHITE); + page.render(bitmap, null, null, PdfRenderer.Page.RENDER_MODE_FOR_PRINT); + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + bitmap.compress(Bitmap.CompressFormat.PNG, 100, baos); + String base64Image = "data:image/png;base64," + Base64.encodeToString(baos.toByteArray(), Base64.NO_WRAP); + + page.close(); + renderer.close(); + fd.close(); + + // Clear the intent so we don't process it twice + getActivity().setIntent(new Intent()); + + JSObject ret = new JSObject(); + ret.put("image", base64Image); + + // If Javascript asked for it directly via checkIntent(): + if (call != null) { + call.resolve(ret); + } + // If Android pushed it automatically via handleOnNewIntent(): + else if (eventName != null) { + cachedBase64Image = base64Image; // Cache it just in case JS misses the event + notifyListeners(eventName, ret); // Try to notify Svelte anyway + } + + } catch (Exception e) { + Log.e("PdfIntentPlugin", "PDF Error", e); + if (call != null) call.reject(e.getMessage() != null ? e.getMessage() : "Error"); + } + }).start(); + } +} \ No newline at end of file