mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-08-27 13:54:11 +02:00
Add a typed platform package to decouple the frontend from Tauri (#539)
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
0068be9ffc
commit
2383f06e71
@@ -1,5 +1,5 @@
|
||||
import { getIdentifier } from "@tauri-apps/api/app";
|
||||
import { invokeCmd } from "./tauri";
|
||||
import { platform } from "@yaakapp-internal/platform";
|
||||
import { rpc } from "./rpc";
|
||||
|
||||
export interface AppInfo {
|
||||
isDev: boolean;
|
||||
@@ -16,8 +16,8 @@ export interface AppInfo {
|
||||
}
|
||||
|
||||
export const appInfo = {
|
||||
...(await invokeCmd("cmd_metadata")),
|
||||
identifier: await getIdentifier(),
|
||||
...(await rpc("cmd_metadata")),
|
||||
identifier: await platform.appIdentifier(),
|
||||
} as AppInfo;
|
||||
|
||||
console.log("App info", appInfo);
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { clear, writeText } from "@tauri-apps/plugin-clipboard-manager";
|
||||
import { showToast } from "./toast";
|
||||
import { platform } from "@yaakapp-internal/platform";
|
||||
|
||||
export function copyToClipboard(
|
||||
text: string | null,
|
||||
{ disableToast }: { disableToast?: boolean } = {},
|
||||
) {
|
||||
if (text == null) {
|
||||
clear().catch(console.error);
|
||||
platform.clipboard.clear().catch(console.error);
|
||||
} else {
|
||||
writeText(text).catch(console.error);
|
||||
platform.clipboard.writeText(text).catch(console.error);
|
||||
}
|
||||
|
||||
if (text !== "" && !disableToast) {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { HttpRequestHeader } from "@yaakapp-internal/models";
|
||||
import { invokeCmd } from "./tauri";
|
||||
import { rpc } from "./rpc";
|
||||
|
||||
/**
|
||||
* Global default headers fetched from the backend.
|
||||
* These are static and fetched once on module load.
|
||||
*/
|
||||
export const defaultHeaders: HttpRequestHeader[] = await invokeCmd("cmd_default_headers");
|
||||
export const defaultHeaders: HttpRequestHeader[] = await rpc("cmd_default_headers");
|
||||
|
||||
@@ -2,7 +2,7 @@ import { parseTemplate } from "@yaakapp-internal/templates";
|
||||
import { activeEnvironmentIdAtom } from "../hooks/useActiveEnvironment";
|
||||
import { activeWorkspaceIdAtom } from "../hooks/useActiveWorkspace";
|
||||
import { jotaiStore } from "./jotai";
|
||||
import { invokeCmd } from "./tauri";
|
||||
import { rpc } from "./rpc";
|
||||
|
||||
export function analyzeTemplate(template: string): "global_secured" | "local_secured" | "insecure" {
|
||||
let secureTags = 0;
|
||||
@@ -39,7 +39,7 @@ export async function convertTemplateToInsecure(template: string) {
|
||||
|
||||
const workspaceId = jotaiStore.get(activeWorkspaceIdAtom) ?? "n/a";
|
||||
const environmentId = jotaiStore.get(activeEnvironmentIdAtom) ?? null;
|
||||
return invokeCmd<string>("cmd_decrypt_template", { template, workspaceId, environmentId });
|
||||
return rpc<string>("cmd_decrypt_template", { template, workspaceId, environmentId });
|
||||
}
|
||||
|
||||
export async function convertTemplateToSecure(template: string): Promise<string> {
|
||||
@@ -53,5 +53,5 @@ export async function convertTemplateToSecure(template: string): Promise<string>
|
||||
|
||||
const workspaceId = jotaiStore.get(activeWorkspaceIdAtom) ?? "n/a";
|
||||
const environmentId = jotaiStore.get(activeEnvironmentIdAtom) ?? null;
|
||||
return invokeCmd<string>("cmd_secure_template", { template, workspaceId, environmentId });
|
||||
return rpc<string>("cmd_secure_template", { template, workspaceId, environmentId });
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import vkBeautify from "vkbeautify";
|
||||
import { invokeCmd } from "./tauri";
|
||||
import { rpc } from "./rpc";
|
||||
|
||||
export async function tryFormatJson(text: string): Promise<string> {
|
||||
if (text === "") return text;
|
||||
|
||||
try {
|
||||
const result = await invokeCmd<string>("cmd_format_json", { text });
|
||||
const result = await rpc<string>("cmd_format_json", { text });
|
||||
return result;
|
||||
} catch (err) {
|
||||
console.warn("Failed to format JSON", err);
|
||||
@@ -24,7 +24,7 @@ export async function tryFormatGraphql(text: string): Promise<string> {
|
||||
if (text === "") return text;
|
||||
|
||||
try {
|
||||
return await invokeCmd<string>("cmd_format_graphql", { text });
|
||||
return await rpc<string>("cmd_format_graphql", { text });
|
||||
} catch (err) {
|
||||
console.warn("Failed to format GraphQL", err);
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import { showDialog } from "./dialog";
|
||||
import { jotaiStore } from "./jotai";
|
||||
import { pluralizeCount } from "./pluralize";
|
||||
import { router } from "./router";
|
||||
import { invokeCmd } from "./tauri";
|
||||
import { rpc } from "./rpc";
|
||||
|
||||
export const importData = createFastMutation({
|
||||
mutationKey: ["import_data"],
|
||||
@@ -50,7 +50,7 @@ export const importData = createFastMutation({
|
||||
|
||||
async function performImport(filePath: string): Promise<boolean> {
|
||||
const activeWorkspace = jotaiStore.get(activeWorkspaceAtom);
|
||||
const imported = await invokeCmd<BatchUpsertResult>("cmd_import_data", {
|
||||
const imported = await rpc<BatchUpsertResult>("cmd_import_data", {
|
||||
filePath,
|
||||
workspaceId: activeWorkspace?.id,
|
||||
});
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { emit } from "@tauri-apps/api/event";
|
||||
import { openUrl } from "@tauri-apps/plugin-opener";
|
||||
import { debounce } from "@yaakapp-internal/lib";
|
||||
import type {
|
||||
FormInput,
|
||||
@@ -20,23 +18,23 @@ import { Button } from "../components/core/Button";
|
||||
import { ButtonInfiniteLoading } from "../components/core/ButtonInfiniteLoading";
|
||||
|
||||
// Listen for toasts
|
||||
import { listenToTauriEvent } from "../hooks/useListenToTauriEvent";
|
||||
import { platform } from "@yaakapp-internal/platform";
|
||||
import { updateAvailableAtom } from "./atoms";
|
||||
import { stringToColor } from "./color";
|
||||
import { generateId } from "./generateId";
|
||||
import { jotaiStore } from "./jotai";
|
||||
import { showPrompt } from "./prompt";
|
||||
import { showPromptForm } from "./prompt-form";
|
||||
import { invokeCmd } from "./tauri";
|
||||
import { rpc } from "./rpc";
|
||||
import { showToast } from "./toast";
|
||||
|
||||
export function initGlobalListeners() {
|
||||
listenToTauriEvent<ShowToastRequest>("show_toast", (event) => {
|
||||
showToast({ ...event.payload });
|
||||
platform.listen<ShowToastRequest>("show_toast", (payload) => {
|
||||
showToast({ ...payload });
|
||||
});
|
||||
|
||||
// Show errors for any plugins that failed to load during startup
|
||||
void invokeCmd<[string, string][]>("cmd_plugin_init_errors").then((errors) => {
|
||||
void rpc<[string, string][]>("cmd_plugin_init_errors").then((errors) => {
|
||||
for (const [dir, err] of errors) {
|
||||
const name = dir.split(/[/\\]/).pop() ?? dir;
|
||||
showToast({
|
||||
@@ -61,13 +59,13 @@ export function initGlobalListeners() {
|
||||
}
|
||||
});
|
||||
|
||||
listenToTauriEvent("settings", () => openSettings.mutate(null));
|
||||
platform.listen("settings", () => openSettings.mutate(null));
|
||||
|
||||
// Track active dynamic form dialogs so follow-up input updates can reach them
|
||||
const activeForms = new Map<string, (inputs: FormInput[]) => void>();
|
||||
|
||||
// Listen for plugin events
|
||||
listenToTauriEvent<InternalEvent>("plugin_event", async ({ payload: event }) => {
|
||||
platform.listen<InternalEvent>("plugin_event", async (event) => {
|
||||
if (event.payload.type === "prompt_text_request") {
|
||||
const value = await showPrompt(event.payload);
|
||||
const result: InternalEvent = {
|
||||
@@ -81,7 +79,7 @@ export function initGlobalListeners() {
|
||||
value,
|
||||
},
|
||||
};
|
||||
await emit(event.id, result);
|
||||
await platform.emit(event.id, result);
|
||||
} else if (event.payload.type === "prompt_form_request") {
|
||||
if (event.replyId != null) {
|
||||
// Follow-up update from plugin runtime — update the active dialog's inputs
|
||||
@@ -106,7 +104,7 @@ export function initGlobalListeners() {
|
||||
done,
|
||||
},
|
||||
};
|
||||
void emit(event.id, result);
|
||||
void platform.emit(event.id, result);
|
||||
};
|
||||
|
||||
const values = await showPromptForm({
|
||||
@@ -127,24 +125,24 @@ export function initGlobalListeners() {
|
||||
}
|
||||
});
|
||||
|
||||
listenToTauriEvent<string>("update_installed", async ({ payload: version }) => {
|
||||
platform.listen<string>("update_installed", async (version) => {
|
||||
console.log("Got update installed event", version);
|
||||
showUpdateInstalledToast(version);
|
||||
});
|
||||
|
||||
// Listen for update events
|
||||
listenToTauriEvent<UpdateInfo>("update_available", async ({ payload }) => {
|
||||
platform.listen<UpdateInfo>("update_available", async (payload) => {
|
||||
console.log("Got update available", payload);
|
||||
void showUpdateAvailableToast(payload);
|
||||
});
|
||||
|
||||
listenToTauriEvent<YaakNotification>("notification", ({ payload }) => {
|
||||
platform.listen<YaakNotification>("notification", (payload) => {
|
||||
console.log("Got notification event", payload);
|
||||
showNotificationToast(payload);
|
||||
});
|
||||
|
||||
// Listen for plugin update events
|
||||
listenToTauriEvent<PluginUpdateNotification>("plugin_updates_available", ({ payload }) => {
|
||||
platform.listen<PluginUpdateNotification>("plugin_updates_available", (payload) => {
|
||||
console.log("Got plugin updates event", payload);
|
||||
showPluginUpdatesToast(payload);
|
||||
});
|
||||
@@ -171,7 +169,7 @@ function showUpdateInstalledToast(version: string) {
|
||||
loadingChildren="Restarting..."
|
||||
onClick={() => {
|
||||
hide();
|
||||
setTimeout(() => invokeCmd("cmd_restart", {}), 200);
|
||||
setTimeout(() => rpc("cmd_restart", {}), 200);
|
||||
}}
|
||||
>
|
||||
Relaunch Yaak
|
||||
@@ -187,7 +185,7 @@ async function showUpdateAvailableToast(updateInfo: UpdateInfo) {
|
||||
jotaiStore.set(updateAvailableAtom, { version, downloaded });
|
||||
|
||||
// Acknowledge the event, so we don't time out and try the fallback update logic
|
||||
await emit<UpdateResponse>(replyEventId, { type: "ack" });
|
||||
await platform.emit(replyEventId, { type: "ack" } satisfies UpdateResponse);
|
||||
|
||||
showToast({
|
||||
id: UPDATE_TOAST_ID,
|
||||
@@ -209,10 +207,10 @@ async function showUpdateAvailableToast(updateInfo: UpdateInfo) {
|
||||
className="min-w-40"
|
||||
loadingChildren={downloaded ? "Installing..." : "Downloading..."}
|
||||
onClick={async () => {
|
||||
await emit<UpdateResponse>(replyEventId, {
|
||||
await platform.emit(replyEventId, {
|
||||
type: "action",
|
||||
action: "install",
|
||||
});
|
||||
} satisfies UpdateResponse);
|
||||
}}
|
||||
>
|
||||
{downloaded ? "Install Now" : "Download and Install"}
|
||||
@@ -223,7 +221,7 @@ async function showUpdateAvailableToast(updateInfo: UpdateInfo) {
|
||||
variant="border"
|
||||
rightSlot={<Icon icon="external_link" />}
|
||||
onClick={async () => {
|
||||
await openUrl(`https://yaak.app/changelog/${version}`);
|
||||
await platform.openUrl(`https://yaak.app/changelog/${version}`);
|
||||
}}
|
||||
>
|
||||
What's New
|
||||
@@ -304,7 +302,7 @@ function showNotificationToast(n: YaakNotification) {
|
||||
</VStack>
|
||||
),
|
||||
onClose: () => {
|
||||
invokeCmd("cmd_dismiss_notification", { notificationId: n.id }).catch(console.error);
|
||||
rpc("cmd_dismiss_notification", { notificationId: n.id }).catch(console.error);
|
||||
},
|
||||
action: ({ hide }) => {
|
||||
return actionLabel && actionUrl ? (
|
||||
@@ -315,7 +313,7 @@ function showNotificationToast(n: YaakNotification) {
|
||||
rightSlot={<Icon icon="external_link" />}
|
||||
onClick={() => {
|
||||
hide();
|
||||
return openUrl(actionUrl);
|
||||
return platform.openUrl(actionUrl);
|
||||
}}
|
||||
>
|
||||
{actionLabel}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { save } from "@tauri-apps/plugin-dialog";
|
||||
import { Icon } from "@yaakapp-internal/ui";
|
||||
import mime from "mime";
|
||||
import { createElement } from "react";
|
||||
@@ -7,8 +6,9 @@ import type { SniffedValue } from "../components/core/Editor/sniffValue";
|
||||
import { isEncodedRun } from "../components/core/Editor/sniffValue";
|
||||
import { copyToClipboard } from "./copy";
|
||||
import { fireAndForget } from "./fireAndForget";
|
||||
import { invokeCmd } from "./tauri";
|
||||
import { rpc } from "./rpc";
|
||||
import { showToast } from "./toast";
|
||||
import { platform } from "@yaakapp-internal/platform";
|
||||
|
||||
/**
|
||||
* How the value is written, which is the thing worth knowing about it — that it is base64
|
||||
@@ -229,7 +229,7 @@ function toBase64(bytes: Uint8Array): string {
|
||||
*/
|
||||
export async function saveValue(text: string, sniffed: SniffedValue | null, name: string) {
|
||||
const ext = sniffed == null ? "txt" : (mime.getExtension(sniffed.mime) ?? "bin");
|
||||
const filepath = await save({ defaultPath: `${name}.${ext}`, title: "Save Value" });
|
||||
const filepath = await platform.dialog.save({ defaultPath: `${name}.${ext}`, title: "Save Value" });
|
||||
if (filepath == null) {
|
||||
return; // Cancelled
|
||||
}
|
||||
@@ -241,6 +241,6 @@ export async function saveValue(text: string, sniffed: SniffedValue | null, name
|
||||
? normalizeBase64(payloadOf(text, sniffed))
|
||||
: toBase64(decodeValue(text, sniffed));
|
||||
|
||||
await invokeCmd("cmd_save_base64_to_binary", { filepath, data });
|
||||
await rpc("cmd_save_base64_to_binary", { filepath, data });
|
||||
showToast({ message: `Saved to ${filepath}` });
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { readFile } from "@tauri-apps/plugin-fs";
|
||||
import type { HttpResponse } from "@yaakapp-internal/models";
|
||||
import type { FilterResponse } from "@yaakapp-internal/plugins";
|
||||
import type { ServerSentEvent, SseSummary } from "@yaakapp-internal/sse";
|
||||
import { candidateJsonPayloadsFromSseText, computeSseSummary } from "@yaakapp-internal/sse";
|
||||
import { invokeCmd } from "./tauri";
|
||||
import { rpc } from "./rpc";
|
||||
import { platform } from "@yaakapp-internal/platform";
|
||||
|
||||
export async function getResponseBodyText({
|
||||
response,
|
||||
@@ -12,7 +12,7 @@ export async function getResponseBodyText({
|
||||
response: HttpResponse;
|
||||
filter: string | null;
|
||||
}): Promise<string | null> {
|
||||
const result = await invokeCmd<FilterResponse>("cmd_http_response_body", {
|
||||
const result = await rpc<FilterResponse>("cmd_http_response_body", {
|
||||
response,
|
||||
filter,
|
||||
});
|
||||
@@ -29,7 +29,7 @@ export async function getResponseBodyEventSource(
|
||||
): Promise<ServerSentEvent[]> {
|
||||
if (!response.bodyPath) return [];
|
||||
try {
|
||||
const events = await invokeCmd<ServerSentEvent[]>("cmd_get_sse_events", {
|
||||
const events = await rpc<ServerSentEvent[]>("cmd_get_sse_events", {
|
||||
filePath: response.bodyPath,
|
||||
});
|
||||
if (events.length > 0) {
|
||||
@@ -39,7 +39,7 @@ export async function getResponseBodyEventSource(
|
||||
// Fall back to raw JSON frame parsing for non-standard SSE-like responses.
|
||||
}
|
||||
|
||||
const bytes = await readFile(response.bodyPath);
|
||||
const bytes = await platform.files.readFile(response.bodyPath);
|
||||
const text = new TextDecoder("utf-8").decode(bytes);
|
||||
return candidateJsonPayloadsFromSseText(text).map((data, index) => ({
|
||||
data,
|
||||
@@ -55,7 +55,7 @@ export async function getResponseBodySseSummary(
|
||||
): Promise<SseSummary> {
|
||||
if (!response.bodyPath) return { fragmentCount: 0, summary: "" };
|
||||
|
||||
const bytes = await readFile(response.bodyPath);
|
||||
const bytes = await platform.files.readFile(response.bodyPath);
|
||||
const text = new TextDecoder("utf-8").decode(bytes);
|
||||
return computeSseSummary(text, resultKeyPath);
|
||||
}
|
||||
@@ -64,5 +64,5 @@ export async function getResponseBodyBytes(
|
||||
response: HttpResponse,
|
||||
): Promise<Uint8Array<ArrayBuffer> | null> {
|
||||
if (!response.bodyPath) return null;
|
||||
return readFile(response.bodyPath);
|
||||
return platform.files.readFile(response.bodyPath);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { type } from "@tauri-apps/plugin-os";
|
||||
import { platform } from "@yaakapp-internal/platform";
|
||||
|
||||
const os = type();
|
||||
const os = platform.osType();
|
||||
export const revealInFinderText =
|
||||
os === "macos"
|
||||
? "Reveal in Finder"
|
||||
|
||||
@@ -1,7 +1,15 @@
|
||||
import type { InvokeArgs } from "@tauri-apps/api/core";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import type { RpcPayload } from "@yaakapp-internal/platform";
|
||||
import { platform } from "@yaakapp-internal/platform";
|
||||
|
||||
type TauriCmd =
|
||||
/**
|
||||
* Every backend command the client sends.
|
||||
*
|
||||
* Listing them keeps typos out and gives us the inventory to check the Rust
|
||||
* side against. Once the app's commands move onto `RpcRouter`, this union is
|
||||
* replaced by the generated `RpcSchema` and the payload and result types come
|
||||
* with it, the way `apps/yaak-proxy/lib/rpc.ts` already works.
|
||||
*/
|
||||
type AppCmd =
|
||||
| "cmd_call_grpc_request_action"
|
||||
| "cmd_call_http_authentication_action"
|
||||
| "cmd_call_http_request_action"
|
||||
@@ -53,14 +61,15 @@ type TauriCmd =
|
||||
| "cmd_send_http_request"
|
||||
| "cmd_template_function_summaries"
|
||||
| "cmd_template_function_config"
|
||||
| "cmd_template_tokens_to_string";
|
||||
| "cmd_template_tokens_to_string"
|
||||
| "models_get_graphql_introspection"
|
||||
| "models_get_settings"
|
||||
| "models_grpc_events"
|
||||
| "models_upsert_graphql_introspection"
|
||||
| "models_websocket_events"
|
||||
| "plugin:yaak-license|check";
|
||||
|
||||
export async function invokeCmd<T>(cmd: TauriCmd, args?: InvokeArgs): Promise<T> {
|
||||
// console.log('RUN COMMAND', cmd, args);
|
||||
try {
|
||||
return await invoke(cmd, args);
|
||||
} catch (err) {
|
||||
console.warn("Tauri command error", cmd, err);
|
||||
throw err;
|
||||
}
|
||||
/** Call a backend command. */
|
||||
export function rpc<T>(cmd: AppCmd, payload?: RpcPayload): Promise<T> {
|
||||
return platform.rpc<T>(cmd, payload);
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { HttpRequest, HttpResponse } from "@yaakapp-internal/models";
|
||||
import { getActiveCookieJar } from "../hooks/useActiveCookieJar";
|
||||
import { invokeCmd } from "./tauri";
|
||||
import { rpc } from "./rpc";
|
||||
|
||||
export async function sendEphemeralRequest(
|
||||
request: HttpRequest,
|
||||
@@ -8,7 +8,7 @@ export async function sendEphemeralRequest(
|
||||
): Promise<HttpResponse> {
|
||||
// Remove some things that we don't want to associate
|
||||
const newRequest = { ...request };
|
||||
return invokeCmd("cmd_send_ephemeral_request", {
|
||||
return rpc("cmd_send_ephemeral_request", {
|
||||
request: newRequest,
|
||||
environmentId,
|
||||
cookieJarId: getActiveCookieJar()?.id,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import type { Settings } from "@yaakapp-internal/models";
|
||||
import { rpc } from "./rpc";
|
||||
|
||||
export function getSettings(): Promise<Settings> {
|
||||
return invoke<Settings>("models_get_settings");
|
||||
return rpc<Settings>("models_get_settings");
|
||||
}
|
||||
|
||||
@@ -5,10 +5,10 @@ import {
|
||||
resolveAppearance,
|
||||
type Appearance,
|
||||
} from "@yaakapp-internal/theme";
|
||||
import { invokeCmd } from "./tauri";
|
||||
import { rpc } from "./rpc";
|
||||
|
||||
export async function getThemes() {
|
||||
const themes = (await invokeCmd<GetThemesResponse[]>("cmd_get_themes")).flatMap((t) => t.themes);
|
||||
const themes = (await rpc<GetThemesResponse[]>("cmd_get_themes")).flatMap((t) => t.themes);
|
||||
themes.sort((a, b) => a.label.localeCompare(b.label));
|
||||
// Remove duplicates, in case multiple plugins provide the same theme
|
||||
const uniqueThemes = Array.from(new Map(themes.map((t) => [t.id, t])).values());
|
||||
|
||||
Reference in New Issue
Block a user