jupyterlab_advanced_image_viewer_extension 1.1.8 → 1.2.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/README.md +16 -2
- package/lib/audio.d.ts +32 -0
- package/lib/audio.js +341 -0
- package/lib/index.js +275 -110
- package/lib/media.d.ts +40 -0
- package/lib/media.js +149 -0
- package/lib/mediautil.d.ts +5 -0
- package/lib/mediautil.js +40 -0
- package/lib/navigate.d.ts +6 -0
- package/lib/navigate.js +67 -0
- package/lib/toolbar.d.ts +9 -0
- package/lib/toolbar.js +51 -0
- package/lib/video.d.ts +13 -0
- package/lib/video.js +110 -0
- package/package.json +12 -2
- package/src/__tests__/mediautil.spec.ts +91 -0
- package/src/audio.ts +428 -0
- package/src/index.ts +349 -130
- package/src/media.ts +219 -0
- package/src/mediautil.ts +45 -0
- package/src/navigate.ts +100 -0
- package/src/toolbar.ts +76 -0
- package/src/typings.d.ts +13 -0
- package/src/video.ts +126 -0
- package/style/base.css +92 -0
- package/style/index.js +2 -0
- package/style/plyr.svg +1 -0
package/README.md
CHANGED
|
@@ -8,9 +8,9 @@
|
|
|
8
8
|
[](https://kolomolo.com)
|
|
9
9
|
[](https://www.paypal.com/donate/?hosted_button_id=B4KPBJDLLXTSA)
|
|
10
10
|
|
|
11
|
-
A JupyterLab 4 extension that makes the built-in image viewer interactive. Images open fitted to the panel just like the stock viewer, then you can zoom with the wheel, pan by dragging, and step through a folder of images with the arrow keys - all in a single viewer tab.
|
|
11
|
+
A JupyterLab 4 extension that makes the built-in image viewer interactive and adds audio and video viewers. Images open fitted to the panel just like the stock viewer, then you can zoom with the wheel, pan by dragging, and step through a folder of images with the arrow keys - all in a single viewer tab.
|
|
12
12
|
|
|
13
|
-

|
|
14
14
|
|
|
15
15
|
The viewer toolbar adds zoom in / out / fit controls and a `help` link that lists every keybinding.
|
|
16
16
|
|
|
@@ -25,6 +25,20 @@ The viewer toolbar adds zoom in / out / fit controls and a `help` link that list
|
|
|
25
25
|
- **Refresh** - a toolbar refresh button reloads the image from disk, the same control the HTML viewer provides
|
|
26
26
|
- **WebP and AVIF support** - `.webp` and `.avif` files open in the image viewer with every feature above; stock JupyterLab does not open them and shows a File Load Error
|
|
27
27
|
|
|
28
|
+
## Audio and video viewers
|
|
29
|
+
|
|
30
|
+
Audio files open in a waveform player built on wavesurfer.js and video files in a Plyr player. The extension disables the stock JupyterLab audio and video viewers, so a double click on a media file opens these instead.
|
|
31
|
+
|
|
32
|
+

|
|
33
|
+

|
|
34
|
+
|
|
35
|
+
- **Audio controls** - play/pause, back and forward 5 s, Loop, a speed choice of 0.5x to 2x, waveform zoom with - / + / Fit, an overview strip that seeks anywhere in the file, the time under the mouse, and a drag-marked loop region of at least 1 s
|
|
36
|
+
- **Video controls** - play/pause, a seek bar, the current time, mute and volume, a speed menu, picture in picture and full screen, in a player fitted to the panel with the aspect ratio kept
|
|
37
|
+
- **Keys in a focused media tab** - Space plays and pauses, Left and Right seek 5 s, and Page Up / Page Down open the previous and next file of the same kind in the folder, in the order the file browser lists them (the folder navigation setting turns this off)
|
|
38
|
+
- **Help dialog** - a `help` link in each media toolbar lists the keys and the waveform gestures
|
|
39
|
+
- **File details** - the toolbar shows the format, the size and the duration, plus the channel count for audio and the frame size for video
|
|
40
|
+
- **100 MB limit** - a file up to 100 MB is downloaded once from its file URL and played from that copy, so seeking works on a server without byte ranges; above 100 MB the audio waveform is skipped and either file plays from its URL, so seeking there needs a server that serves byte ranges
|
|
41
|
+
|
|
28
42
|
## Requirements
|
|
29
43
|
|
|
30
44
|
- JupyterLab >= 4.6
|
package/lib/audio.d.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { Toolbar } from '@jupyterlab/apputils';
|
|
2
|
+
import type WaveSurfer from 'wavesurfer.js';
|
|
3
|
+
import { MediaViewer } from './media';
|
|
4
|
+
export declare class AudioViewer extends MediaViewer {
|
|
5
|
+
constructor(options: Omit<MediaViewer.IOptions, 'kind'>);
|
|
6
|
+
wavesurfer: WaveSurfer | null;
|
|
7
|
+
load(): Promise<void>;
|
|
8
|
+
addControls(toolbar: Toolbar): void;
|
|
9
|
+
zoomBy(factor: number): void;
|
|
10
|
+
setLoop(on: boolean): void;
|
|
11
|
+
dispose(): void;
|
|
12
|
+
private setZoomEnabled;
|
|
13
|
+
private createPlayer;
|
|
14
|
+
private colours;
|
|
15
|
+
private paint;
|
|
16
|
+
private labelRegion;
|
|
17
|
+
private showTime;
|
|
18
|
+
private showView;
|
|
19
|
+
private showDetails;
|
|
20
|
+
private wave;
|
|
21
|
+
private time;
|
|
22
|
+
private view;
|
|
23
|
+
private themeObserver;
|
|
24
|
+
private minimap;
|
|
25
|
+
private createMinimap;
|
|
26
|
+
private region;
|
|
27
|
+
private looping;
|
|
28
|
+
private loopButton;
|
|
29
|
+
private zoomButtons;
|
|
30
|
+
private zoomEnabled;
|
|
31
|
+
private last;
|
|
32
|
+
}
|
package/lib/audio.js
ADDED
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
// Audio viewer: the shared media widget with a wavesurfer.js waveform, an
|
|
2
|
+
// overview strip, a time ruler, the time under the mouse, a loop region and
|
|
3
|
+
// zoom. wavesurfer.js loads when the first audio file opens, not at start-up.
|
|
4
|
+
import { ToolbarButton } from '@jupyterlab/apputils';
|
|
5
|
+
import { jumpBackIcon, jumpForwardIcon, pauseIcon, runIcon } from '@jupyterlab/ui-components';
|
|
6
|
+
import { Widget } from '@lumino/widgets';
|
|
7
|
+
import { MediaViewer } from './media';
|
|
8
|
+
import { drawsWaveform, formatTime, SEEK_STEP, WAVEFORM_LIMIT } from './mediautil';
|
|
9
|
+
import { addZoomButtons } from './toolbar';
|
|
10
|
+
const SPEEDS = [0.5, 0.75, 1, 1.25, 1.5, 2];
|
|
11
|
+
// The shortest drag, in seconds, that makes a loop region: a click that
|
|
12
|
+
// moves a few pixels must not arm Loop on a sub-second region.
|
|
13
|
+
const MIN_LOOP = 1;
|
|
14
|
+
// wavesurfer.js and the plugins the viewer uses.
|
|
15
|
+
const loadPlayer = () => Promise.all([
|
|
16
|
+
import('wavesurfer.js'),
|
|
17
|
+
import('wavesurfer.js/dist/plugins/minimap.js'),
|
|
18
|
+
import('wavesurfer.js/dist/plugins/timeline.js'),
|
|
19
|
+
import('wavesurfer.js/dist/plugins/hover.js'),
|
|
20
|
+
import('wavesurfer.js/dist/plugins/regions.js'),
|
|
21
|
+
import('wavesurfer.js/dist/plugins/zoom.js')
|
|
22
|
+
]);
|
|
23
|
+
export class AudioViewer extends MediaViewer {
|
|
24
|
+
constructor(options) {
|
|
25
|
+
super({ ...options, kind: 'audio' });
|
|
26
|
+
// The player of the waveform; null before the first one and above the
|
|
27
|
+
// limit.
|
|
28
|
+
this.wavesurfer = null;
|
|
29
|
+
this.minimap = null;
|
|
30
|
+
this.createMinimap = null;
|
|
31
|
+
this.region = null;
|
|
32
|
+
this.looping = false;
|
|
33
|
+
this.loopButton = null;
|
|
34
|
+
this.zoomButtons = null;
|
|
35
|
+
// Kept here too, so a load that finishes before addControls() disables the
|
|
36
|
+
// buttons when they are made.
|
|
37
|
+
this.zoomEnabled = true;
|
|
38
|
+
// Playback time at the previous update, to tell a run over the region end
|
|
39
|
+
// from a seek.
|
|
40
|
+
this.last = 0;
|
|
41
|
+
this.addClass('jp-AdvancedMediaViewer-audio');
|
|
42
|
+
this.wave = document.createElement('div');
|
|
43
|
+
this.wave.className = 'jp-AdvancedMediaViewer-wave';
|
|
44
|
+
const status = document.createElement('div');
|
|
45
|
+
status.className = 'jp-AdvancedMediaViewer-status';
|
|
46
|
+
this.time = document.createElement('span');
|
|
47
|
+
this.view = document.createElement('span');
|
|
48
|
+
status.append(this.time, this.view);
|
|
49
|
+
this.node.prepend(this.wave, status);
|
|
50
|
+
for (const type of ['timeupdate', 'durationchange', 'seeking', 'emptied']) {
|
|
51
|
+
this.media.addEventListener(type, () => this.showTime());
|
|
52
|
+
}
|
|
53
|
+
this.media.addEventListener('durationchange', () => this.showDetails());
|
|
54
|
+
// The waveform colours are painted on a canvas, which cannot read CSS
|
|
55
|
+
// variables, so they are resolved here and again on every theme change.
|
|
56
|
+
this.themeObserver = new MutationObserver(() => this.paint());
|
|
57
|
+
this.themeObserver.observe(document.body, {
|
|
58
|
+
attributes: true,
|
|
59
|
+
attributeFilter: ['data-jp-theme-name']
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
async load() {
|
|
63
|
+
var _a, _b;
|
|
64
|
+
const token = ++this.loads;
|
|
65
|
+
this.setLoop(false);
|
|
66
|
+
this.showMessage(this.trans.__('Loading...'));
|
|
67
|
+
try {
|
|
68
|
+
const size = await this.readSize();
|
|
69
|
+
const url = await this.contents.getDownloadUrl(this.context.path);
|
|
70
|
+
if (this.isDisposed || token !== this.loads) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
this.showDetails();
|
|
74
|
+
if (!drawsWaveform(size)) {
|
|
75
|
+
// Played from the file URL: seeking needs a server that answers byte
|
|
76
|
+
// ranges, as the stock file handler does.
|
|
77
|
+
(_a = this.wavesurfer) === null || _a === void 0 ? void 0 : _a.destroy();
|
|
78
|
+
this.wavesurfer = null;
|
|
79
|
+
// There is no waveform to zoom, so the buttons would do nothing.
|
|
80
|
+
this.setZoomEnabled(false);
|
|
81
|
+
this.media.controls = true;
|
|
82
|
+
this.media.src = url;
|
|
83
|
+
this.showMessage(this.trans.__('The waveform is skipped for files over %1 MB.', WAVEFORM_LIMIT / 1024 / 1024));
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
this.media.controls = false;
|
|
87
|
+
this.setZoomEnabled(true);
|
|
88
|
+
// Fetched once and handed over as a blob: the player then seeks in
|
|
89
|
+
// memory, also on a server that ignores byte ranges.
|
|
90
|
+
const response = await fetch(url);
|
|
91
|
+
if (!response.ok) {
|
|
92
|
+
throw new Error(`${response.status} ${response.statusText}`);
|
|
93
|
+
}
|
|
94
|
+
const blob = await response.blob();
|
|
95
|
+
const modules = await loadPlayer();
|
|
96
|
+
// A reload or a close while this one was fetching supersedes it.
|
|
97
|
+
if (this.isDisposed || token !== this.loads) {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
// One player per tab: a new player would load the element's previous
|
|
101
|
+
// source on its own, so a reload hands the existing one the new blob.
|
|
102
|
+
(_b = this.wavesurfer) !== null && _b !== void 0 ? _b : (this.wavesurfer = this.createPlayer(modules));
|
|
103
|
+
await this.wavesurfer.loadBlob(blob);
|
|
104
|
+
this.showMessage('');
|
|
105
|
+
}
|
|
106
|
+
catch (reason) {
|
|
107
|
+
// AbortError: a newer load of the same player took over.
|
|
108
|
+
if (!this.isDisposed &&
|
|
109
|
+
token === this.loads &&
|
|
110
|
+
reason.name !== 'AbortError') {
|
|
111
|
+
this.showMessage(this.trans.__('This file cannot be played: %1', String(reason)));
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
// The playback controls, zoom, loop and speed, between Reload and the
|
|
116
|
+
// details on the toolbar.
|
|
117
|
+
addControls(toolbar) {
|
|
118
|
+
const trans = this.trans;
|
|
119
|
+
const play = new ToolbarButton({
|
|
120
|
+
icon: runIcon,
|
|
121
|
+
pressedIcon: pauseIcon,
|
|
122
|
+
tooltip: trans.__('Play'),
|
|
123
|
+
pressedTooltip: trans.__('Pause'),
|
|
124
|
+
noFocusOnClick: true,
|
|
125
|
+
onClick: () => this.togglePlay()
|
|
126
|
+
});
|
|
127
|
+
for (const type of ['play', 'pause', 'emptied']) {
|
|
128
|
+
this.media.addEventListener(type, () => (play.pressed = !this.media.paused));
|
|
129
|
+
}
|
|
130
|
+
toolbar.addItem('advanced-play', play);
|
|
131
|
+
toolbar.addItem('advanced-back', new ToolbarButton({
|
|
132
|
+
icon: jumpBackIcon,
|
|
133
|
+
tooltip: trans.__('Back %1 seconds', SEEK_STEP),
|
|
134
|
+
noFocusOnClick: true,
|
|
135
|
+
onClick: () => this.seekBy(-SEEK_STEP)
|
|
136
|
+
}));
|
|
137
|
+
toolbar.addItem('advanced-forward', new ToolbarButton({
|
|
138
|
+
icon: jumpForwardIcon,
|
|
139
|
+
tooltip: trans.__('Forward %1 seconds', SEEK_STEP),
|
|
140
|
+
noFocusOnClick: true,
|
|
141
|
+
onClick: () => this.seekBy(SEEK_STEP)
|
|
142
|
+
}));
|
|
143
|
+
this.zoomButtons = addZoomButtons(toolbar, trans, () => this.zoomBy(0.5), () => this.zoomBy(2), () => this.zoomBy(0));
|
|
144
|
+
this.setZoomEnabled(this.zoomEnabled);
|
|
145
|
+
this.loopButton = new ToolbarButton({
|
|
146
|
+
label: trans.__('Loop'),
|
|
147
|
+
tooltip: trans.__('Loop the selected region, or the whole file'),
|
|
148
|
+
noFocusOnClick: true,
|
|
149
|
+
onClick: () => this.setLoop(!this.looping)
|
|
150
|
+
});
|
|
151
|
+
toolbar.addItem('advanced-loop', this.loopButton);
|
|
152
|
+
const speed = document.createElement('select');
|
|
153
|
+
speed.className = 'jp-AdvancedMediaViewer-speed';
|
|
154
|
+
speed.title = trans.__('Playback speed');
|
|
155
|
+
for (const rate of SPEEDS) {
|
|
156
|
+
speed.add(new Option(`${rate}×`, String(rate), rate === 1, rate === 1));
|
|
157
|
+
}
|
|
158
|
+
speed.addEventListener('change', () => {
|
|
159
|
+
this.media.playbackRate = Number(speed.value);
|
|
160
|
+
});
|
|
161
|
+
toolbar.addItem('advanced-speed', new Widget({ node: speed }));
|
|
162
|
+
}
|
|
163
|
+
// Multiply the shown time scale by `factor`; 0 fits the whole file. The
|
|
164
|
+
// waveform never zooms out past the fit.
|
|
165
|
+
zoomBy(factor) {
|
|
166
|
+
const ws = this.wavesurfer;
|
|
167
|
+
const duration = ws === null || ws === void 0 ? void 0 : ws.getDuration();
|
|
168
|
+
if (!(ws === null || ws === void 0 ? void 0 : ws.getDecodedData()) || !duration) {
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
// The level comes from wavesurfer's own setting, not from the wrapper
|
|
172
|
+
// width: the wrapper keeps the previous zoom's width until its next
|
|
173
|
+
// render, so a click right after a fit or a wheel zoom would start from
|
|
174
|
+
// the old level. 0 fits the file.
|
|
175
|
+
ws.zoom(factor ? (ws.options.minPxPerSec || ws.getWidth() / duration) * factor : 0);
|
|
176
|
+
}
|
|
177
|
+
// Loop on repeats the region, or the whole file without one; loop off
|
|
178
|
+
// removes the region.
|
|
179
|
+
setLoop(on) {
|
|
180
|
+
this.looping = on;
|
|
181
|
+
if (!on && this.region) {
|
|
182
|
+
this.region.remove();
|
|
183
|
+
this.region = null;
|
|
184
|
+
}
|
|
185
|
+
this.media.loop = on && !this.region;
|
|
186
|
+
if (this.loopButton) {
|
|
187
|
+
this.loopButton.pressed = on;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
dispose() {
|
|
191
|
+
var _a;
|
|
192
|
+
if (this.isDisposed) {
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
this.themeObserver.disconnect();
|
|
196
|
+
// Revokes the blob URL it made for the media element.
|
|
197
|
+
(_a = this.wavesurfer) === null || _a === void 0 ? void 0 : _a.destroy();
|
|
198
|
+
this.wavesurfer = null;
|
|
199
|
+
super.dispose();
|
|
200
|
+
}
|
|
201
|
+
setZoomEnabled(on) {
|
|
202
|
+
this.zoomEnabled = on;
|
|
203
|
+
if (this.zoomButtons) {
|
|
204
|
+
this.zoomButtons.zoomOut.enabled = on;
|
|
205
|
+
this.zoomButtons.zoomIn.enabled = on;
|
|
206
|
+
this.zoomButtons.fit.enabled = on;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
createPlayer([{ default: Player }, { default: Minimap }, { default: Timeline }, { default: Hover }, { default: Regions }, { default: Zoom }]) {
|
|
210
|
+
this.createMinimap = () => Minimap.create({
|
|
211
|
+
...this.colours(),
|
|
212
|
+
cursorColor: 'var(--jp-ui-font-color0)',
|
|
213
|
+
height: 44,
|
|
214
|
+
dragToSeek: true,
|
|
215
|
+
insertPosition: 'beforebegin',
|
|
216
|
+
overlayColor: 'color-mix(in srgb, var(--jp-brand-color1) 15%, transparent)'
|
|
217
|
+
});
|
|
218
|
+
this.minimap = this.createMinimap();
|
|
219
|
+
const regions = Regions.create();
|
|
220
|
+
const ws = Player.create({
|
|
221
|
+
container: this.wave,
|
|
222
|
+
media: this.media,
|
|
223
|
+
height: 200,
|
|
224
|
+
...this.colours(),
|
|
225
|
+
cursorColor: 'var(--jp-ui-font-color0)',
|
|
226
|
+
cursorWidth: 2,
|
|
227
|
+
plugins: [
|
|
228
|
+
this.minimap,
|
|
229
|
+
Timeline.create({
|
|
230
|
+
height: 18,
|
|
231
|
+
insertPosition: 'beforebegin',
|
|
232
|
+
formatTimeCallback: (seconds) => formatTime(seconds, 0),
|
|
233
|
+
style: { color: 'var(--jp-ui-font-color2)' }
|
|
234
|
+
}),
|
|
235
|
+
Hover.create({
|
|
236
|
+
lineColor: 'var(--jp-ui-font-color2)',
|
|
237
|
+
labelColor: 'var(--jp-ui-font-color0)',
|
|
238
|
+
labelBackground: 'var(--jp-layout-color3)',
|
|
239
|
+
formatTimeCallback: (seconds) => formatTime(seconds)
|
|
240
|
+
}),
|
|
241
|
+
regions,
|
|
242
|
+
Zoom.create()
|
|
243
|
+
]
|
|
244
|
+
});
|
|
245
|
+
regions.enableDragSelection({
|
|
246
|
+
color: 'color-mix(in srgb, var(--jp-brand-color1) 20%, transparent)',
|
|
247
|
+
minLength: MIN_LOOP
|
|
248
|
+
});
|
|
249
|
+
// One loop region at a time: a new drag replaces the old one.
|
|
250
|
+
regions.on('region-created', region => {
|
|
251
|
+
// wavesurfer stretches a shorter drag to exactly minLength instead of
|
|
252
|
+
// dropping it; the small margin covers the float error of that sum.
|
|
253
|
+
if (region.end - region.start < MIN_LOOP + 0.01) {
|
|
254
|
+
region.remove();
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
regions
|
|
258
|
+
.getRegions()
|
|
259
|
+
.filter(other => other !== region)
|
|
260
|
+
.forEach(other => other.remove());
|
|
261
|
+
this.region = region;
|
|
262
|
+
this.labelRegion();
|
|
263
|
+
this.setLoop(true);
|
|
264
|
+
});
|
|
265
|
+
regions.on('region-updated', () => this.labelRegion());
|
|
266
|
+
// Return to the region start when playback runs over its end, not when
|
|
267
|
+
// the user seeks past it.
|
|
268
|
+
ws.on('timeupdate', time => {
|
|
269
|
+
const r = this.region;
|
|
270
|
+
if (this.looping &&
|
|
271
|
+
r &&
|
|
272
|
+
ws.isPlaying() &&
|
|
273
|
+
this.last >= r.start &&
|
|
274
|
+
this.last < r.end &&
|
|
275
|
+
time >= r.end) {
|
|
276
|
+
ws.setTime(r.start);
|
|
277
|
+
time = r.start;
|
|
278
|
+
}
|
|
279
|
+
this.last = time;
|
|
280
|
+
});
|
|
281
|
+
ws.on('seeking', time => (this.last = time));
|
|
282
|
+
for (const event of ['zoom', 'scroll', 'redrawcomplete']) {
|
|
283
|
+
ws.on(event, () => this.showView());
|
|
284
|
+
}
|
|
285
|
+
ws.on('decode', () => this.showDetails());
|
|
286
|
+
return ws;
|
|
287
|
+
}
|
|
288
|
+
colours() {
|
|
289
|
+
const style = getComputedStyle(document.body);
|
|
290
|
+
return {
|
|
291
|
+
// color3 is 2.68:1 on white; the waveform is graphical content and
|
|
292
|
+
// WCAG 1.4.11 asks 3:1, which color2 clears.
|
|
293
|
+
waveColor: style.getPropertyValue('--jp-ui-font-color2').trim(),
|
|
294
|
+
progressColor: style.getPropertyValue('--jp-brand-color1').trim()
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
paint() {
|
|
298
|
+
const ws = this.wavesurfer;
|
|
299
|
+
if (!ws || !this.minimap || !this.createMinimap) {
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
ws.setOptions(this.colours());
|
|
303
|
+
// The overview copies its colours when it is made, so it is made anew.
|
|
304
|
+
ws.unregisterPlugin(this.minimap);
|
|
305
|
+
this.minimap = ws.registerPlugin(this.createMinimap());
|
|
306
|
+
}
|
|
307
|
+
labelRegion() {
|
|
308
|
+
const r = this.region;
|
|
309
|
+
r === null || r === void 0 ? void 0 : r.setContent(this.trans.__('Loop %1 - %2', formatTime(r.start), formatTime(r.end)));
|
|
310
|
+
}
|
|
311
|
+
showTime() {
|
|
312
|
+
this.time.textContent = `${formatTime(this.media.currentTime)} / ${formatTime(this.media.duration)}`;
|
|
313
|
+
}
|
|
314
|
+
showView() {
|
|
315
|
+
const ws = this.wavesurfer;
|
|
316
|
+
const duration = ws === null || ws === void 0 ? void 0 : ws.getDuration();
|
|
317
|
+
if (!ws || !duration) {
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
// From the zoom level, not the rendered width: on the zoom event the
|
|
321
|
+
// wrapper still has its old width.
|
|
322
|
+
const width = ws.getWidth();
|
|
323
|
+
const pxPerSec = Math.max(ws.options.minPxPerSec, width / duration);
|
|
324
|
+
const shown = Math.min(width / pxPerSec, duration);
|
|
325
|
+
const start = Math.min(ws.getScroll() / pxPerSec, duration - shown);
|
|
326
|
+
this.view.textContent = this.trans.__('showing %1 - %2', formatTime(start), formatTime(start + shown));
|
|
327
|
+
}
|
|
328
|
+
showDetails() {
|
|
329
|
+
var _a, _b;
|
|
330
|
+
const channels = (_b = (_a = this.wavesurfer) === null || _a === void 0 ? void 0 : _a.getDecodedData()) === null || _b === void 0 ? void 0 : _b.numberOfChannels;
|
|
331
|
+
this.setDetails(channels === undefined
|
|
332
|
+
? []
|
|
333
|
+
: [
|
|
334
|
+
channels === 1
|
|
335
|
+
? this.trans.__('mono')
|
|
336
|
+
: channels === 2
|
|
337
|
+
? this.trans.__('stereo')
|
|
338
|
+
: this.trans.__('%1 channels', channels)
|
|
339
|
+
]);
|
|
340
|
+
}
|
|
341
|
+
}
|