@gradio/video 0.20.0 → 0.20.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,33 @@
1
1
  # @gradio/video
2
2
 
3
+ ## 0.20.2
4
+
5
+ ### Dependency updates
6
+
7
+ - @gradio/atoms@0.21.0
8
+ - @gradio/client@2.0.4
9
+ - @gradio/statustracker@0.12.3
10
+ - @gradio/image@0.25.2
11
+ - @gradio/upload@0.17.5
12
+
13
+ ## 0.20.1
14
+
15
+ ### Fixes
16
+
17
+ - [#12800](https://github.com/gradio-app/gradio/pull/12800) [`7a1c321`](https://github.com/gradio-app/gradio/commit/7a1c321b6546ba05a353488f5133e8262c4a8a39) - Bump svelte/kit for security reasons. Thanks @freddyaboulton!
18
+ - [#12758](https://github.com/gradio-app/gradio/pull/12758) [`fb4b92a`](https://github.com/gradio-app/gradio/commit/fb4b92afe9cf43c0d35ba0293496058d967ad818) - Add volume control to gr.Video. Thanks @hysts!
19
+ - [#12779](https://github.com/gradio-app/gradio/pull/12779) [`ea2d3e9`](https://github.com/gradio-app/gradio/commit/ea2d3e985a8b42d188e551f517c5825c00790628) - Migrate Audio + Upload + Atoms to Svelte 5. Thanks @dawoodkhan82!
20
+
21
+ ### Dependency updates
22
+
23
+ - @gradio/statustracker@0.12.2
24
+ - @gradio/atoms@0.20.1
25
+ - @gradio/utils@0.11.2
26
+ - @gradio/icons@0.15.1
27
+ - @gradio/upload@0.17.4
28
+ - @gradio/client@2.0.3
29
+ - @gradio/image@0.25.1
30
+
3
31
  ## 0.20.0
4
32
 
5
33
  ### Features
@@ -1,12 +1,13 @@
1
- <script context="module">
2
- import { Template, Story } from "@storybook/addon-svelte-csf";
1
+ <script module>
2
+ import { defineMeta } from "@storybook/addon-svelte-csf";
3
3
  import Video from "./Index.svelte";
4
- import { format } from "svelte-i18n";
5
- import { get } from "svelte/store";
6
- import { userEvent, within } from "@storybook/test";
4
+ import { userEvent, within } from "storybook/test";
7
5
  import { allModes } from "../storybook/modes";
6
+ import { wrapProps } from "../storybook/wrapProps";
8
7
 
9
- export const meta = {
8
+ const video_sample = "/video_sample.mp4";
9
+
10
+ const { Story } = defineMeta({
10
11
  title: "Components/Video",
11
12
  component: Video,
12
13
  parameters: {
@@ -17,14 +18,12 @@
17
18
  }
18
19
  }
19
20
  }
20
- };
21
+ });
21
22
  </script>
22
23
 
23
- <div>
24
- <Template let:args>
25
- <Video i18n={get(format)} {...args} />
26
- </Template>
27
- </div>
24
+ {#snippet template(args)}
25
+ <Video {...wrapProps(args)} />
26
+ {/snippet}
28
27
 
29
28
  <Story
30
29
  name="Record from webcam"
@@ -35,54 +34,46 @@
35
34
  interactive: true,
36
35
  height: 400,
37
36
  width: 400,
38
- webcam_options: {
39
- mirror: true,
40
- constraints: null
41
- }
37
+ webcam_options: { mirror: true, constraints: null }
42
38
  }}
39
+ {template}
43
40
  />
44
-
45
41
  <Story
46
42
  name="Static video"
