diff --git a/.changeset/strong-files-swim.md b/.changeset/strong-files-swim.md
new file mode 100644
index 000000000000..002dc818f9d2
--- /dev/null
+++ b/.changeset/strong-files-swim.md
@@ -0,0 +1,11 @@
+---
+"@gradio/app": patch
+"@gradio/atoms": patch
+"@gradio/audio": patch
+"@gradio/image": patch
+"@gradio/upload": patch
+"@gradio/video": patch
+"gradio": patch
+---
+
+fix:Improve source selection UX
diff --git a/js/app/src/lang/en.json b/js/app/src/lang/en.json
index e29f3ee13065..58ef444c90f1 100644
--- a/js/app/src/lang/en.json
+++ b/js/app/src/lang/en.json
@@ -48,7 +48,8 @@
"remove": "Remove",
"share": "Share",
"submit": "Submit",
- "undo": "Undo"
+ "undo": "Undo",
+ "no_devices": "No devices found"
},
"dataframe": {
"incorrect_format": "Incorrect format, only CSV and TSV files are supported",
@@ -110,6 +111,7 @@
"drop_csv": "Drop CSV Here",
"drop_file": "Drop File Here",
"drop_image": "Drop Image Here",
- "drop_video": "Drop Video Here"
+ "drop_video": "Drop Video Here",
+ "paste_clipboard": "Paste from Clipboard"
}
}
diff --git a/js/app/test/image_component_events.spec.ts b/js/app/test/image_component_events.spec.ts
index 36cce521c880..54d46086b4bf 100644
--- a/js/app/test/image_component_events.spec.ts
+++ b/js/app/test/image_component_events.spec.ts
@@ -65,7 +65,33 @@ test("Image copy from clipboard dispatches upload event.", async ({ page }) => {
navigator.clipboard.write([new ClipboardItem({ [blob.type]: blob })]);
});
- await page.getByLabel("clipboard-image-toolbar-btn").click();
+ await page.getByLabel("Paste from clipboard").click();
+ await Promise.all([
+ page.waitForResponse(
+ (resp) => resp.url().includes("/clipboard.png") && resp.status() === 200
+ )
+ ]);
await expect(page.getByLabel("# Change Events").first()).toHaveValue("1");
await expect(page.getByLabel("# Upload Events")).toHaveValue("1");
});
+
+test("Image paste to clipboard via the Upload component works", async ({
+ page
+}) => {
+ await page.evaluate(async () => {
+ navigator.clipboard.writeText("123");
+ });
+
+ await page.getByLabel("Paste from clipboard").click();
+ await page.evaluate(async () => {
+ const blob = await (
+ await fetch(
+ `https://gradio-builds.s3.amazonaws.com/assets/PDFDisplay.png`
+ )
+ ).blob();
+ navigator.clipboard.write([new ClipboardItem({ [blob.type]: blob })]);
+ });
+
+ await page.getByText("Paste from clipboard").click();
+ await expect(page.getByLabel("# Upload Events")).toHaveValue("1");
+});
diff --git a/js/app/test/video_component_events.spec.ts b/js/app/test/video_component_events.spec.ts
index 0006ed35c260..8120a6281646 100644
--- a/js/app/test/video_component_events.spec.ts
+++ b/js/app/test/video_component_events.spec.ts
@@ -3,9 +3,7 @@ import { test, expect, drag_and_drop_file } from "@gradio/tootils";
test("Video click-to-upload uploads video successfuly. Clear, play, and pause buttons dispatch events correctly. Downloading the file works and has the correct name.", async ({
page
}) => {
- await page
- .getByRole("button", { name: "Drop Video Here - or - Click to Upload" })
- .click();
+ await page.getByRole("button", { name: "Upload file" }).click();
const uploader = await page.locator("input[type=file]");
await uploader.setInputFiles(["./test/files/file_test.ogg"]);
@@ -14,9 +12,7 @@ test("Video click-to-upload uploads video successfuly. Clear, play, and pause bu
await page.getByLabel("Clear").click();
await expect(page.getByLabel("# Change Events")).toHaveValue("2");
- await page
- .getByRole("button", { name: "Drop Video Here - or - Click to Upload" })
- .click();
+ await page.getByRole("button", { name: "Upload file" }).click();
await uploader.setInputFiles(["./test/files/file_test.ogg"]);
@@ -30,9 +26,7 @@ test("Video click-to-upload uploads video successfuly. Clear, play, and pause bu
});
test("Video play, pause events work correctly.", async ({ page }) => {
- await page
- .getByRole("button", { name: "Drop Video Here - or - Click to Upload" })
- .click();
+ await page.getByLabel("Upload file").click();
const uploader = await page.locator("input[type=file]");
await uploader.setInputFiles(["./test/files/file_test.ogg"]);
@@ -48,6 +42,7 @@ test("Video play, pause events work correctly.", async ({ page }) => {
test("Video drag-and-drop uploads a file to the server correctly.", async ({
page
}) => {
+ await page.getByLabel("Upload file").click();
await drag_and_drop_file(
page,
"input[type=file]",
@@ -62,6 +57,7 @@ test("Video drag-and-drop uploads a file to the server correctly.", async ({
test("Video drag-and-drop displays a warning when the file is of the wrong mime type.", async ({
page
}) => {
+ await page.getByLabel("Upload file").click();
await drag_and_drop_file(
page,
"input[type=file]",
diff --git a/js/atoms/src/SelectSource.svelte b/js/atoms/src/SelectSource.svelte
index a2db4585cb1f..6e01a3cff9e6 100644
--- a/js/atoms/src/SelectSource.svelte
+++ b/js/atoms/src/SelectSource.svelte
@@ -1,22 +1,34 @@
-{#if sources.length > 1}
+{#if unique_sources.length > 1}
{#if sources.includes("upload")}
handle_select_source("upload")}>
{/if}
@@ -25,10 +37,8 @@
class="icon"
class:selected={active_source === "microphone"}
aria-label="Record audio"
- on:click={() => {
- handle_clear();
- active_source = "microphone";
- }}> handle_select_source("microphone")}
+ >
{/if}
@@ -36,11 +46,17 @@
handle_select_source("webcam")}>
+ {/if}
+ {#if sources.includes("clipboard")}
+
{/if}
@@ -58,7 +74,6 @@
right: 0;
margin-left: auto;
margin-right: auto;
- align-self: flex-end;
}
.icon {
diff --git a/js/atoms/src/UploadText.svelte b/js/atoms/src/UploadText.svelte
index 84112f82980d..88f9a7d823d0 100644
--- a/js/atoms/src/UploadText.svelte
+++ b/js/atoms/src/UploadText.svelte
@@ -1,7 +1,8 @@
-
+
+ {#if type === "clipboard"}
+
+ {:else}
+
+ {/if}
+
{i18n(defs[type] || defs.file)}
diff --git a/js/audio/Index.svelte b/js/audio/Index.svelte
index 8529278d2f2c..a34278e524f5 100644
--- a/js/audio/Index.svelte
+++ b/js/audio/Index.svelte
@@ -85,7 +85,7 @@
let dragging: boolean;
- $: if (sources) {
+ $: if (!active_source && sources) {
active_source = sources[0];
}
diff --git a/js/audio/interactive/InteractiveAudio.svelte b/js/audio/interactive/InteractiveAudio.svelte
index 2db20039d464..21492620ebd6 100644
--- a/js/audio/interactive/InteractiveAudio.svelte
+++ b/js/audio/interactive/InteractiveAudio.svelte
@@ -249,7 +249,6 @@
bind:dragging
on:error={({ detail }) => dispatch("error", detail)}
{root}
- include_sources={sources.length > 1}
>
diff --git a/js/image/Image.stories.svelte b/js/image/Image.stories.svelte
index 579f53ca3d08..1d7d349130ae 100644
--- a/js/image/Image.stories.svelte
+++ b/js/image/Image.stories.svelte
@@ -1,25 +1,10 @@
-
+
+
+
{
+ const canvas = within(canvasElement);
+
+ const webcamButton = await canvas.findByLabelText("Capture from camera");
+ userEvent.click(webcamButton);
+
+ userEvent.click(await canvas.findByTitle("select video source"));
+ userEvent.click(await canvas.findByLabelText("select source"));
+ userEvent.click(await canvas.findByLabelText("Upload file"));
+ userEvent.click(await canvas.findByLabelText("Paste from clipboard"));
+ }}
+/>
+
+
+
+
diff --git a/js/image/Index.svelte b/js/image/Index.svelte
index 6b7618abbe47..cb40440db903 100644
--- a/js/image/Index.svelte
+++ b/js/image/Index.svelte
@@ -20,6 +20,8 @@
import type { LoadingStatus } from "@gradio/statustracker";
import { normalise_file } from "@gradio/client";
+ type sources = "upload" | "webcam" | "clipboard" | null;
+
export let elem_id = "";
export let elem_classes: string[] = [];
export let visible = true;
@@ -66,7 +68,7 @@
$: url && gradio.dispatch("change");
let dragging: boolean;
- let active_tool: null | "webcam" = null;
+ let active_source: sources = null;
{#if !interactive}
@@ -124,7 +126,7 @@
/>
gradio.dispatch("error", "bad thing happened")}
- on:error
{label}
{show_label}
{pending}
@@ -153,8 +153,10 @@
{mirror_webcam}
i18n={gradio.i18n}
>
- {#if sources.includes("upload")}
-
+ {#if active_source === "upload" || !active_source}
+
+ {:else if active_source === "clipboard"}
+
{:else}
{/if}
diff --git a/js/image/shared/ImageUploader.svelte b/js/image/shared/ImageUploader.svelte
index 91cb293e6866..6ded6dbe6131 100644
--- a/js/image/shared/ImageUploader.svelte
+++ b/js/image/shared/ImageUploader.svelte
@@ -4,28 +4,21 @@
import { Image as ImageIcon } from "@gradio/icons";
import type { SelectData, I18nFormatter } from "@gradio/utils";
import { get_coordinates_of_clicked_image } from "./utils";
- import {
- Webcam as WebcamIcon,
- ImagePaste,
- Upload as UploadIcon
- } from "@gradio/icons";
import Webcam from "./Webcam.svelte";
- import { Toolbar, IconButton } from "@gradio/atoms";
import { Upload } from "@gradio/upload";
import { type FileData, normalise_file } from "@gradio/client";
import ClearImage from "./ClearImage.svelte";
+ import { SelectSource } from "@gradio/atoms";
import Image from "./Image.svelte";
export let value: null | FileData;
export let label: string | undefined = undefined;
export let show_label: boolean;
- export let sources: ("clipboard" | "webcam" | "upload")[] = [
- "upload",
- "clipboard",
- "webcam"
- ];
+ type source_type = "upload" | "webcam" | "clipboard" | "microphone" | null;
+
+ export let sources: source_type[] = ["upload", "clipboard", "webcam"];
export let streaming = false;
export let pending = false;
export let mirror_webcam: boolean;
@@ -35,19 +28,24 @@
let upload: Upload;
let uploading = false;
- export let active_tool: "webcam" | null = null;
+ export let active_source: source_type = null;
function handle_upload({ detail }: CustomEvent): void {
value = normalise_file(detail, root, null);
dispatch("upload");
}
+ function handle_clear(): void {
+ value = null;
+ dispatch("clear");
+ dispatch("change", null);
+ }
+
async function handle_save(img_blob: Blob | any): Promise {
pending = true;
const f = await upload.load_files([new File([img_blob], `webcam.png`)]);
value = f?.[0] || null;
- if (!streaming) active_tool = null;
await tick();
@@ -55,7 +53,7 @@
pending = false;
}
- $: active_streaming = streaming && active_tool === "webcam";
+ $: active_streaming = streaming && active_source === "webcam";
$: if (uploading && !active_streaming) value = null;
$: value && !value.url && (value = normalise_file(value, root, null));
@@ -80,61 +78,16 @@
}
}
- const sources_meta = {
- upload: {
- icon: UploadIcon,
- label: i18n("Upload"),
- order: 0
- },
- webcam: {
- icon: WebcamIcon,
- label: i18n("Webcam"),
- order: 1
- },
- clipboard: {
- icon: ImagePaste,
- label: i18n("Paste"),
- order: 2
- }
- };
-
- $: sources_list = sources.sort(
- (a, b) => sources_meta[a].order - sources_meta[b].order
- );
-
- $: {
- if (sources.length === 1 && sources[0] === "webcam") {
- active_tool = "webcam";
- }
+ $: if (!active_source && sources) {
+ active_source = sources[0];
}
- async function handle_toolbar(
+ async function handle_select_source(
source: (typeof sources)[number]
): Promise {
switch (source) {
case "clipboard":
- navigator.clipboard.read().then(async (items) => {
- for (let i = 0; i < items.length; i++) {
- const type = items[i].types.find((t) => t.startsWith("image/"));
- if (type) {
- value = null;
- items[i].getType(type).then(async (blob) => {
- const f = await upload.load_files([
- new File([blob], `clipboard.${type.replace("image/", "")}`)
- ]);
- f;
- value = f?.[0] || null;
- });
- break;
- }
- }
- });
- break;
- case "webcam":
- active_tool = "webcam";
- break;
- case "upload":
- upload.open_file_upload();
+ upload.paste_clipboard();
break;
default:
break;
@@ -155,21 +108,21 @@
{/if}
- {#if value === null && !active_tool}
+ {#if value === null}
{/if}
- {#if active_tool === "webcam"}
+ {#if active_source === "webcam" && !value}
handle_save(e.detail)}
on:stream={(e) => handle_save(e.detail)}
@@ -191,17 +144,12 @@
{/if}
{#if sources.length > 1 || sources.includes("clipboard")}
-
- {#each sources_list as source}
- handle_toolbar(source)}
- Icon={sources_meta[source].icon}
- size="large"
- label="{source}-image-toolbar-btn"
- padded={false}
- />
- {/each}
-
+
{/if}
@@ -209,6 +157,13 @@
.image-frame :global(img) {
width: var(--size-full);
height: var(--size-full);
+ object-fit: cover;
+ }
+
+ .image-frame {
+ object-fit: cover;
+ width: 100%;
+ height: 100%;
}
.upload-container {
diff --git a/js/image/shared/Webcam.svelte b/js/image/shared/Webcam.svelte
index c1ea8a88f2dc..a1385ef1c355 100644
--- a/js/image/shared/Webcam.svelte
+++ b/js/image/shared/Webcam.svelte
@@ -182,7 +182,7 @@
{#if !streaming}
-
+ {#if options_open}
+
+ {/if}
{/if}
@@ -243,36 +246,28 @@
position: relative;
width: var(--size-full);
height: var(--size-full);
- min-height: var(--size-60);
}
video {
width: var(--size-full);
height: var(--size-full);
+ object-fit: cover;
}
.button-wrap {
- display: flex;
position: absolute;
- right: 0px;
+ background-color: var(--block-background-fill);
+ border: 1px solid var(--border-color-primary);
+ border-radius: var(--radius-xl);
+ padding: var(--size-1-5);
+ display: flex;
bottom: var(--size-2);
- left: 0px;
- justify-content: center;
- align-items: center;
- margin: auto;
+ left: 50%;
+ transform: translate(-50%, 0);
box-shadow: var(--shadow-drop-lg);
border-radius: var(--radius-xl);
- background-color: rgba(0, 0, 0, 0.9);
- width: var(--size-10);
- height: var(--size-8);
- padding: var(--size-2-5);
- padding-right: var(--size-1);
- z-index: var(--layer-3);
- }
-
- .capture {
- width: var(--size-14);
- transform: translateX(var(--size-2-5));
+ line-height: var(--size-3);
+ color: var(--button-secondary-text-color);
}
@media (--screen-md) {
@@ -289,9 +284,8 @@
.icon {
opacity: 0.8;
- width: 100%;
- height: 100%;
- color: white;
+ width: 18px;
+ height: 18px;
display: flex;
justify-content: space-between;
align-items: center;
@@ -310,35 +304,39 @@
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
+ color: var(--button-secondary-text-color);
background-color: transparent;
- border: none;
- width: auto;
- font-size: 1rem;
- /* padding: 0.5rem; */
- width: max-content;
+ width: 95%;
+ font-size: var(--text-md);
position: absolute;
- top: 0;
- right: 0;
+ bottom: var(--size-2);
background-color: var(--block-background-fill);
box-shadow: var(--shadow-drop-lg);
border-radius: var(--radius-xl);
z-index: var(--layer-top);
- border: 1px solid var(--border-color-accent);
+ border: 1px solid var(--border-color-primary);
text-align: left;
- overflow: hidden;
+ line-height: var(--size-4);
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ left: 50%;
+ transform: translate(-50%, 0);
+ max-width: var(--size-52);
}
- .select-wrap > div {
+ .select-wrap > option {
padding: 0.25rem 0.5rem;
border-bottom: 1px solid var(--border-color-accent);
padding-right: var(--size-8);
+ text-overflow: ellipsis;
+ overflow: hidden;
}
- .select-wrap > div:hover {
+ .select-wrap > option:hover {
background-color: var(--color-accent);
}
- .select-wrap > div:last-child {
+ .select-wrap > option:last-child {
border: none;
}
diff --git a/js/upload/src/Upload.svelte b/js/upload/src/Upload.svelte
index be104c5cdc5f..039c1e72700a 100644
--- a/js/upload/src/Upload.svelte
+++ b/js/upload/src/Upload.svelte
@@ -15,7 +15,6 @@
export let root: string;
export let hidden = false;
export let format: "blob" | "file" = "file";
- export let include_sources = false;
export let uploading = false;
let upload_id: string;
@@ -44,6 +43,26 @@
dragging = !dragging;
}
+ export function paste_clipboard(): void {
+ navigator.clipboard.read().then(async (items) => {
+ 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]);
+ });
+ break;
+ }
+ }
+ });
+ }
+
export function open_file_upload(): void {
if (disable_click) return;
hidden_upload.value = "";
@@ -126,7 +145,19 @@
}
-{#if uploading}
+{#if filetype === "clipboard"}
+
+{:else if uploading}
{#if !hidden}
{/if}
@@ -136,7 +167,7 @@
class:center
class:boundedheight
class:flex
- style:height={include_sources ? "calc(100% - 40px" : "100%"}
+ style:height="100%"
tabindex={hidden ? -1 : 0}
on:drag|preventDefault|stopPropagation
on:dragstart|preventDefault|stopPropagation
diff --git a/js/video/Index.svelte b/js/video/Index.svelte
index 50e405065310..540eeac18101 100644
--- a/js/video/Index.svelte
+++ b/js/video/Index.svelte
@@ -73,12 +73,8 @@
value = initial_value;
};
- $: if (sources) {
- if (sources.length > 1) {
- active_source = "upload";
- } else {
- active_source = sources[0];
- }
+ $: if (sources && !active_source) {
+ active_source = sources[0];
}
$: {
diff --git a/js/video/shared/InteractiveVideo.svelte b/js/video/shared/InteractiveVideo.svelte
index c6ad8780950d..2b01da2d7c59 100644
--- a/js/video/shared/InteractiveVideo.svelte
+++ b/js/video/shared/InteractiveVideo.svelte
@@ -62,59 +62,62 @@
-{#if value === null || value.url === undefined}
- {#if active_source === "upload"}
- dispatch("error", detail)}
- {root}
- include_sources={sources.length > 1}
- >
-
-
- {:else if active_source === "webcam"}
- dispatch("change")}
- on:start_recording
- on:stop_recording
- {i18n}
- />
- {/if}
-{:else}
-
- {#if playable()}
- {#key value?.url}
-
- {/key}
- {:else if value.size}
- {value.orig_name || value.url}
-
- {prettyBytes(value.size)}
+
+ {#if value === null || value.url === undefined}
+
+ {#if active_source === "upload"}
+ dispatch("error", detail)}
+ {root}
+ >
+
+
+ {:else if active_source === "webcam"}
+ dispatch("change")}
+ on:start_recording
+ on:stop_recording
+ {i18n}
+ />
+ {/if}
+ {:else}
+
+ {#if playable()}
+ {#key value?.url}
+
+ {/key}
+ {:else if value.size}
+
{value.orig_name || value.url}
+
+ {prettyBytes(value.size)}
+
+ {/if}
{/if}
-{/if}
-
+
+