mirror of
https://github.com/gradio-app/gradio.git
synced 2025-02-17 11:29:58 +08:00
Fix functional tests (#6931)
* changes * add changeset * add changeset * changes * changes * changes * add changeset * changes * changes * changes * changes * changes * changes --------- Co-authored-by: Ali Abid <aliabid@Alis-MacBook-Pro.local> Co-authored-by: gradio-pr-bot <gradio-pr-bot@users.noreply.github.com> Co-authored-by: Abubakar Abid <abubakar@huggingface.co>
This commit is contained in:
parent
d361a0f179
commit
6c863af92f
9
.changeset/empty-paths-write.md
Normal file
9
.changeset/empty-paths-write.md
Normal file
@ -0,0 +1,9 @@
|
||||
---
|
||||
"@gradio/client": patch
|
||||
"@gradio/fileexplorer": patch
|
||||
"@gradio/image": patch
|
||||
"@gradio/upload": patch
|
||||
"gradio": patch
|
||||
---
|
||||
|
||||
feat:Fix functional tests
|
@ -820,10 +820,6 @@ export function api_factory(
|
||||
last_status[fn_index]
|
||||
);
|
||||
|
||||
// TODO: Find out how to print this information
|
||||
// only during testing
|
||||
// console.info("data", type, status, data);
|
||||
|
||||
if (type == "heartbeat") {
|
||||
return;
|
||||
}
|
||||
@ -844,7 +840,8 @@ export function api_factory(
|
||||
fire_event({
|
||||
type: "status",
|
||||
stage: "error",
|
||||
message: "An Unexpected Error Occurred!",
|
||||
message:
|
||||
status?.message || "An Unexpected Error Occurred!",
|
||||
queue: true,
|
||||
endpoint: _endpoint,
|
||||
fn_index,
|
||||
@ -1053,6 +1050,17 @@ export function api_factory(
|
||||
pending_stream_messages[event_id].push(_data);
|
||||
}
|
||||
};
|
||||
event_stream.onerror = async function (event) {
|
||||
await Promise.all(
|
||||
Object.keys(event_callbacks).map((event_id) =>
|
||||
event_callbacks[event_id]({
|
||||
msg: "unexpected_error",
|
||||
message: BROKEN_CONNECTION_MSG
|
||||
})
|
||||
)
|
||||
);
|
||||
close_stream();
|
||||
};
|
||||
}
|
||||
|
||||
function close_stream(): void {
|
||||
|
@ -1 +1 @@
|
||||
{"cells": [{"cell_type": "markdown", "id": "302934307671667531413257853548643485645", "metadata": {}, "source": ["# Gradio Demo: cancel_events"]}, {"cell_type": "code", "execution_count": null, "id": "272996653310673477252411125948039410165", "metadata": {}, "outputs": [], "source": ["!pip install -q gradio "]}, {"cell_type": "code", "execution_count": null, "id": "288918539441861185822528903084949547379", "metadata": {}, "outputs": [], "source": ["import time\n", "import gradio as gr\n", "\n", "\n", "def fake_diffusion(steps):\n", " for i in range(steps):\n", " print(f\"Current step: {i}\")\n", " time.sleep(1)\n", " yield str(i)\n", "\n", "\n", "def long_prediction(*args, **kwargs):\n", " time.sleep(10)\n", " return 42\n", "\n", "\n", "with gr.Blocks() as demo:\n", " with gr.Row():\n", " with gr.Column():\n", " n = gr.Slider(1, 10, value=9, step=1, label=\"Number Steps\")\n", " run = gr.Button(value=\"Start Iterating\")\n", " output = gr.Textbox(label=\"Iterative Output\")\n", " stop = gr.Button(value=\"Stop Iterating\")\n", " with gr.Column():\n", " textbox = gr.Textbox(label=\"Prompt\")\n", " prediction = gr.Number(label=\"Expensive Calculation\")\n", " run_pred = gr.Button(value=\"Run Expensive Calculation\")\n", " with gr.Column():\n", " cancel_on_change = gr.Textbox(label=\"Cancel Iteration and Expensive Calculation on Change\")\n", " cancel_on_submit = gr.Textbox(label=\"Cancel Iteration and Expensive Calculation on Submit\")\n", " echo = gr.Textbox(label=\"Echo\")\n", " with gr.Row():\n", " with gr.Column():\n", " image = gr.Image(sources=[\"webcam\"], label=\"Cancel on clear\", interactive=True)\n", " with gr.Column():\n", " video = gr.Video(sources=[\"webcam\"], label=\"Cancel on start recording\", interactive=True)\n", "\n", " click_event = run.click(fake_diffusion, n, output)\n", " stop.click(fn=None, inputs=None, outputs=None, cancels=[click_event])\n", " pred_event = run_pred.click(fn=long_prediction, inputs=[textbox], outputs=prediction)\n", "\n", " cancel_on_change.change(None, None, None, cancels=[click_event, pred_event])\n", " cancel_on_submit.submit(lambda s: s, cancel_on_submit, echo, cancels=[click_event, pred_event])\n", " image.clear(None, None, None, cancels=[click_event, pred_event])\n", " video.start_recording(None, None, None, cancels=[click_event, pred_event])\n", "\n", " demo.queue(max_size=20)\n", "\n", "if __name__ == \"__main__\":\n", " demo.launch()\n"]}], "metadata": {}, "nbformat": 4, "nbformat_minor": 5}
|
||||
{"cells": [{"cell_type": "markdown", "id": "302934307671667531413257853548643485645", "metadata": {}, "source": ["# Gradio Demo: cancel_events"]}, {"cell_type": "code", "execution_count": null, "id": "272996653310673477252411125948039410165", "metadata": {}, "outputs": [], "source": ["!pip install -q gradio "]}, {"cell_type": "code", "execution_count": null, "id": "288918539441861185822528903084949547379", "metadata": {}, "outputs": [], "source": ["import time\n", "import gradio as gr\n", "\n", "\n", "def fake_diffusion(steps):\n", " for i in range(steps):\n", " print(f\"Current step: {i}\")\n", " time.sleep(0.2)\n", " yield str(i)\n", "\n", "\n", "def long_prediction(*args, **kwargs):\n", " time.sleep(10)\n", " return 42\n", "\n", "\n", "with gr.Blocks() as demo:\n", " with gr.Row():\n", " with gr.Column():\n", " n = gr.Slider(1, 10, value=9, step=1, label=\"Number Steps\")\n", " run = gr.Button(value=\"Start Iterating\")\n", " output = gr.Textbox(label=\"Iterative Output\")\n", " stop = gr.Button(value=\"Stop Iterating\")\n", " with gr.Column():\n", " textbox = gr.Textbox(label=\"Prompt\")\n", " prediction = gr.Number(label=\"Expensive Calculation\")\n", " run_pred = gr.Button(value=\"Run Expensive Calculation\")\n", " with gr.Column():\n", " cancel_on_change = gr.Textbox(label=\"Cancel Iteration and Expensive Calculation on Change\")\n", " cancel_on_submit = gr.Textbox(label=\"Cancel Iteration and Expensive Calculation on Submit\")\n", " echo = gr.Textbox(label=\"Echo\")\n", " with gr.Row():\n", " with gr.Column():\n", " image = gr.Image(sources=[\"webcam\"], label=\"Cancel on clear\", interactive=True)\n", " with gr.Column():\n", " video = gr.Video(sources=[\"webcam\"], label=\"Cancel on start recording\", interactive=True)\n", "\n", " click_event = run.click(fake_diffusion, n, output)\n", " stop.click(fn=None, inputs=None, outputs=None, cancels=[click_event])\n", " pred_event = run_pred.click(fn=long_prediction, inputs=[textbox], outputs=prediction)\n", "\n", " cancel_on_change.change(None, None, None, cancels=[click_event, pred_event])\n", " cancel_on_submit.submit(lambda s: s, cancel_on_submit, echo, cancels=[click_event, pred_event])\n", " image.clear(None, None, None, cancels=[click_event, pred_event])\n", " video.start_recording(None, None, None, cancels=[click_event, pred_event])\n", "\n", " demo.queue(max_size=20)\n", "\n", "if __name__ == \"__main__\":\n", " demo.launch()\n"]}], "metadata": {}, "nbformat": 4, "nbformat_minor": 5}
|
@ -5,7 +5,7 @@ import gradio as gr
|
||||
def fake_diffusion(steps):
|
||||
for i in range(steps):
|
||||
print(f"Current step: {i}")
|
||||
time.sleep(1)
|
||||
time.sleep(0.2)
|
||||
yield str(i)
|
||||
|
||||
|
||||
|
@ -7,10 +7,10 @@ import os
|
||||
import threading
|
||||
import urllib.parse
|
||||
import warnings
|
||||
from packaging.version import Version
|
||||
from typing import Any
|
||||
|
||||
import httpx
|
||||
from packaging.version import Version
|
||||
|
||||
import gradio
|
||||
from gradio import wasm_utils
|
||||
|
@ -117,7 +117,7 @@ class Block:
|
||||
self.share_token = secrets.token_urlsafe(32)
|
||||
self.parent: BlockContext | None = None
|
||||
self.is_rendered: bool = False
|
||||
self._constructor_args: dict
|
||||
self._constructor_args: list[dict]
|
||||
self.state_session_capacity = 10000
|
||||
self.temp_files: set[str] = set()
|
||||
self.GRADIO_CACHE = str(
|
||||
|
@ -5,6 +5,7 @@ from __future__ import annotations
|
||||
import itertools
|
||||
import os
|
||||
import re
|
||||
import warnings
|
||||
from pathlib import Path
|
||||
from typing import Any, Callable, List, Literal
|
||||
|
||||
@ -39,7 +40,7 @@ class FileExplorer(Component):
|
||||
*,
|
||||
value: str | list[str] | Callable | None = None,
|
||||
file_count: Literal["single", "multiple"] = "multiple",
|
||||
root: str | Path = ".",
|
||||
root_dir: str | Path = ".",
|
||||
ignore_glob: str | None = None,
|
||||
label: str | None = None,
|
||||
every: float | None = None,
|
||||
@ -53,13 +54,14 @@ class FileExplorer(Component):
|
||||
elem_id: str | None = None,
|
||||
elem_classes: list[str] | str | None = None,
|
||||
render: bool = True,
|
||||
root: None = None,
|
||||
):
|
||||
"""
|
||||
Parameters:
|
||||
glob: The glob-style pattern used to select which files to display, e.g. "*" to match all files, "*.png" to match all .png files, "**/*.txt" to match any .txt file in any subdirectory, etc. The default value matches all files and folders recursively. See the Python glob documentation at https://docs.python.org/3/library/glob.html for more information.
|
||||
value: The file (or list of files, depending on the `file_count` parameter) to show as "selected" when the component is first loaded. If a callable is provided, it will be called when the app loads to set the initial value of the component. If not provided, no files are shown as selected.
|
||||
file_count: Whether to allow single or multiple files to be selected. If "single", the component will return a single absolute file path as a string. If "multiple", the component will return a list of absolute file paths as a list of strings.
|
||||
root: Path to root directory to select files from. If not provided, defaults to current working directory.
|
||||
root_dir: Path to root directory to select files from. If not provided, defaults to current working directory.
|
||||
ignore_glob: The glob-tyle pattern that will be used to exclude files from the list. For example, "*.py" will exclude all .py files from the list. See the Python glob documentation at https://docs.python.org/3/library/glob.html for more information.
|
||||
label: The label for this component. Appears above the component and is also used as the header if there are a table of examples for this component. If None and used in a `gr.Interface`, the label will be the name of the parameter this component is assigned to.
|
||||
every: If `value` is a callable, run the function 'every' number of seconds while the client connection is open. Has no effect otherwise. Queue must be enabled. The event can be accessed (e.g. to cancel it) via this component's .load_event attribute.
|
||||
@ -74,7 +76,13 @@ class FileExplorer(Component):
|
||||
elem_classes: An optional list of strings that are assigned as the classes of this component in the HTML DOM. Can be used for targeting CSS styles.
|
||||
render: If False, component will not render be rendered in the Blocks context. Should be used if the intention is to assign event listeners now but render the component later.
|
||||
"""
|
||||
self.root = os.path.abspath(root)
|
||||
if root is not None:
|
||||
warnings.warn(
|
||||
"The `root` parameter has been deprecated. Please use `root_dir` instead."
|
||||
)
|
||||
root_dir = root
|
||||
self._constructor_args[0]["root_dir"] = root
|
||||
self.root_dir = os.path.abspath(root_dir)
|
||||
self.glob = glob
|
||||
self.ignore_glob = ignore_glob
|
||||
valid_file_count = ["single", "multiple", "directory"]
|
||||
@ -123,8 +131,8 @@ class FileExplorer(Component):
|
||||
return files
|
||||
|
||||
def _strip_root(self, path):
|
||||
if path.startswith(self.root):
|
||||
return path[len(self.root) + 1 :]
|
||||
if path.startswith(self.root_dir):
|
||||
return path[len(self.root_dir) + 1 :]
|
||||
return path
|
||||
|
||||
def postprocess(self, value: str | list[str] | None) -> FileExplorerData | None:
|
||||
@ -194,19 +202,19 @@ class FileExplorer(Component):
|
||||
|
||||
files: list[Path] = []
|
||||
for result in expand_braces(self.glob):
|
||||
files += list(Path(self.root).resolve().glob(result))
|
||||
files += list(Path(self.root_dir).resolve().glob(result))
|
||||
|
||||
files = [f for f in files if f != Path(self.root).resolve()]
|
||||
files = [f for f in files if f != Path(self.root_dir).resolve()]
|
||||
|
||||
ignore_files = []
|
||||
if self.ignore_glob:
|
||||
for result in expand_braces(self.ignore_glob):
|
||||
ignore_files += list(Path(self.root).resolve().glob(result))
|
||||
ignore_files += list(Path(self.root_dir).resolve().glob(result))
|
||||
files = list(set(files) - set(ignore_files))
|
||||
|
||||
files_with_sep = []
|
||||
for f in files:
|
||||
file = str(f.relative_to(self.root))
|
||||
file = str(f.relative_to(self.root_dir))
|
||||
if f.is_dir():
|
||||
file += os.path.sep
|
||||
files_with_sep.append(file)
|
||||
@ -215,10 +223,10 @@ class FileExplorer(Component):
|
||||
return tree
|
||||
|
||||
def _safe_join(self, folders):
|
||||
combined_path = os.path.join(self.root, *folders)
|
||||
combined_path = os.path.join(self.root_dir, *folders)
|
||||
absolute_path = os.path.abspath(combined_path)
|
||||
if os.path.commonprefix([self.root, absolute_path]) != os.path.abspath(
|
||||
self.root
|
||||
if os.path.commonprefix([self.root_dir, absolute_path]) != os.path.abspath(
|
||||
self.root_dir
|
||||
):
|
||||
raise ValueError("Attempted to navigate outside of root directory")
|
||||
return absolute_path
|
||||
|
@ -1,72 +0,0 @@
|
||||
import { test, expect } from "@gradio/tootils";
|
||||
|
||||
test.skip("when using an iterative function the UI should update over time as iteration results are received", async ({
|
||||
page
|
||||
}) => {
|
||||
let first_iteration;
|
||||
let fourth_iteration;
|
||||
let last_iteration;
|
||||
|
||||
const start_button = await page.locator("button", {
|
||||
hasText: /Start Iterating/
|
||||
});
|
||||
const textbox = await page.getByLabel("Iterative Output");
|
||||
|
||||
page.on("websocket", (ws) => {
|
||||
first_iteration = ws.waitForEvent("framereceived", {
|
||||
predicate: (event) => {
|
||||
return JSON.parse(event.payload as string).msg === "process_generating";
|
||||
}
|
||||
});
|
||||
|
||||
fourth_iteration = ws.waitForEvent("framereceived", {
|
||||
predicate: (event) => {
|
||||
return JSON.parse(event.payload as string)?.output?.data?.[0] === "3";
|
||||
}
|
||||
});
|
||||
|
||||
last_iteration = ws.waitForEvent("framereceived", {
|
||||
predicate: (event) => {
|
||||
return JSON.parse(event.payload as string).msg === "process_completed";
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
await start_button.click();
|
||||
|
||||
await first_iteration;
|
||||
await expect(textbox).toHaveValue("0");
|
||||
await fourth_iteration;
|
||||
await expect(textbox).toHaveValue("3");
|
||||
await last_iteration;
|
||||
await expect(textbox).toHaveValue("8");
|
||||
});
|
||||
|
||||
test.skip("when using an iterative function it should be possible to cancel the function, after which the UI should stop updating", async ({
|
||||
page
|
||||
}) => {
|
||||
let first_iteration;
|
||||
const start_button = await page.locator("button", {
|
||||
hasText: /Start Iterating/
|
||||
});
|
||||
const stop_button = await page.locator("button", {
|
||||
hasText: /Stop Iterating/
|
||||
});
|
||||
const textbox = await page.getByLabel("Iterative Output");
|
||||
|
||||
page.on("websocket", (ws) => {
|
||||
first_iteration = ws.waitForEvent("framereceived", {
|
||||
predicate: (event) => {
|
||||
return JSON.parse(event.payload as string).msg === "process_generating";
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
await start_button.click();
|
||||
|
||||
await first_iteration;
|
||||
await stop_button.click();
|
||||
await expect(textbox).toHaveValue("0");
|
||||
await page.waitForTimeout(1000);
|
||||
await expect(textbox).toHaveValue("0");
|
||||
});
|
46
js/app/test/cancel_events.spec.ts
Normal file
46
js/app/test/cancel_events.spec.ts
Normal file
@ -0,0 +1,46 @@
|
||||
import { test, expect } from "@gradio/tootils";
|
||||
|
||||
test("when using an iterative function the UI should update over time as iteration results are received", async ({
|
||||
page
|
||||
}) => {
|
||||
const start_button = await page.locator("button", {
|
||||
hasText: /Start Iterating/
|
||||
});
|
||||
const textbox = await page.getByLabel("Iterative Output");
|
||||
|
||||
let output_values: string[] = [];
|
||||
let last_output_value = "";
|
||||
let interval = setInterval(async () => {
|
||||
let value = await textbox.inputValue();
|
||||
if (value !== last_output_value) {
|
||||
output_values.push(value);
|
||||
last_output_value = value;
|
||||
}
|
||||
}, 100);
|
||||
|
||||
await start_button.click();
|
||||
await expect(textbox).toHaveValue("8");
|
||||
clearInterval(interval);
|
||||
for (let i = 1; i < 8; i++) {
|
||||
expect(output_values).toContain(i.toString());
|
||||
}
|
||||
});
|
||||
|
||||
test("when using an iterative function it should be possible to cancel the function, after which the UI should stop updating", async ({
|
||||
page
|
||||
}) => {
|
||||
const start_button = await page.locator("button", {
|
||||
hasText: /Start Iterating/
|
||||
});
|
||||
const stop_button = await page.locator("button", {
|
||||
hasText: /Stop Iterating/
|
||||
});
|
||||
const textbox = await page.getByLabel("Iterative Output");
|
||||
|
||||
await start_button.click();
|
||||
await expect(textbox).toHaveValue("0");
|
||||
await stop_button.click();
|
||||
await expect(textbox).toHaveValue("0");
|
||||
await page.waitForTimeout(1000);
|
||||
await expect(textbox).toHaveValue("0");
|
||||
});
|
@ -53,9 +53,7 @@ test("Image drag-to-upload uploads image successfuly.", async ({ page }) => {
|
||||
await expect(page.getByLabel("# Upload Events")).toHaveValue("1");
|
||||
});
|
||||
|
||||
test.skip("Image copy from clipboard dispatches upload event", async ({
|
||||
page
|
||||
}) => {
|
||||
test("Image copy from clipboard dispatches upload event.", async ({ page }) => {
|
||||
// Need to make request from inside browser for blob to be formatted correctly
|
||||
// tried lots of different things
|
||||
await page.evaluate(async () => {
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { test, expect } from "@gradio/tootils";
|
||||
import { BASE64_IMAGE } from "./media_data";
|
||||
|
||||
test.skip("test inputs", async ({ page }) => {
|
||||
test("test inputs", async ({ page }) => {
|
||||
const textbox = await page.getByLabel("Textbox").nth(0);
|
||||
await expect(textbox).toHaveValue("Lorem ipsum");
|
||||
|
||||
@ -34,13 +34,10 @@ test.skip("test inputs", async ({ page }) => {
|
||||
await expect(image_data_cropper).toContain("cheetah1.jpg");
|
||||
});
|
||||
|
||||
test.skip("test outputs", async ({ page }) => {
|
||||
test("test outputs", async ({ page }) => {
|
||||
const submit_button = await page.locator("button", { hasText: /Submit/ });
|
||||
|
||||
await Promise.all([
|
||||
submit_button.click(),
|
||||
page.waitForResponse("**/run/predict")
|
||||
]);
|
||||
await submit_button.click();
|
||||
|
||||
const textbox = await page.getByLabel("Textbox").nth(2);
|
||||
await expect(textbox).toHaveValue(", selected:foo, bar");
|
@ -1,6 +1,6 @@
|
||||
import { test, expect } from "@gradio/tootils";
|
||||
|
||||
test.skip("UploadButton properly dispatches load event and click event for the single file case.", async ({
|
||||
test("UploadButton properly dispatches load event and click event for the single file case.", async ({
|
||||
page
|
||||
}) => {
|
||||
await page.getByRole("button", { name: "Upload Single File" }).click();
|
||||
@ -18,7 +18,7 @@ test.skip("UploadButton properly dispatches load event and click event for the s
|
||||
await expect(download.suggestedFilename()).toBe("cheetah1.jpg");
|
||||
});
|
||||
|
||||
test.skip("UploadButton properly dispatches load event and click event for the multiple file case.", async ({
|
||||
test("UploadButton properly dispatches load event and click event for the multiple file case.", async ({
|
||||
page
|
||||
}) => {
|
||||
await page.getByRole("button", { name: "Upload Multiple Files" }).click();
|
||||
|
@ -23,7 +23,7 @@
|
||||
|
||||
export let glob: string;
|
||||
export let ignore_glob: string;
|
||||
export let root: string;
|
||||
export let root_dir: string;
|
||||
|
||||
export let loading_status: LoadingStatus;
|
||||
export let container = true;
|
||||
@ -67,7 +67,7 @@
|
||||
{file_count}
|
||||
{server}
|
||||
{interactive}
|
||||
{root}
|
||||
{root_dir}
|
||||
{glob}
|
||||
{ignore_glob}
|
||||
on:change={() => gradio.dispatch("change")}
|
||||
|
@ -1,6 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from "svelte";
|
||||
import type { Node } from "./utils";
|
||||
import { dequal } from "dequal";
|
||||
import FileTree from "./FileTree.svelte";
|
||||
import { make_fs_store } from "./utils";
|
||||
@ -9,7 +8,7 @@
|
||||
|
||||
export let glob: string;
|
||||
export let ignore_glob: string;
|
||||
export let root: string;
|
||||
export let root_dir: string;
|
||||
export let interactive: boolean;
|
||||
export let server: any;
|
||||
export let file_count: "single" | "multiple" = "multiple";
|
||||
@ -21,12 +20,13 @@
|
||||
}>();
|
||||
const tree = make_fs_store();
|
||||
|
||||
$: glob,
|
||||
ignore_glob,
|
||||
root,
|
||||
const render_tree = (): void => {
|
||||
server.ls().then((v: any) => {
|
||||
tree.create_fs_graph(v);
|
||||
});
|
||||
};
|
||||
|
||||
$: glob, ignore_glob, root_dir, render_tree();
|
||||
|
||||
$: value.length && $tree && set_checked_from_paths();
|
||||
|
||||
|
@ -65,7 +65,7 @@
|
||||
}>;
|
||||
|
||||
$: url = _value?.url;
|
||||
$: url && gradio.dispatch("change");
|
||||
$: url, gradio.dispatch("change");
|
||||
|
||||
let dragging: boolean;
|
||||
let active_source: sources = null;
|
||||
@ -134,7 +134,6 @@
|
||||
on:edit={() => gradio.dispatch("edit")}
|
||||
on:clear={() => {
|
||||
gradio.dispatch("clear");
|
||||
gradio.dispatch("change");
|
||||
}}
|
||||
on:stream={() => gradio.dispatch("stream")}
|
||||
on:drag={({ detail }) => (dragging = detail)}
|
||||
|
@ -48,14 +48,12 @@
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
const type = items[i].types.find((t) => t.startsWith("image/"));
|
||||
if (type) {
|
||||
dispatch("load", null);
|
||||
items[i].getType(type).then(async (blob) => {
|
||||
const file = new File(
|
||||
[blob],
|
||||
`clipboard.${type.replace("image/", "")}`
|
||||
);
|
||||
const f = await load_files([file]);
|
||||
dispatch("load", f?.[0]);
|
||||
await load_files([file]);
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user