47
43
  args={{
48
44
  value: {
49
- path: "https://gradio-static-files.s3.us-west-2.amazonaws.com/world.mp4",
50
- url: "https://gradio-static-files.s3.us-west-2.amazonaws.com/world.mp4",
51
- orig_name: "world.mp4"
45
+ path: video_sample,
46
+ url: video_sample,
47
+ orig_name: "video_sample.mp4"
52
48
  },
53
49
  label: "world video",
54
50
  show_label: true,
55
- show_download_button: true,
51
+ buttons: ["download"],
56
52
  interactive: false,
57
53
  height: 200,
58
54
  width: 400,
59
- webcam_options: {
60
- mirror: true,
61
- constraints: null
62
- }
55
+ webcam_options: { mirror: true, constraints: null }
63
56
  }}
57
+ {template}
64
58
  />
65
59
  <Story
66
60
  name="Static video with vertical video"
67
61
  args={{
68
62
  value: {
69
- path: "https://gradio-static-files.s3.us-west-2.amazonaws.com/world_vertical.mp4",
70
- url: "https://gradio-static-files.s3.us-west-2.amazonaws.com/world_vertical.mp4",
71
- orig_name: "world_vertical.mp4"
63
+ path: video_sample,
64
+ url: video_sample,
65
+ orig_name: "video_sample.mp4"
72
66
  },
73
67
  label: "world video",
74
68
  show_label: true,
75
- show_download_button: false,
69
+ buttons: [],
76
70
  interactive: false,
77
71
  height: 200,
78
72
  width: 400,
79
- webcam_options: {
80
- mirror: true,
81
- constraints: null
82
- }
73
+ webcam_options: { mirror: true, constraints: null }
83
74
  }}
75
+ {template}
84
76
  />
85
-
86
77
  <Story
87
78
  name="Upload video"
88
79
  args={{
@@ -93,13 +84,10 @@
93
84
  width: 400,
94
85
  height: 400,
95
86
  value: null,
96
- webcam_options: {
97
- mirror: true,
98
- constraints: null
99
- }
87
+ webcam_options: { mirror: true, constraints: null }
100
88
  }}
89
+ {template}
101
90
  />
102
-
103
91
  <Story
104
92
  name="Upload video with download button"
105
93
  args={{
@@ -107,42 +95,37 @@
107
95
  show_label: true,
108
96
  interactive: true,
109
97
  sources: ["upload", "webcam"],
110
- show_download_button: true,
98
+ buttons: ["download"],
111
99
  width: 400,
112
100
  height: 400,
113
101
  value: {
114
- path: "https://gradio-static-files.s3.us-west-2.amazonaws.com/world.mp4",
115
- url: "https://gradio-static-files.s3.us-west-2.amazonaws.com/world.mp4",
116
- orig_name: "world.mp4"
102
+ path: video_sample,
103
+ url: video_sample,
104
+ orig_name: "video_sample.mp4"
117
105
  },
118
- webcam_options: {
119
- mirror: true,
120
- constraints: null
121
- }
106
+ webcam_options: { mirror: true, constraints: null }
122
107
  }}
108
+ {template}
123
109
  />
124
-
125
110
  <Story
126
111
  name="Trim video"
127
112
  args={{
128
113
  value: {
129
- path: "https://gradio-static-files.s3.us-west-2.amazonaws.com/world.mp4",
130
- url: "https://gradio-static-files.s3.us-west-2.amazonaws.com/world.mp4",
131
- orig_name: "world.mp4"
114
+ path: video_sample,
115
+ url: video_sample,
116
+ orig_name: "video_sample.mp4"
132
117
  },
133
118
  label: "world video",
134
119
  show_label: true,
135
120
  interactive: "true",
136
121
  sources: ["upload"],
137
122
  width: 400,
138
- webcam_options: {
139
- mirror: true,
140
- constraints: null
141
- }
123
+ webcam_options: { mirror: true, constraints: null }
142
124
  }}
143
125
  play={async ({ canvasElement }) => {
144
126
  const canvas = within(canvasElement);
145
127
  const trimButton = canvas.getByLabelText("Trim video to selection");
146
128
  userEvent.click(trimButton);
147
129
  }}
130
+ {template}
148
131
  />
@@ -51,7 +51,7 @@
51
51
  stop_recording?: never;
52
52
  }>();
53
53
 
