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:
Gregory Schier
2026-08-14 12:44:01 -07:00
committed by GitHub
co-authored by Claude Opus 5
parent 0068be9ffc
commit 2383f06e71
107 changed files with 954 additions and 505 deletions
@@ -1,4 +1,3 @@
import { open } from "@tauri-apps/plugin-dialog";
import { gitClone } from "@yaakapp-internal/git";
import { Banner, VStack } from "@yaakapp-internal/ui";
import { useState } from "react";
@@ -11,6 +10,7 @@ import { Checkbox } from "./core/Checkbox";
import { IconButton } from "./core/IconButton";
import { PlainInput } from "./core/PlainInput";
import { promptCredentials } from "./git/credentials";
import { platform } from "@yaakapp-internal/platform";
interface Props {
hide: () => void;
@@ -38,7 +38,7 @@ export function CloneGitRepositoryDialog({ hide }: Props) {
hasSubdirectory && subdirectory ? `${directory}${sep}${subdirectory}` : directory;
const handleSelectDirectory = async () => {
const dir = await open({
const dir = await platform.dialog.open({
title: "Select Directory",
directory: true,
multiple: false,
@@ -1,11 +1,11 @@
import { invoke } from "@tauri-apps/api/core";
import { openUrl } from "@tauri-apps/plugin-opener";
import type { LicenseCheckStatus } from "@yaakapp-internal/license";
import { useCallback, useEffect, useRef, useState } from "react";
import { useKeyValue } from "../hooks/useKeyValue";
import { appInfo } from "../lib/appInfo";
import { pricingUrl } from "../lib/pricingUrl";
import { DismissibleBanner } from "./core/DismissibleBanner";
import { rpc } from "../lib/rpc";
import { platform } from "@yaakapp-internal/platform";
const COMMERCIAL_USE_SNOOZE_MS = 7 * 24 * 60 * 60 * 1000;
const COMMERCIAL_USE_BANNER_MESSAGE =
@@ -95,7 +95,7 @@ async function shouldShowCommercialUsePrompt(): Promise<boolean> {
}
try {
const license = await invoke<LicenseCheckStatus>("plugin:yaak-license|check");
const license = await rpc<LicenseCheckStatus>("plugin:yaak-license|check");
return license.status === "personal_use";
} catch (err) {
console.log("Failed to check license before commercial-use prompt", err);
@@ -104,7 +104,7 @@ async function shouldShowCommercialUsePrompt(): Promise<boolean> {
}
async function openCommercialUsePricing(source: string): Promise<void> {
await openUrl(pricingUrl(`app.commercial-use.${source}`)).catch(console.error);
await platform.openUrl(pricingUrl(`app.commercial-use.${source}`)).catch(console.error);
}
function isSnoozed(value: string | null, ms: number): boolean {
@@ -5,7 +5,7 @@ import { VStack } from "@yaakapp-internal/ui";
import { useState } from "react";
import { router } from "../lib/router";
import { setupOrConfigureEncryption } from "../lib/setupOrConfigureEncryption";
import { invokeCmd } from "../lib/tauri";
import { rpc } from "../lib/rpc";
import { showErrorToast } from "../lib/toast";
import { Button } from "./core/Button";
import { Checkbox } from "./core/Checkbox";
@@ -39,7 +39,7 @@ export function CreateWorkspaceDialog({ hide }: Props) {
// Do getWorkspaceMeta instead of naively creating one because it might have
// been created already when the store refreshes the workspace meta after
const workspaceMeta = await invokeCmd<WorkspaceMeta>("cmd_get_workspace_meta", {
const workspaceMeta = await rpc<WorkspaceMeta>("cmd_get_workspace_meta", {
workspaceId,
});
await updateModel({
@@ -1,4 +1,3 @@
import { save } from "@tauri-apps/plugin-dialog";
import type { Workspace } from "@yaakapp-internal/models";
import { workspacesAtom } from "@yaakapp-internal/models";
import { HStack, VStack } from "@yaakapp-internal/ui";
@@ -7,12 +6,13 @@ import { useCallback, useMemo, useState } from "react";
import slugify from "slugify";
import { activeWorkspaceAtom } from "../hooks/useActiveWorkspace";
import { pluralizeCount } from "../lib/pluralize";
import { invokeCmd } from "../lib/tauri";
import { rpc } from "../lib/rpc";
import { CommercialUseBanner } from "./CommercialUseBanner";
import { Button } from "./core/Button";
import { Checkbox } from "./core/Checkbox";
import { DetailsBanner } from "./core/DetailsBanner";
import { Link } from "./core/Link";
import { platform } from "@yaakapp-internal/platform";
interface Props {
onHide: () => void;
@@ -65,7 +65,7 @@ function ExportDataDialogContent({
const ids = Object.keys(selectedWorkspaces).filter((k) => selectedWorkspaces[k]);
const workspace = ids.length === 1 ? workspaces.find((w) => w.id === ids[0]) : undefined;
const slug = workspace ? slugify(workspace.name, { lower: true }) : "workspaces";
const exportPath = await save({
const exportPath = await platform.dialog.save({
title: "Export Data",
defaultPath: `yaak.${slug}.json`,
});
@@ -73,7 +73,7 @@ function ExportDataDialogContent({
return;
}
await invokeCmd("cmd_export_data", {
await rpc("cmd_export_data", {
workspaceIds: ids,
exportPath,
includePrivateEnvironments: includePrivateEnvironments,
@@ -2,7 +2,7 @@ import { HStack, VStack } from "@yaakapp-internal/ui";
import { useRef, useState } from "react";
import type { FeedbackFeature } from "../lib/featureFeedbackConstants";
import { FEEDBACK_FEATURES } from "../lib/featureFeedbackConstants";
import { invokeCmd } from "../lib/tauri";
import { rpc } from "../lib/rpc";
import { hideToastById, showToast } from "../lib/toast";
import { Button } from "./core/Button";
import { Input } from "./core/Input";
@@ -31,7 +31,7 @@ export function FeedbackToast({ feature, onDone }: Props) {
onDone();
// Fire-and-forget; failures are intentionally ignored
invokeCmd("cmd_send_feedback", { feature, text: trimmedText }).catch(() => {});
rpc("cmd_send_feedback", { feature, text: trimmedText }).catch(() => {});
showToast({
id: `feature-feedback-${feature}`,
timeout: 3000,
@@ -1,4 +1,3 @@
import { open } from "@tauri-apps/plugin-dialog";
import type { GrpcRequest } from "@yaakapp-internal/models";
import { Banner, HStack, Icon, InlineCode, VStack } from "@yaakapp-internal/ui";
import { useActiveRequest } from "../hooks/useActiveRequest";
@@ -8,6 +7,7 @@ import { pluralizeCount } from "../lib/pluralize";
import { Button } from "./core/Button";
import { IconButton } from "./core/IconButton";
import { Link } from "./core/Link";
import { platform } from "@yaakapp-internal/platform";
interface Props {
onDone: () => void;
@@ -45,7 +45,7 @@ function GrpcProtoSelectionDialogWithRequest({ request }: Props & { request: Grp
color="primary"
variant="border"
onClick={async () => {
const selected = await open({
const selected = await platform.dialog.open({
title: "Select Proto Files",
multiple: true,
filters: [{ name: "Proto Files", extensions: ["proto"] }],
@@ -63,7 +63,7 @@ function GrpcProtoSelectionDialogWithRequest({ request }: Props & { request: Grp
variant="border"
color="primary"
onClick={async () => {
const selected = await open({
const selected = await platform.dialog.open({
title: "Select Proto Directory",
directory: true,
});
+6 -7
View File
@@ -1,5 +1,4 @@
import { isTauri } from "@tauri-apps/api/core";
import { readText } from "@tauri-apps/plugin-clipboard-manager";
import { platform, useCapability } from "@yaakapp-internal/platform";
import { Icon } from "@yaakapp-internal/ui";
import * as m from "motion/react-m";
import { useEffect, useState } from "react";
@@ -9,12 +8,12 @@ import { showToast } from "../lib/toast";
import { Button } from "./core/Button";
/**
* Offers to create a request from a Curl command on the clipboard. The desktop app can
* read the clipboard whenever the window is focused, but a browser can't without
* prompting for permission, so it waits for the user to paste instead.
* Offers to create a request from a Curl command on the clipboard. A host that can read
* the clipboard on its own offers it whenever the window is focused; one that would have
* to prompt for permission first waits for the user to paste instead.
*/
export function ImportCurl() {
return isTauri() ? <ImportCurlButton /> : <ImportCurlOnPaste />;
return useCapability("clipboardRead") ? <ImportCurlButton /> : <ImportCurlOnPaste />;
}
function ImportCurlButton() {
@@ -24,7 +23,7 @@ function ImportCurlButton() {
// oxlint-disable-next-line react-hooks/exhaustive-deps -- none
useEffect(() => {
void readText().then(setClipboardText);
void platform.clipboard.readText().then(setClipboardText);
}, [focused]);
if (!looksLikeCurl(clipboardText)) {
+3 -3
View File
@@ -1,4 +1,3 @@
import { openUrl } from "@tauri-apps/plugin-opener";
import type { LicenseCheckStatus } from "@yaakapp-internal/license";
import { useLicense } from "@yaakapp-internal/license";
import { settingsAtom } from "@yaakapp-internal/models";
@@ -14,6 +13,7 @@ import type { ButtonProps } from "./core/Button";
import { Dropdown, type DropdownItem } from "./core/Dropdown";
import { Icon } from "@yaakapp-internal/ui";
import { PillButton } from "./core/PillButton";
import { platform } from "@yaakapp-internal/platform";
const dismissedAtom = atomWithKVStorage<string | null>("dismissed_license_expired", null);
@@ -52,7 +52,7 @@ function getDetail(
leftSlot: <Icon icon="gift" />,
rightSlot: <Icon icon="external_link" size="sm" className="opacity-disabled" />,
hidden: data.data.changes === 0 || data.data.changesUrl == null,
onSelect: () => openUrl(data.data.changesUrl ?? ""),
onSelect: () => platform.openUrl(data.data.changesUrl ?? ""),
},
{
type: "separator",
@@ -63,7 +63,7 @@ function getDetail(
leftSlot: <Icon icon="refresh" />,
rightSlot: <Icon icon="external_link" size="sm" className="opacity-disabled" />,
hidden: data.data.changesUrl == null,
onSelect: () => openUrl(data.data.billingUrl),
onSelect: () => platform.openUrl(data.data.billingUrl),
},
{
label: "Enter License Key",
+3 -4
View File
@@ -1,7 +1,6 @@
import { useQuery } from "@tanstack/react-query";
import { convertFileSrc } from "@tauri-apps/api/core";
import { resolveResource } from "@tauri-apps/api/path";
import classNames from "classnames";
import { platform } from "@yaakapp-internal/platform";
interface Props {
src: string;
@@ -12,8 +11,8 @@ export function LocalImage({ src: srcPath, className }: Props) {
const src = useQuery({
queryKey: ["local-image", srcPath],
queryFn: async () => {
const p = await resolveResource(srcPath);
return convertFileSrc(p);
const p = await platform.files.resolveResource(srcPath);
return platform.files.url(p);
},
});
@@ -1,4 +1,3 @@
import { openUrl } from "@tauri-apps/plugin-opener";
import type { HttpResponse } from "@yaakapp-internal/models";
import { format, formatDistanceToNowStrict } from "date-fns";
import { useMemo } from "react";
@@ -6,6 +5,7 @@ import { CountBadge } from "./core/CountBadge";
import { DetailsBanner } from "./core/DetailsBanner";
import { IconButton } from "./core/IconButton";
import { KeyValueRow, KeyValueRows } from "./core/KeyValueRow";
import { platform } from "@yaakapp-internal/platform";
interface Props {
response: HttpResponse;
@@ -45,7 +45,7 @@ export function ResponseHeaders({ response }: Props) {
iconSize="sm"
className="inline-block w-auto h-auto! opacity-50 hover:opacity-100"
icon="external_link"
onClick={() => openUrl(response.url)}
onClick={() => platform.openUrl(response.url)}
title="Open in browser"
/>
</div>
+2 -2
View File
@@ -1,7 +1,7 @@
import { openUrl } from "@tauri-apps/plugin-opener";
import type { HttpResponse } from "@yaakapp-internal/models";
import { IconButton } from "./core/IconButton";
import { KeyValueRow, KeyValueRows } from "./core/KeyValueRow";
import { platform } from "@yaakapp-internal/platform";
interface Props {
response: HttpResponse;
@@ -26,7 +26,7 @@ export function ResponseInfo({ response }: Props) {
iconSize="sm"
className="inline-block w-auto ml-1 h-auto! opacity-50 hover:opacity-100"
icon="external_link"
onClick={() => openUrl(response.url)}
onClick={() => platform.openUrl(response.url)}
title="Open in browser"
/>
</div>
+17 -29
View File
@@ -1,5 +1,4 @@
import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow";
import { open } from "@tauri-apps/plugin-dialog";
import { platform } from "@yaakapp-internal/platform";
import { HStack } from "@yaakapp-internal/ui";
import classNames from "classnames";
import mime from "mime";
@@ -41,7 +40,7 @@ export function SelectFile({
...props
}: Props) {
const handleClick = async () => {
const filePath = await open({
const filePath = await platform.dialog.open({
title: directory ? "Select Folder" : "Select File",
multiple: false,
directory,
@@ -64,32 +63,21 @@ export function SelectFile({
// NOTE: This doesn't work for Windows since native drag-n-drop can't work at the same tmie
// as browser drag-n-drop.
useEffect(() => {
let unlisten: (() => void) | undefined;
const setup = async () => {
const webview = getCurrentWebviewWindow();
unlisten = await webview.onDragDropEvent((event) => {
if (event.payload.type === "over") {
const p = event.payload.position;
const r = ref.current?.getBoundingClientRect();
if (r == null) return;
const isOver = p.x >= r.left && p.x <= r.right && p.y >= r.top && p.y <= r.bottom;
console.log("IS OVER", isOver);
setIsHovering(isOver);
} else if (event.payload.type === "drop" && isHovering) {
console.log("User dropped", event.payload.paths);
const p = event.payload.paths[0];
if (p) onChange({ filePath: p, contentType: null });
setIsHovering(false);
} else {
console.log("File drop cancelled");
setIsHovering(false);
}
});
};
setup().catch(console.error);
return () => {
if (unlisten) unlisten();
};
return platform.window.onDragDrop((event) => {
if (event.type === "over") {
const p = event.position;
const r = ref.current?.getBoundingClientRect();
if (r == null) return;
const isOver = p.x >= r.left && p.x <= r.right && p.y >= r.top && p.y <= r.bottom;
setIsHovering(isOver);
} else if (event.type === "drop" && isHovering) {
const p = event.paths[0];
if (p) onChange({ filePath: p, contentType: null });
setIsHovering(false);
} else {
setIsHovering(false);
}
});
}, [isHovering, onChange]);
const filePathWithNameOverride = nameOverride ? `${filePath} (${nameOverride})` : filePath;
@@ -1,6 +1,5 @@
import { useSearch } from "@tanstack/react-router";
import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow";
import { type } from "@tauri-apps/plugin-os";
import { platform } from "@yaakapp-internal/platform";
import { useLicense } from "@yaakapp-internal/license";
import { pluginsAtom, settingsAtom } from "@yaakapp-internal/models";
import { HeaderSize, HStack, Icon } from "@yaakapp-internal/ui";
@@ -60,7 +59,7 @@ export default function Settings({ hide }: Props) {
hide();
} else {
// It's being shown in a window, so close the window
await getCurrentWebviewWindow().close();
await platform.window.close();
}
});
@@ -75,7 +74,7 @@ export default function Settings({ hide }: Props) {
onlyXWindowControl
size="md"
className="x-theme-appHeader bg-surface text-text-subtle flex items-center justify-center border-b border-border-subtle text-sm font-semibold"
osType={type()}
osType={platform.osType()}
hideWindowControls={settings.hideWindowControls}
useNativeTitlebar={settings.useNativeTitlebar}
interfaceScale={settings.interfaceScale}
@@ -85,7 +84,7 @@ export default function Settings({ hide }: Props) {
justifyContent="center"
className="w-full h-full grid grid-cols-[1fr_auto] pointer-events-none"
>
<div className={classNames(type() === "macos" ? "text-center" : "pl-2")}>Settings</div>
<div className={classNames(platform.osType() === "macos" ? "text-center" : "pl-2")}>Settings</div>
</HStack>
</HeaderSize>
)}
@@ -1,4 +1,3 @@
import { revealItemInDir } from "@tauri-apps/plugin-opener";
import { patchModel, settingsAtom } from "@yaakapp-internal/models";
import { Heading, VStack } from "@yaakapp-internal/ui";
import { useAtomValue } from "jotai";
@@ -9,6 +8,7 @@ import { CargoFeature } from "../CargoFeature";
import { CommercialUseBanner } from "../CommercialUseBanner";
import { DismissibleBanner } from "../core/DismissibleBanner";
import { IconButton } from "../core/IconButton";
import { platform } from "@yaakapp-internal/platform";
import {
ModelSettingRowBoolean,
ModelSettingSelectControl,
@@ -152,7 +152,7 @@ export function SettingsGeneral() {
{
title: revealInFinderText,
icon: "folder_open",
onClick: () => revealItemInDir(appInfo.appDataDir),
onClick: () => platform.revealItemInDir(appInfo.appDataDir),
},
]}
/>
@@ -168,7 +168,7 @@ export function SettingsGeneral() {
{
title: revealInFinderText,
icon: "folder_open",
onClick: () => revealItemInDir(appInfo.appLogDir),
onClick: () => platform.revealItemInDir(appInfo.appLogDir),
},
]}
/>
@@ -1,4 +1,4 @@
import { type } from "@tauri-apps/plugin-os";
import { platform } from "@yaakapp-internal/platform";
import { useFonts } from "@yaakapp-internal/fonts";
import { useLicense } from "@yaakapp-internal/license";
import type { EditorKeymap, Settings } from "@yaakapp-internal/models";
@@ -9,7 +9,7 @@ import { useState } from "react";
import { activeWorkspaceAtom } from "../../hooks/useActiveWorkspace";
import { showConfirm } from "../../lib/confirm";
import { pricingUrl } from "../../lib/pricingUrl";
import { invokeCmd } from "../../lib/tauri";
import { rpc } from "../../lib/rpc";
import { CargoFeature } from "../CargoFeature";
import { Button } from "../core/Button";
import { Checkbox } from "../core/Checkbox";
@@ -176,7 +176,7 @@ export function SettingsInterface() {
<SettingsSection title="Window">
<NativeTitlebarSetting settings={settings} />
{type() !== "macos" && (
{platform.osType() !== "macos" && (
<ModelSettingRowBoolean
model={settings}
modelKey="hideWindowControls"
@@ -216,7 +216,7 @@ function NativeTitlebarSetting({ settings }: { settings: Settings }) {
size="xs"
onClick={async () => {
await patchModel(settings, { useNativeTitlebar: nativeTitlebar });
await invokeCmd("cmd_restart");
await rpc("cmd_restart");
}}
>
Apply and Restart
@@ -1,4 +1,3 @@
import { openUrl } from "@tauri-apps/plugin-opener";
import { useLicense } from "@yaakapp-internal/license";
import { Banner, HStack, Icon, VStack } from "@yaakapp-internal/ui";
import { differenceInDays } from "date-fns";
@@ -12,6 +11,7 @@ import { Button } from "../core/Button";
import { Link } from "../core/Link";
import { PlainInput } from "../core/PlainInput";
import { Separator } from "../core/Separator";
import { platform } from "@yaakapp-internal/platform";
export function SettingsLicense() {
return (
@@ -135,7 +135,7 @@ function SettingsLicenseCmp() {
<Button
color="secondary"
size="sm"
onClick={() => openUrl("https://yaak.app/dashboard?intent=app.license.support")}
onClick={() => platform.openUrl("https://yaak.app/dashboard?intent=app.license.support")}
rightSlot={<Icon icon="external_link" />}
>
Direct Support
@@ -151,7 +151,7 @@ function SettingsLicenseCmp() {
color="primary"
rightSlot={<Icon icon="external_link" />}
onClick={() =>
openUrl(pricingUrl(`app.license.purchase.${check.data?.status ?? "unknown"}`))
platform.openUrl(pricingUrl(`app.license.purchase.${check.data?.status ?? "unknown"}`))
}
>
Purchase License
@@ -1,5 +1,4 @@
import { useMutation, useQuery } from "@tanstack/react-query";
import { openUrl } from "@tauri-apps/plugin-opener";
import type { Plugin } from "@yaakapp-internal/models";
import { patchModel, pluginsAtom } from "@yaakapp-internal/models";
import type { PluginVersion } from "@yaakapp-internal/plugins";
@@ -39,6 +38,7 @@ import { PlainInput } from "../core/PlainInput";
import { TabContent, Tabs } from "../core/Tabs/Tabs";
import { EmptyStateText } from "../EmptyStateText";
import { SelectFile } from "../SelectFile";
import { platform } from "@yaakapp-internal/platform";
interface SettingsPluginsProps {
defaultSubtab?: string;
@@ -113,7 +113,7 @@ export function SettingsPlugins({ defaultSubtab }: SettingsPluginsProps) {
icon="help"
title="View documentation"
onClick={() =>
openUrl("https://yaak.app/docs/plugin-development/plugins-quick-start")
platform.openUrl("https://yaak.app/docs/plugin-development/plugins-quick-start")
}
/>
</HStack>
@@ -1,4 +1,3 @@
import { openUrl } from "@tauri-apps/plugin-opener";
import { useLicense } from "@yaakapp-internal/license";
import { useRef } from "react";
import { openSettings } from "../commands/openSettings";
@@ -13,6 +12,7 @@ import { Dropdown } from "./core/Dropdown";
import { Icon } from "@yaakapp-internal/ui";
import { IconButton } from "./core/IconButton";
import { KeyboardShortcutsDialog } from "./KeyboardShortcutsDialog";
import { platform } from "@yaakapp-internal/platform";
export function SettingsDropdown() {
const exportData = useExportData();
@@ -62,7 +62,7 @@ export function SettingsDropdown() {
{
label: "Create Run Button",
leftSlot: <Icon icon="rocket" />,
onSelect: () => openUrl("https://yaak.app/button/new"),
onSelect: () => platform.openUrl("https://yaak.app/button/new"),
},
{ type: "separator", label: `Yaak v${appInfo.version}` },
{
@@ -78,26 +78,26 @@ export function SettingsDropdown() {
leftSlot: <Icon icon="circle_dollar_sign" />,
rightSlot: <Icon icon="external_link" color="success" className="opacity-60" />,
onSelect: () =>
openUrl(pricingUrl(`app.menu.purchase.${check.data?.status ?? "unknown"}`)),
platform.openUrl(pricingUrl(`app.menu.purchase.${check.data?.status ?? "unknown"}`)),
},
{
label: "Install CLI",
hidden: appInfo.cliVersion != null,
leftSlot: <Icon icon="square_terminal" />,
rightSlot: <Icon icon="external_link" color="secondary" />,
onSelect: () => openUrl("https://yaak.app/docs/cli"),
onSelect: () => platform.openUrl("https://yaak.app/docs/cli"),
},
{
label: "Feedback",
leftSlot: <Icon icon="chat" />,
rightSlot: <Icon icon="external_link" color="secondary" />,
onSelect: () => openUrl("https://yaak.app/feedback"),
onSelect: () => platform.openUrl("https://yaak.app/feedback"),
},
{
label: "Changelog",
leftSlot: <Icon icon="cake" />,
rightSlot: <Icon icon="external_link" color="secondary" />,
onSelect: () => openUrl(`https://yaak.app/changelog/${appInfo.version}`),
onSelect: () => platform.openUrl(`https://yaak.app/changelog/${appInfo.version}`),
},
]}
>
+4 -4
View File
@@ -1,6 +1,5 @@
import type { Extension } from "@codemirror/state";
import { Compartment } from "@codemirror/state";
import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow";
import { debounce } from "@yaakapp-internal/lib";
import { gitMutations } from "@yaakapp-internal/git";
import type { GitStatus } from "@yaakapp-internal/git";
@@ -44,7 +43,8 @@ import { getFolderActions } from "../hooks/useFolderActions";
import { getGrpcRequestActions } from "../hooks/useGrpcRequestActions";
import { useHotKey } from "../hooks/useHotKey";
import { getHttpRequestActions } from "../hooks/useHttpRequestActions";
import { useListenToTauriEvent } from "../hooks/useListenToTauriEvent";
import { platform } from "@yaakapp-internal/platform";
import { usePlatformEvent } from "../hooks/usePlatformEvent";
import { getModelAncestors } from "../hooks/useModelAncestors";
import { sendAnyHttpRequest } from "../hooks/useSendAnyHttpRequest";
import { useSidebarHidden } from "../hooks/useSidebarHidden";
@@ -135,10 +135,10 @@ function Sidebar({ className }: { className?: string }) {
// Focus new sidebar models created by the user in this window. Writes from other
// sources (import, sync, CLI) can carry thousands of models and shouldn't move
// the selection.
useListenToTauriEvent<ModelPayload[]>("model_writes", ({ payload: payloads }) => {
usePlatformEvent<ModelPayload[]>("model_writes", (payloads) => {
for (const payload of payloads) {
if (payload.updateSource.type !== "window") continue;
if (payload.updateSource.label !== getCurrentWebviewWindow().label) continue;
if (payload.updateSource.label !== platform.window.label) continue;
if (!isSidebarLeafModel(payload.model)) continue;
if (!(payload.change.type === "upsert" && payload.change.created)) continue;
treeRef.current?.selectItem(payload.model.id, true);
@@ -1,4 +1,3 @@
import { readDir } from "@tauri-apps/plugin-fs";
import { Banner, VStack } from "@yaakapp-internal/ui";
import { useState } from "react";
import { openWorkspaceFromSyncDir } from "../commands/openWorkspaceFromSyncDir";
@@ -6,6 +5,7 @@ import { Button } from "./core/Button";
import { Checkbox } from "./core/Checkbox";
import { SettingRowBoolean, SettingRowDirectory } from "./core/SettingRow";
import { SelectFile } from "./SelectFile";
import { platform } from "@yaakapp-internal/platform";
export interface SyncToFilesystemSettingProps {
layout?: "form" | "settings";
@@ -24,7 +24,7 @@ export function SyncToFilesystemSetting({
const handleFilePathChange = async (filePath: string | null) => {
if (filePath != null) {
const files = await readDir(filePath);
const files = await platform.files.readDir(filePath);
if (files.length > 0) {
setSyncDir(filePath);
return;
+2 -2
View File
@@ -1,4 +1,4 @@
import { type } from "@tauri-apps/plugin-os";
import { platform } from "@yaakapp-internal/platform";
import { settingsAtom, workspacesAtom } from "@yaakapp-internal/models";
import { Banner, HeaderSize, HStack, SidebarLayout } from "@yaakapp-internal/ui";
import classNames from "classnames";
@@ -53,7 +53,7 @@ export function Workspace() {
const workspaces = useAtomValue(workspacesAtom);
const settings = useAtomValue(settingsAtom);
const osType = type();
const osType = platform.osType();
const [width, setWidth] = useSidebarWidth();
const [sidebarHidden, setSidebarHidden] = useSidebarHidden();
const [floatingSidebarHidden, setFloatingSidebarHidden] = useFloatingSidebarHidden();
@@ -1,5 +1,3 @@
import { open } from "@tauri-apps/plugin-dialog";
import { revealItemInDir } from "@tauri-apps/plugin-opener";
import { getModel, settingsAtom, workspacesAtom } from "@yaakapp-internal/models";
import classNames from "classnames";
import { useAtomValue } from "jotai";
@@ -27,6 +25,7 @@ import { Icon } from "@yaakapp-internal/ui";
import type { RadioDropdownItem } from "./core/RadioDropdown";
import { RadioDropdown } from "./core/RadioDropdown";
import { SwitchWorkspaceDialog } from "./SwitchWorkspaceDialog";
import { platform } from "@yaakapp-internal/platform";
type Props = Pick<ButtonProps, "className" | "justify" | "forDropdown" | "leftSlot">;
@@ -76,7 +75,7 @@ export const WorkspaceActionsDropdown = memo(function WorkspaceActionsDropdown({
label: "Open Folder",
leftSlot: <Icon icon="folder_open" />,
onSelect: async () => {
const dir = await open({
const dir = await platform.dialog.open({
title: "Select Workspace Directory",
directory: true,
multiple: false,
@@ -120,7 +119,7 @@ export const WorkspaceActionsDropdown = memo(function WorkspaceActionsDropdown({
leftSlot: <Icon icon="folder_symlink" />,
onSelect: async () => {
if (workspaceMeta?.settingSyncDir == null) return;
await revealItemInDir(workspaceMeta.settingSyncDir);
await platform.revealItemInDir(workspaceMeta.settingSyncDir);
},
},
{
@@ -9,7 +9,6 @@ import {
useSensor,
useSensors,
} from "@dnd-kit/core";
import { basename } from "@tauri-apps/api/path";
import classNames from "classnames";
import { Fragment, useCallback, useEffect, useMemo, useRef, useState } from "react";
import type { WrappedEnvironmentVariable } from "../../hooks/useEnvironmentVariables";
@@ -34,6 +33,7 @@ import { Input } from "./Input";
import { ensurePairId } from "./PairEditor.util";
import type { RadioDropdownItem } from "./RadioDropdown";
import { RadioDropdown } from "./RadioDropdown";
import { platform } from "@yaakapp-internal/platform";
export interface PairEditorHandle {
/**
@@ -851,7 +851,7 @@ function FileActionsDropdown({
leftSlot: <Icon icon="file_code" />,
onSelect: async () => {
console.log("PAIR", pair);
const defaultFilename = await basename(pair.value ?? "");
const defaultFilename = await platform.files.basename(pair.value ?? "");
const filename = await showPrompt({
id: "filename",
title: "Override Filename",
+2 -2
View File
@@ -1,4 +1,4 @@
import { type } from "@tauri-apps/plugin-os";
import { platform } from "@yaakapp-internal/platform";
import { HStack } from "@yaakapp-internal/ui";
import classNames from "classnames";
import type { CSSProperties, ReactNode } from "react";
@@ -66,7 +66,7 @@ export function Select<T extends string>({
<Label htmlFor={id} visuallyHidden={hideLabel} className={labelClassName} help={help}>
{label}
</Label>
{type() === "macos" && !filterable ? (
{platform.osType() === "macos" && !filterable ? (
<HStack
space={2}
className={classNames(
@@ -1,5 +1,5 @@
import { convertFileSrc } from "@tauri-apps/api/core";
import { useEffect, useState } from "react";
import { platform } from "@yaakapp-internal/platform";
interface Props {
bodyPath?: string;
@@ -12,7 +12,7 @@ export function AudioViewer({ bodyPath, data, mimeType }: Props) {
useEffect(() => {
if (bodyPath) {
setSrc(convertFileSrc(bodyPath));
setSrc(platform.files.url(bodyPath));
} else if (data) {
// The type matters here in a way it doesn't for an image: a media element goes by what
// the blob declares rather than sniffing it, so an Ogg labelled as MP3 won't play
@@ -1,6 +1,6 @@
import { convertFileSrc } from "@tauri-apps/api/core";
import classNames from "classnames";
import { useEffect, useState } from "react";
import { platform } from "@yaakapp-internal/platform";
type Props = { className?: string; mimeType?: string } & (
| {
@@ -18,7 +18,7 @@ export function ImageViewer({ className, mimeType, ...props }: Props) {
useEffect(() => {
if (bodyPath != null) {
setSrc(convertFileSrc(bodyPath));
setSrc(platform.files.url(bodyPath));
} else if (data != null) {
const blob = new Blob([data], { type: mimeType ?? "image/png" });
const url = URL.createObjectURL(blob);
@@ -1,12 +1,12 @@
import "react-pdf/dist/Page/TextLayer.css";
import "react-pdf/dist/Page/AnnotationLayer.css";
import { convertFileSrc } from "@tauri-apps/api/core";
import "./PdfViewer.css";
import type { PDFDocumentProxy } from "pdfjs-dist";
import { useMemo, useRef, useState } from "react";
import { Document, Page } from "react-pdf";
import { useContainerSize } from "@yaakapp-internal/ui";
import { fireAndForget } from "../../lib/fireAndForget";
import { platform } from "@yaakapp-internal/platform";
fireAndForget(
import("react-pdf").then(({ pdfjs }) => {
@@ -37,7 +37,7 @@ export function PdfViewer({ bodyPath, data }: Props) {
// `Document` renders its "Failed to load PDF file" state for that frame before recovering
const src = useMemo(() => {
if (bodyPath) {
return convertFileSrc(bodyPath);
return platform.files.url(bodyPath);
}
if (data) {
// Create a copy to avoid "Buffer is already detached" errors
@@ -1,5 +1,5 @@
import { convertFileSrc } from "@tauri-apps/api/core";
import { useEffect, useState } from "react";
import { platform } from "@yaakapp-internal/platform";
interface Props {
bodyPath?: string;
@@ -12,7 +12,7 @@ export function VideoViewer({ bodyPath, data, mimeType }: Props) {
useEffect(() => {
if (bodyPath) {
setSrc(convertFileSrc(bodyPath));
setSrc(platform.files.url(bodyPath));
} else if (data) {
// As in AudioViewer: a media element trusts the declared type instead of sniffing
const blob = new Blob([new Uint8Array(data)], { type: mimeType ?? "video/mp4" });