54
- function handle_load({ detail }: CustomEvent<FileData | null>): void {
54
+ function handle_load(detail: FileData | null): void {
55
55
  value = detail;
56
56
  dispatch("change", detail);
57
57
  dispatch("upload", detail!);
@@ -88,9 +88,9 @@
88
88
  bind:dragging
89
89
  bind:uploading
90
90
  filetype="video/x-m4v,video/*"
91
- on:load={handle_load}
91
+ onload={handle_load}
92
92
  {max_file_size}
93
- on:error={({ detail }) => dispatch("error", detail)}
93
+ onerror={(detail) => dispatch("error", detail)}
94
94
  {root}
95
95
  {upload}
96
96
  {stream_handler}
@@ -1,8 +1,10 @@
1
1
  <script lang="ts">
2
- import { createEventDispatcher } from "svelte";
2
+ import { createEventDispatcher, onMount, onDestroy } from "svelte";
3
3
  import { Play, Pause, Maximize, Undo } from "@gradio/icons";
4
4
  import Video from "./Video.svelte";
5
5
  import VideoControls from "./VideoControls.svelte";
6
+ import VolumeControl from "./VolumeControl.svelte";
7
+ import VolumeLevels from "../../audio/shared/VolumeLevels.svelte";
6
8
  import type { FileData, Client } from "@gradio/client";
7
9
  import { prepare_files } from "@gradio/client";
8
10
  import { format_time } from "@gradio/utils";
@@ -40,6 +42,9 @@
40
42
  let paused = true;
41
43
  let video: HTMLVideoElement;
42
44
  let processingVideo = false;
45
+ let show_volume_slider = false;
46
+ let current_volume = 1;
47
+ let is_fullscreen = false;
43
48
 
44
49
  function handleMove(e: TouchEvent | MouseEvent): void {
45
50
  if (!duration) return;
@@ -96,7 +101,51 @@
96
101
  };
97
102
 
98
103
  function open_full_screen(): void {
99
- video.requestFullscreen();
104
+ if (!is_fullscreen) {
105
+ video.requestFullscreen();
106
+ } else {
107
+ document.exitFullscreen();
108
+ }
109
+ }
110
+
111
+ function handleFullscreenChange(): void {
112
+ is_fullscreen = document.fullscreenElement === video;
113
+ if (video) {
114
+ video.controls = is_fullscreen;
115
+ }
116
+ }
117
+
118
+ let last_synced_volume = 1;
119
+ let previous_video: HTMLVideoElement | undefined;
120
+ // Tolerance for floating-point comparison of volume values
121
+ const VOLUME_EPSILON = 0.001;
122
+
123
+ function handleVolumeChange(): void {
124
+ if (video && Math.abs(video.volume - last_synced_volume) > VOLUME_EPSILON) {
125
+ current_volume = video.volume;
126
+ last_synced_volume = video.volume;
127
+ }
128
+ }
129
+
130
+ onMount(() => {
131
+ document.addEventListener("fullscreenchange", handleFullscreenChange);
132
+ return () => {
133
+ document.removeEventListener("fullscreenchange", handleFullscreenChange);
134
+ };
135
+ });
136
+
137
+ onDestroy(() => {
138
+ if (video) {
139
+ video.removeEventListener("volumechange", handleVolumeChange);
140
+ }
141
+ });
142
+
143
+ $: if (video && video !== previous_video) {
144
+ if (previous_video) {
145
+ previous_video.removeEventListener("volumechange", handleVolumeChange);
146
+ }
147
+ video.addEventListener("volumechange", handleVolumeChange);
148
+ previous_video = video;
100
149
  }
101
150
 
102
151
  $: time = time || 0;
@@ -105,6 +154,16 @@
105
154
  $: if (playback_position !== time && video) {
106
155
  video.currentTime = playback_position;
107
156
  }
157
+ $: if (video && !is_fullscreen) {
158
+ if (Math.abs(video.volume - current_volume) > VOLUME_EPSILON) {
159
+ video.volume = current_volume;
160
+ last_synced_volume = current_volume;
161
+ }
162
+ video.controls = false;
163
+ }
164
+ $: if (video && is_fullscreen) {
165
+ last_synced_volume = video.volume;
166
+ }
108
167
  </script>
109
168
 
110
169
  <div class="wrap">
@@ -115,6 +174,7 @@
115
174
  {autoplay}
116
175
  {loop}
117
176
  {is_stream}
177
+ controls={is_fullscreen}
118
178
  on:click={play_pause}
119
179
  on:play
120
180
  on:pause
@@ -165,16 +225,33 @@
165
225
  on:click|stopPropagation|preventDefault={handle_click}
166
226
  />
167
227
 
168
- <div
169
- role="button"
170
- tabindex="0"
171
- class="icon"
172
- aria-label="full-screen"
173
- on:click={open_full_screen}
174
- on:keypress={open_full_screen}
175
- >
176
- <Maximize />
228
+ <div class="volume-control-wrapper">
229
+ <button
230
+ class="icon volume-button"
231
+ style:color={show_volume_slider ? "var(--color-accent)" : "white"}
232
+ aria-label="Adjust volume"
233
+ on:click={() => (show_volume_slider = !show_volume_slider)}
234
+ >
235
+ <VolumeLevels currentVolume={current_volume} />
236
+ </button>
237
+
238
+ {#if show_volume_slider}
239
+ <VolumeControl bind:current_volume bind:show_volume_slider />
240
+ {/if}
177
241
  </div>
242
+
243
+ {#if !show_volume_slider}
244
+ <div
245
+ role="button"
246
+ tabindex="0"
247
+ class="icon"
248
+ aria-label="full-screen"
249
+ on:click={open_full_screen}
250
+ on:keypress={open_full_screen}
251
+ >
252
+ <Maximize />
253
+ </div>
254
+ {/if}
178
255
  </div>
179
256
  </div>
180
257
  </div>
@@ -236,10 +313,14 @@
236
313
  padding: var(--size-2) var(--size-1);
237
314
  width: calc(100% - 0.375rem * 2);
238
315
  width: calc(100% - var(--size-2) * 2);
316
+ z-index: 10;
239
317
  }
240
318
  .wrap:hover .controls {
241
319
  opacity: 1;
242
320
  }
321
+ :global(:fullscreen) .controls {
322
+ display: none;
323
+ }
243
324
 
244
325
  .inner {
245
326
  display: flex;
@@ -259,6 +340,25 @@
259
340
  color: white;
260
341
  }
261
342
 
343
+ .volume-control-wrapper {
344
+ position: relative;
345
+ display: flex;
346
+ align-items: center;
347
+ margin-right: var(--spacing-md);
348
+ }
349
+
350
+ .volume-button {
351
+ display: flex;
352
+ justify-content: center;
353
+ align-items: center;
354
+ cursor: pointer;
355
+ width: var(--size-6);
356
+ color: white;
357
+ border: none;
358
+ background: none;
359
+ padding: 0;
360
+ }
361
+
262
362
  .time {
263
363
  flex-shrink: 0;
264
364
  margin-right: var(--size-3);
@@ -98,7 +98,7 @@
98
98
 
99
99
  <ModifyUpload
100
100
  {i18n}
101
- on:clear={() => handle_clear()}
101
+ onclear={() => handle_clear()}
102
102
  download={show_download_button ? value?.url : null}
103
103
  >
104
104
  {#if showRedo && mode === ""}
@@ -106,7 +106,7 @@
106
106
  Icon={Undo}
107
107
  label="Reset video to initial value"
108
108
  disabled={processingVideo || !has_change_history}
109
- on:click={() => {
109
+ onclick={() => {
110
110
  handle_reset_value();
111
111
  mode = "";
112
112
  }}
@@ -118,7 +118,7 @@
118
118
  Icon={Trim}
119
119
  label="Trim video to selection"
120
120
  disabled={processingVideo}
121
- on:click={toggleTrimmingMode}
121
+ onclick={toggleTrimmingMode}
122
122
  />
123
123
  {/if}
124
124
  </ModifyUpload>
@@ -0,0 +1,75 @@
1
+ <script lang="ts">
2
+ import { onMount } from "svelte";
3
+
4
+ export let current_volume = 1;
5
+ export let show_volume_slider = false;
6
+
7
+ let volume_element: HTMLInputElement;
8
+
9
+ onMount(() => {
10
+ adjustSlider();
11
+ });
12
+
13
+ const adjustSlider = (): void => {
14
+ let slider = volume_element;
15
+ if (!slider) return;
16
+
17
+ slider.style.background = `linear-gradient(to right, white ${
18
+ current_volume * 100
19
+ }%, rgba(255, 255, 255, 0.3) ${current_volume * 100}%)`;
20
+ };
21
+
22
+ $: (current_volume, adjustSlider());
23
+ </script>
24
+
25
+ <input
26
+ bind:this={volume_element}
27
+ id="volume"
28
+ class="volume-slider"
29
+ type="range"
30
+ min="0"
31
+ max="1"
32
+ step="0.01"
33
+ value={current_volume}
34
+ on:focusout={() => (show_volume_slider = false)}
35
+ on:input={(e) => {
36
+ if (e.target instanceof HTMLInputElement) {
37
+ current_volume = parseFloat(e.target.value);
38
+ }
39
+ }}
40
+ />
41
+
42
+ <style>
43
+ .volume-slider {
44
+ -webkit-appearance: none;
45
+ appearance: none;
46
+ width: var(--size-20);
47
+ accent-color: var(--color-accent);
48
+ height: 4px;
49
+ cursor: pointer;
50
+ outline: none;
51
+ border-radius: 15px;
52
+ background-color: rgba(255, 255, 255, 0.3);
53
+ margin-left: var(--spacing-sm);
54
+ }
55
+
56
+ input[type="range"]::-webkit-slider-thumb {
57
+ -webkit-appearance: none;
58
+ appearance: none;
59
+ height: 15px;
60
+ width: 15px;
61
+ background-color: white;
62
+ border-radius: 50%;
63
+ border: none;
64
+ transition: 0.2s ease-in-out;
65
+ }
66
+
67
+ input[type="range"]::-moz-range-thumb {
68
+ height: 15px;
69
+ width: 15px;
70
+ background-color: white;
71
+ border-radius: 50%;
72
+ border: none;
73
+ transition: 0.2s ease-in-out;
74
+ }
75
+ </style>
@@ -0,0 +1,21 @@
1
+ interface $$__sveltets_2_IsomorphicComponent<Props extends Record<string, any> = any, Events extends Record<string, any> = any, Slots extends Record<string, any> = any, Exports = {}, Bindings = string> {
2
+ new (options: import('svelte').ComponentConstructorOptions<Props>): import('svelte').SvelteComponent<Props, Events, Slots> & {
3
+ $$bindings?: Bindings;
4
+ } & Exports;
5
+ (internal: unknown, props: Props & {
6
+ $$events?: Events;
7
+ $$slots?: Slots;
8
+ }): Exports & {
9
+ $set?: any;
10
+ $on?: any;
11
+ };
12
+ z_$$bindings?: Bindings;
13
+ }
14
+ declare const VolumeControl: $$__sveltets_2_IsomorphicComponent<{
15
+ current_volume?: number;
16
+ show_volume_slider?: boolean;
17
+ }, {
18
+ [evt: string]: CustomEvent<any>;
19
+ }, {}, {}, string>;
20
+ type VolumeControl = InstanceType<typeof VolumeControl>;
21
+ export default VolumeControl;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gradio/video",
3
- "version": "0.20.0",
3
+ "version": "0.20.2",
4
4
  "description": "Gradio UI packages",
5
5
  "type": "module",
6
6
  "author": "",
@@ -11,16 +11,16 @@
11
11
  "@ffmpeg/util": "^0.12.2",
12
12
  "hls.js": "^1.6.13",
13
13
  "mrmime": "^2.0.1",
14
- "@gradio/atoms": "^0.20.0",
15
- "@gradio/client": "^2.0.2",
16
- "@gradio/icons": "^0.15.0",
17
- "@gradio/image": "^0.25.0",
18
- "@gradio/statustracker": "^0.12.1",
19
- "@gradio/upload": "^0.17.3",
20
- "@gradio/utils": "^0.11.1"
14
+ "@gradio/atoms": "^0.21.0",
15
+ "@gradio/client": "^2.0.4",
16
+ "@gradio/icons": "^0.15.1",
17
+ "@gradio/image": "^0.25.2",
18
+ "@gradio/statustracker": "^0.12.3",
19
+ "@gradio/upload": "^0.17.5",
20
+ "@gradio/utils": "^0.11.2"
21
21
  },
22
22
  "devDependencies": {
23
- "@gradio/preview": "^0.15.1"
23
+ "@gradio/preview": "^0.15.2"
24
24
  },
25
25
  "exports": {
26
26
  "./package.json": "./package.json",
@@ -46,7 +46,7 @@
46
46
  }
47
47
  },
48
48
  "peerDependencies": {
49
- "svelte": "^5.43.4"
49
+ "svelte": "^5.48.0"
50
50
  },
51
51
  "main": "index.ts",
52
52
  "main_changeset": true,
@@ -51,7 +51,7 @@
51
51
  stop_recording?: never;
52
52
  }>();
53
53
 
54
- function handle_load({ detail }: CustomEvent<FileData | null>): void {
54
+ function handle_load(detail: FileData | null): void {
55
55
  value = detail;
56
56
  dispatch("change", detail);
57
57
  dispatch("upload", detail!);
@@ -88,9 +88,9 @@
88
88
  bind:dragging
89
89
  bind:uploading
90
90
  filetype="video/x-m4v,video/*"
91
- on:load={handle_load}
91
+ onload={handle_load}
92
92
  {max_file_size}
93
- on:error={({ detail }) => dispatch("error", detail)}
93
+ onerror={(detail) => dispatch("error", detail)}
94
94
  {root}
95
95
  {upload}
96
96
  {stream_handler}
@@ -1,8 +1,10 @@
1
1
  <script lang="ts">
2
- import { createEventDispatcher } from "svelte";
2
+ import { createEventDispatcher, onMount, onDestroy } from "svelte";
3
3
  import { Play, Pause, Maximize, Undo } from "@gradio/icons";
4
4
  import Video from "./Video.svelte";
5
5
  import VideoControls from "./VideoControls.svelte";
6
+ import VolumeControl from "./VolumeControl.svelte";
7
+ import VolumeLevels from "../../audio/shared/VolumeLevels.svelte";
6
8
  import type { FileData, Client } from "@gradio/client";
7
9
  import { prepare_files } from "@gradio/client";
8
10
  import { format_time } from "@gradio/utils";
@@ -40,6 +42,9 @@
40
42
  let paused = true;
41
43
  let video: HTMLVideoElement;
42
44
  let processingVideo = false;
45
+ let show_volume_slider = false;
46
+ let current_volume = 1;
47
+ let is_fullscreen = false;
43
48
 
44
49
  function handleMove(e: TouchEvent | MouseEvent): void {
45
50
  if (!duration) return;
@@ -96,7 +101,51 @@
96
101
  };
97
102
 
98
103
  function open_full_screen(): void {
99
- video.requestFullscreen();
104
+ if (!is_fullscreen) {
105
+ video.requestFullscreen();
106
+ } else {
107
+ document.exitFullscreen();
108
+ }
109
+ }
110
+
111
+ function handleFullscreenChange(): void {
112
+ is_fullscreen = document.fullscreenElement === video;
113
+ if (video) {
114
+ video.controls = is_fullscreen;
115
+ }
116
+ }
117
+
118
+ let last_synced_volume = 1;
119
+ let previous_video: HTMLVideoElement | undefined;
120
+ // Tolerance for floating-point comparison of volume values
121
+ const VOLUME_EPSILON = 0.001;
122
+
123
+ function handleVolumeChange(): void {
124
+ if (video && Math.abs(video.volume - last_synced_volume) > VOLUME_EPSILON) {
125
+ current_volume = video.volume;
126
+ last_synced_volume = video.volume;
127
+ }
128
+ }
129
+
130
+ onMount(() => {
131
+ document.addEventListener("fullscreenchange", handleFullscreenChange);
132
+ return () => {
133
+ document.removeEventListener("fullscreenchange", handleFullscreenChange);
134
+ };
135
+ });
136
+
137
+ onDestroy(() => {
138
+ if (video) {
139
+ video.removeEventListener("volumechange", handleVolumeChange);
140
+ }
141
+ });
142
+
143
+ $: if (video && video !== previous_video) {
144
+ if (previous_video) {
145
+ previous_video.removeEventListener("volumechange", handleVolumeChange);
146
+ }
147
+ video.addEventListener("volumechange", handleVolumeChange);
148
+ previous_video = video;
100
149
  }
101
150
 
102
151
  $: time = time || 0;
@@ -105,6 +154,16 @@
105
154
  $: if (playback_position !== time && video) {
106
155
  video.currentTime = playback_position;
107
156
  }
157
+ $: if (video && !is_fullscreen) {
158
+ if (Math.abs(video.volume - current_volume) > VOLUME_EPSILON) {
159
+ video.volume = current_volume;
160
+ last_synced_volume = current_volume;
161
+ }
162
+ video.controls = false;
163
+ }
164
+ $: if (video && is_fullscreen) {
165
+ last_synced_volume = video.volume;
166
+ }
108
167
  </script>
109
168
 
110
169
  <div class="wrap">
@@ -115,6 +174,7 @@
115
174
  {autoplay}
116
175
  {loop}
117
176
  {is_stream}
177
+ controls={is_fullscreen}
118
178
  on:click={play_pause}
119
179
  on:play
120
180
  on:pause
@@ -165,16 +225,33 @@
165
225
  on:click|stopPropagation|preventDefault={handle_click}
166
226
  />
167
227
 
168
- <div
169
- role="button"
170
- tabindex="0"
171
- class="icon"
172
- aria-label="full-screen"
173
- on:click={open_full_screen}
174
- on:keypress={open_full_screen}
175
- >
176
- <Maximize />
228
+ <div class="volume-control-wrapper">
229
+ <button
230
+ class="icon volume-button"
231
+ style:color={show_volume_slider ? "var(--color-accent)" : "white"}
232
+ aria-label="Adjust volume"
233
+ on:click={() => (show_volume_slider = !show_volume_slider)}
234
+ >
235
+ <VolumeLevels currentVolume={current_volume} />
236
+ </button>
237
+
238
+ {#if show_volume_slider}
239
+ <VolumeControl bind:current_volume bind:show_volume_slider />
240
+ {/if}
177
241
  </div>
242
+
243
+ {#if !show_volume_slider}
244
+ <div
245
+ role="button"
246
+ tabindex="0"
247
+ class="icon"
248
+ aria-label="full-screen"
249
+ on:click={open_full_screen}
250
+ on:keypress={open_full_screen}
251
+ >
252
+ <Maximize />
253
+ </div>
254
+ {/if}
178
255
  </div>
179
256
  </div>
180
257
  </div>
@@ -236,10 +313,14 @@
236
313
  padding: var(--size-2) var(--size-1);
237
314
  width: calc(100% - 0.375rem * 2);
238
315
  width: calc(100% - var(--size-2) * 2);
316
+ z-index: 10;
239
317
  }
240
318
  .wrap:hover .controls {
241
319
  opacity: 1;
242
320
  }
321
+ :global(:fullscreen) .controls {
322
+ display: none;
323
+ }
243
324
 
244
325
  .inner {
245
326
  display: flex;
@@ -259,6 +340,25 @@
259
340
  color: white;
260
341
  }
261
342
 
343
+ .volume-control-wrapper {
344
+ position: relative;
345
+ display: flex;
346
+ align-items: center;
347
+ margin-right: var(--spacing-md);
348
+ }
349
+
350
+ .volume-button {
351
+ display: flex;
352
+ justify-content: center;
353
+ align-items: center;
354
+ cursor: pointer;
355
+ width: var(--size-6);
356
+ color: white;
357
+ border: none;
358
+ background: none;
359
+ padding: 0;
360
+ }
361
+
262
362
  .time {
263
363
  flex-shrink: 0;
264
364
  margin-right: var(--size-3);
@@ -98,7 +98,7 @@
98
98
 
99
99
  <ModifyUpload
100
100
  {i18n}
101
- on:clear={() => handle_clear()}
101
+ onclear={() => handle_clear()}
102
102
  download={show_download_button ? value?.url : null}
103
103
  >
104
104
  {#if showRedo && mode === ""}
@@ -106,7 +106,7 @@
106
106
  Icon={Undo}
107
107
  label="Reset video to initial value"
108
108
  disabled={processingVideo || !has_change_history}
109
- on:click={() => {
109
+ onclick={() => {
110
110
  handle_reset_value();
111
111
  mode = "";
112
112
  }}
@@ -118,7 +118,7 @@
118
118
  Icon={Trim}
119
119
  label="Trim video to selection"
120
120
  disabled={processingVideo}
121
- on:click={toggleTrimmingMode}
121
+ onclick={toggleTrimmingMode}
122
122
  />
123
123
  {/if}
124
124
  </ModifyUpload>
@@ -0,0 +1,75 @@
1
+ <script lang="ts">
2
+ import { onMount } from "svelte";
3
+
4
+ export let current_volume = 1;
5
+ export let show_volume_slider = false;
6
+
7
+ let volume_element: HTMLInputElement;
8
+
9
+ onMount(() => {
10
+ adjustSlider();
11
+ });
12
+
13
+ const adjustSlider = (): void => {
14
+ let slider = volume_element;
15
+ if (!slider) return;
16
+
17
+ slider.style.background = `linear-gradient(to right, white ${
18
+ current_volume * 100
19
+ }%, rgba(255, 255, 255, 0.3) ${current_volume * 100}%)`;
20
+ };
21
+
22
+ $: (current_volume, adjustSlider());
23
+ </script>
24
+
25
+ <input
26
+ bind:this={volume_element}
27
+ id="volume"
28
+ class="volume-slider"
29
+ type="range"
30
+ min="0"
31
+ max="1"
32
+ step="0.01"
33
+ value={current_volume}
34
+ on:focusout={() => (show_volume_slider = false)}
35
+ on:input={(e) => {
36
+ if (e.target instanceof HTMLInputElement) {
37
+ current_volume = parseFloat(e.target.value);
38
+ }
39
+ }}
40
+ />
41
+
42
+ <style>
43
+ .volume-slider {
44
+ -webkit-appearance: none;
45
+ appearance: none;
46
+ width: var(--size-20);
47
+ accent-color: var(--color-accent);
48
+ height: 4px;
49
+ cursor: pointer;
50
+ outline: none;
51
+ border-radius: 15px;
52
+ background-color: rgba(255, 255, 255, 0.3);
53
+ margin-left: var(--spacing-sm);
54
+ }
55
+
56
+ input[type="range"]::-webkit-slider-thumb {
57
+ -webkit-appearance: none;
58
+ appearance: none;
59
+ height: 15px;
60
+ width: 15px;
61
+ background-color: white;
62
+ border-radius: 50%;
63
+ border: none;
64
+ transition: 0.2s ease-in-out;
65
+ }
66
+
67
+ input[type="range"]::-moz-range-thumb {
68
+ height: 15px;
69
+ width: 15px;
70
+ background-color: white;
71
+ border-radius: 50%;
72
+ border: none;
73
+ transition: 0.2s ease-in-out;
74
+ }
75
+ </style>