retold-remote 0.0.1 → 0.0.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/html/index.html +2 -0
- package/package.json +20 -14
- package/source/Pict-Application-RetoldRemote.js +46 -5
- package/source/cli/RetoldRemote-CLI-Run.js +0 -0
- package/source/cli/RetoldRemote-Server-Setup.js +790 -8
- package/source/cli/commands/RetoldRemote-Command-Serve.js +34 -1
- package/source/providers/Pict-Provider-GalleryFilterSort.js +61 -9
- package/source/providers/Pict-Provider-GalleryNavigation.js +517 -18
- package/source/providers/Pict-Provider-RetoldRemote.js +11 -2
- package/source/providers/Pict-Provider-RetoldRemoteIcons.js +1 -0
- package/source/server/RetoldRemote-ArchiveService.js +830 -0
- package/source/server/RetoldRemote-AudioWaveformService.js +673 -0
- package/source/server/RetoldRemote-EbookService.js +242 -0
- package/source/server/RetoldRemote-MediaService.js +1 -1
- package/source/server/RetoldRemote-ToolDetector.js +31 -1
- package/source/server/RetoldRemote-VideoFrameService.js +486 -0
- package/source/views/PictView-Remote-AudioExplorer.js +1213 -0
- package/source/views/PictView-Remote-Gallery.js +141 -2
- package/source/views/PictView-Remote-Layout.js +18 -27
- package/source/views/PictView-Remote-MediaViewer.js +638 -39
- package/source/views/PictView-Remote-SettingsPanel.js +23 -0
- package/source/views/PictView-Remote-TopBar.js +121 -0
- package/source/views/PictView-Remote-VideoExplorer.js +1229 -0
- package/web-application/index.html +2 -0
- package/web-application/js/epub.min.js +1 -0
- package/web-application/retold-remote.js +7030 -1244
- package/web-application/retold-remote.js.map +1 -1
- package/web-application/retold-remote.min.js +13 -44
- package/web-application/retold-remote.min.js.map +1 -1
- package/web-application/retold-remote.compatible.js +0 -5764
- package/web-application/retold-remote.compatible.js.map +0 -1
- package/web-application/retold-remote.compatible.min.js +0 -120
- package/web-application/retold-remote.compatible.min.js.map +0 -1
|
@@ -147,6 +147,213 @@ const _ViewConfiguration =
|
|
|
147
147
|
.retold-remote-code-viewer-container .pict-code-editor .tag { color: #E06C75; }
|
|
148
148
|
.retold-remote-code-viewer-container .pict-code-editor .attr-name { color: #D19A66; }
|
|
149
149
|
.retold-remote-code-viewer-container .pict-code-editor .attr-value { color: #98C379; }
|
|
150
|
+
/* Video wrap with stats bar */
|
|
151
|
+
.retold-remote-video-wrap
|
|
152
|
+
{
|
|
153
|
+
display: flex;
|
|
154
|
+
flex-direction: column;
|
|
155
|
+
align-items: center;
|
|
156
|
+
max-width: 100%;
|
|
157
|
+
max-height: 100%;
|
|
158
|
+
width: 100%;
|
|
159
|
+
height: 100%;
|
|
160
|
+
}
|
|
161
|
+
.retold-remote-video-wrap video
|
|
162
|
+
{
|
|
163
|
+
flex: 1;
|
|
164
|
+
min-height: 0;
|
|
165
|
+
max-width: 100%;
|
|
166
|
+
max-height: calc(100% - 40px);
|
|
167
|
+
object-fit: contain;
|
|
168
|
+
}
|
|
169
|
+
.retold-remote-video-stats
|
|
170
|
+
{
|
|
171
|
+
display: flex;
|
|
172
|
+
align-items: center;
|
|
173
|
+
gap: 16px;
|
|
174
|
+
padding: 6px 16px;
|
|
175
|
+
background: var(--retold-bg-secondary);
|
|
176
|
+
border-top: 1px solid var(--retold-border);
|
|
177
|
+
width: 100%;
|
|
178
|
+
flex-shrink: 0;
|
|
179
|
+
font-size: 0.75rem;
|
|
180
|
+
color: var(--retold-text-dim);
|
|
181
|
+
white-space: nowrap;
|
|
182
|
+
overflow-x: auto;
|
|
183
|
+
}
|
|
184
|
+
.retold-remote-video-stats span
|
|
185
|
+
{
|
|
186
|
+
display: inline-flex;
|
|
187
|
+
align-items: center;
|
|
188
|
+
gap: 4px;
|
|
189
|
+
}
|
|
190
|
+
.retold-remote-video-stats .retold-remote-video-stat-label
|
|
191
|
+
{
|
|
192
|
+
color: var(--retold-text-muted);
|
|
193
|
+
}
|
|
194
|
+
.retold-remote-video-stats .retold-remote-video-stat-value
|
|
195
|
+
{
|
|
196
|
+
color: var(--retold-text-secondary);
|
|
197
|
+
}
|
|
198
|
+
.retold-remote-explore-btn
|
|
199
|
+
{
|
|
200
|
+
margin-left: auto;
|
|
201
|
+
padding: 3px 12px;
|
|
202
|
+
border: 1px solid var(--retold-accent);
|
|
203
|
+
border-radius: 3px;
|
|
204
|
+
background: transparent;
|
|
205
|
+
color: var(--retold-accent);
|
|
206
|
+
font-size: 0.75rem;
|
|
207
|
+
cursor: pointer;
|
|
208
|
+
transition: background 0.15s, color 0.15s;
|
|
209
|
+
font-family: inherit;
|
|
210
|
+
white-space: nowrap;
|
|
211
|
+
}
|
|
212
|
+
.retold-remote-explore-btn:hover
|
|
213
|
+
{
|
|
214
|
+
background: var(--retold-accent);
|
|
215
|
+
color: var(--retold-bg-primary);
|
|
216
|
+
}
|
|
217
|
+
.retold-remote-vlc-btn
|
|
218
|
+
{
|
|
219
|
+
padding: 3px 12px;
|
|
220
|
+
border: 1px solid var(--retold-accent);
|
|
221
|
+
border-radius: 3px;
|
|
222
|
+
background: transparent;
|
|
223
|
+
color: var(--retold-accent);
|
|
224
|
+
font-size: 0.75rem;
|
|
225
|
+
cursor: pointer;
|
|
226
|
+
transition: background 0.15s, color 0.15s;
|
|
227
|
+
font-family: inherit;
|
|
228
|
+
white-space: nowrap;
|
|
229
|
+
}
|
|
230
|
+
.retold-remote-vlc-btn:hover
|
|
231
|
+
{
|
|
232
|
+
background: var(--retold-accent);
|
|
233
|
+
color: var(--retold-bg-primary);
|
|
234
|
+
}
|
|
235
|
+
/* Ebook reader */
|
|
236
|
+
.retold-remote-ebook-wrap
|
|
237
|
+
{
|
|
238
|
+
display: flex;
|
|
239
|
+
width: 100%;
|
|
240
|
+
height: 100%;
|
|
241
|
+
position: relative;
|
|
242
|
+
}
|
|
243
|
+
.retold-remote-ebook-toc
|
|
244
|
+
{
|
|
245
|
+
width: 240px;
|
|
246
|
+
flex-shrink: 0;
|
|
247
|
+
background: var(--retold-bg-secondary);
|
|
248
|
+
border-right: 1px solid var(--retold-border);
|
|
249
|
+
overflow-y: auto;
|
|
250
|
+
font-size: 0.78rem;
|
|
251
|
+
padding: 8px 0;
|
|
252
|
+
}
|
|
253
|
+
.retold-remote-ebook-toc.collapsed
|
|
254
|
+
{
|
|
255
|
+
display: none;
|
|
256
|
+
}
|
|
257
|
+
.retold-remote-ebook-toc-item
|
|
258
|
+
{
|
|
259
|
+
display: block;
|
|
260
|
+
padding: 6px 16px;
|
|
261
|
+
color: var(--retold-text-secondary);
|
|
262
|
+
text-decoration: none;
|
|
263
|
+
cursor: pointer;
|
|
264
|
+
transition: background 0.1s, color 0.1s;
|
|
265
|
+
border: none;
|
|
266
|
+
background: none;
|
|
267
|
+
width: 100%;
|
|
268
|
+
text-align: left;
|
|
269
|
+
font-family: inherit;
|
|
270
|
+
font-size: inherit;
|
|
271
|
+
}
|
|
272
|
+
.retold-remote-ebook-toc-item:hover
|
|
273
|
+
{
|
|
274
|
+
background: var(--retold-bg-tertiary);
|
|
275
|
+
color: var(--retold-text-primary);
|
|
276
|
+
}
|
|
277
|
+
.retold-remote-ebook-toc-item.indent-1
|
|
278
|
+
{
|
|
279
|
+
padding-left: 32px;
|
|
280
|
+
}
|
|
281
|
+
.retold-remote-ebook-toc-item.indent-2
|
|
282
|
+
{
|
|
283
|
+
padding-left: 48px;
|
|
284
|
+
}
|
|
285
|
+
.retold-remote-ebook-reader
|
|
286
|
+
{
|
|
287
|
+
flex: 1;
|
|
288
|
+
display: flex;
|
|
289
|
+
flex-direction: column;
|
|
290
|
+
min-width: 0;
|
|
291
|
+
position: relative;
|
|
292
|
+
}
|
|
293
|
+
.retold-remote-ebook-content
|
|
294
|
+
{
|
|
295
|
+
flex: 1;
|
|
296
|
+
position: relative;
|
|
297
|
+
overflow: hidden;
|
|
298
|
+
}
|
|
299
|
+
.retold-remote-ebook-content iframe
|
|
300
|
+
{
|
|
301
|
+
border: none;
|
|
302
|
+
}
|
|
303
|
+
.retold-remote-ebook-controls
|
|
304
|
+
{
|
|
305
|
+
display: flex;
|
|
306
|
+
align-items: center;
|
|
307
|
+
justify-content: center;
|
|
308
|
+
gap: 16px;
|
|
309
|
+
padding: 8px 16px;
|
|
310
|
+
background: var(--retold-bg-secondary);
|
|
311
|
+
border-top: 1px solid var(--retold-border);
|
|
312
|
+
flex-shrink: 0;
|
|
313
|
+
}
|
|
314
|
+
.retold-remote-ebook-page-btn
|
|
315
|
+
{
|
|
316
|
+
padding: 6px 20px;
|
|
317
|
+
border: 1px solid var(--retold-border);
|
|
318
|
+
border-radius: 3px;
|
|
319
|
+
background: transparent;
|
|
320
|
+
color: var(--retold-text-muted);
|
|
321
|
+
font-size: 0.82rem;
|
|
322
|
+
cursor: pointer;
|
|
323
|
+
transition: color 0.15s, border-color 0.15s;
|
|
324
|
+
font-family: inherit;
|
|
325
|
+
}
|
|
326
|
+
.retold-remote-ebook-page-btn:hover
|
|
327
|
+
{
|
|
328
|
+
color: var(--retold-text-primary);
|
|
329
|
+
border-color: var(--retold-accent);
|
|
330
|
+
}
|
|
331
|
+
.retold-remote-ebook-toc-btn
|
|
332
|
+
{
|
|
333
|
+
padding: 6px 12px;
|
|
334
|
+
border: 1px solid var(--retold-border);
|
|
335
|
+
border-radius: 3px;
|
|
336
|
+
background: transparent;
|
|
337
|
+
color: var(--retold-text-muted);
|
|
338
|
+
font-size: 0.75rem;
|
|
339
|
+
cursor: pointer;
|
|
340
|
+
font-family: inherit;
|
|
341
|
+
}
|
|
342
|
+
.retold-remote-ebook-toc-btn:hover
|
|
343
|
+
{
|
|
344
|
+
color: var(--retold-text-primary);
|
|
345
|
+
border-color: var(--retold-accent);
|
|
346
|
+
}
|
|
347
|
+
.retold-remote-ebook-loading
|
|
348
|
+
{
|
|
349
|
+
display: flex;
|
|
350
|
+
flex-direction: column;
|
|
351
|
+
align-items: center;
|
|
352
|
+
justify-content: center;
|
|
353
|
+
height: 100%;
|
|
354
|
+
color: var(--retold-text-dim);
|
|
355
|
+
font-size: 0.85rem;
|
|
356
|
+
}
|
|
150
357
|
`
|
|
151
358
|
};
|
|
152
359
|
|
|
@@ -241,6 +448,16 @@ class RetoldRemoteMediaViewerView extends libPictView
|
|
|
241
448
|
this._loadCodeViewer(tmpContentURL, pFilePath);
|
|
242
449
|
}
|
|
243
450
|
|
|
451
|
+
// Load ebook viewer for epub/mobi
|
|
452
|
+
if (pMediaType === 'document')
|
|
453
|
+
{
|
|
454
|
+
let tmpExt = pFilePath.replace(/^.*\./, '').toLowerCase();
|
|
455
|
+
if (tmpExt === 'epub' || tmpExt === 'mobi')
|
|
456
|
+
{
|
|
457
|
+
this._loadEbookViewer(tmpContentURL, pFilePath);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
244
461
|
// Update topbar
|
|
245
462
|
let tmpTopBar = this.pict.views['ContentEditor-TopBar'];
|
|
246
463
|
if (tmpTopBar)
|
|
@@ -260,26 +477,74 @@ class RetoldRemoteMediaViewerView extends libPictView
|
|
|
260
477
|
|
|
261
478
|
_buildVideoHTML(pURL, pFileName)
|
|
262
479
|
{
|
|
263
|
-
|
|
264
|
-
|
|
480
|
+
let tmpHTML = '<div class="retold-remote-video-wrap">';
|
|
481
|
+
|
|
482
|
+
let tmpAutoplayVideo = this.pict.AppData.RetoldRemote.AutoplayVideo ? ' autoplay' : '';
|
|
483
|
+
tmpHTML += '<video controls' + tmpAutoplayVideo + ' preload="metadata" '
|
|
265
484
|
+ 'id="RetoldRemote-VideoPlayer">'
|
|
266
485
|
+ '<source src="' + pURL + '">'
|
|
267
486
|
+ 'Your browser does not support the video tag.'
|
|
268
487
|
+ '</video>';
|
|
488
|
+
|
|
489
|
+
// Stats bar below the video
|
|
490
|
+
tmpHTML += '<div class="retold-remote-video-stats" id="RetoldRemote-VideoStats">';
|
|
491
|
+
tmpHTML += '<span class="retold-remote-video-stat-label">Loading info...</span>';
|
|
492
|
+
|
|
493
|
+
// Explore Video button (only when ffmpeg is available)
|
|
494
|
+
let tmpCapabilities = this.pict.AppData.RetoldRemote.ServerCapabilities || {};
|
|
495
|
+
if (tmpCapabilities.ffmpeg)
|
|
496
|
+
{
|
|
497
|
+
tmpHTML += '<button class="retold-remote-explore-btn" '
|
|
498
|
+
+ 'onclick="pict.views[\'RetoldRemote-VideoExplorer\'].showExplorer(pict.AppData.RetoldRemote.CurrentViewerFile)" '
|
|
499
|
+
+ 'title="Explore frames from this video">'
|
|
500
|
+
+ '🔎 Explore Video'
|
|
501
|
+
+ '</button>';
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// VLC button (only shown when VLC capability is available)
|
|
505
|
+
if (tmpCapabilities.vlc)
|
|
506
|
+
{
|
|
507
|
+
tmpHTML += '<button class="retold-remote-vlc-btn" '
|
|
508
|
+
+ 'onclick="pict.providers[\'RetoldRemote-GalleryNavigation\']._openWithVLC()" '
|
|
509
|
+
+ 'title="Open with VLC (Enter)">'
|
|
510
|
+
+ '▶ Open ' + this._escapeHTML(pFileName) + ' with VLC'
|
|
511
|
+
+ '</button>';
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
tmpHTML += '</div>'; // end stats
|
|
515
|
+
tmpHTML += '</div>'; // end wrap
|
|
516
|
+
|
|
517
|
+
return tmpHTML;
|
|
269
518
|
}
|
|
270
519
|
|
|
271
520
|
_buildAudioHTML(pURL, pFileName)
|
|
272
521
|
{
|
|
273
522
|
let tmpIconProvider = this.pict.providers['RetoldRemote-Icons'];
|
|
274
523
|
let tmpIconHTML = tmpIconProvider ? '<span class="retold-remote-icon retold-remote-icon-lg">' + tmpIconProvider.getIcon('music-note', 64) + '</span>' : '🎵';
|
|
275
|
-
|
|
524
|
+
|
|
525
|
+
let tmpHTML = '<div style="text-align: center; padding: 40px;">'
|
|
276
526
|
+ '<div style="margin-bottom: 24px;">' + tmpIconHTML + '</div>'
|
|
277
527
|
+ '<div style="font-size: 1.1rem; color: var(--retold-text-secondary); margin-bottom: 24px;">' + this._escapeHTML(pFileName) + '</div>'
|
|
278
|
-
+ '<audio controls autoplay preload="metadata" id="RetoldRemote-AudioPlayer" style="width: 100%; max-width: 500px;">'
|
|
528
|
+
+ '<audio controls' + (this.pict.AppData.RetoldRemote.AutoplayAudio ? ' autoplay' : '') + ' preload="metadata" id="RetoldRemote-AudioPlayer" style="width: 100%; max-width: 500px;">'
|
|
279
529
|
+ '<source src="' + pURL + '">'
|
|
280
530
|
+ 'Your browser does not support the audio tag.'
|
|
281
|
-
+ '</audio>'
|
|
282
|
-
|
|
531
|
+
+ '</audio>';
|
|
532
|
+
|
|
533
|
+
// Explore Audio button (available when ffprobe is present)
|
|
534
|
+
let tmpCapabilities = this.pict.AppData.RetoldRemote.ServerCapabilities || {};
|
|
535
|
+
if (tmpCapabilities.ffprobe || tmpCapabilities.ffmpeg)
|
|
536
|
+
{
|
|
537
|
+
tmpHTML += '<div style="margin-top: 20px;">'
|
|
538
|
+
+ '<button class="retold-remote-explore-btn" '
|
|
539
|
+
+ 'onclick="pict.views[\'RetoldRemote-AudioExplorer\'].showExplorer(pict.AppData.RetoldRemote.CurrentViewerFile)" '
|
|
540
|
+
+ 'title="Explore waveform and extract segments from this audio">'
|
|
541
|
+
+ '📊 Explore Audio'
|
|
542
|
+
+ '</button>'
|
|
543
|
+
+ '</div>';
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
tmpHTML += '</div>';
|
|
547
|
+
return tmpHTML;
|
|
283
548
|
}
|
|
284
549
|
|
|
285
550
|
_buildDocumentHTML(pURL, pFileName, pFilePath)
|
|
@@ -293,6 +558,11 @@ class RetoldRemoteMediaViewerView extends libPictView
|
|
|
293
558
|
+ '</iframe>';
|
|
294
559
|
}
|
|
295
560
|
|
|
561
|
+
if (tmpExtension === 'epub' || tmpExtension === 'mobi')
|
|
562
|
+
{
|
|
563
|
+
return this._buildEbookHTML(pURL, pFileName, pFilePath);
|
|
564
|
+
}
|
|
565
|
+
|
|
296
566
|
// For other document types, show a download link
|
|
297
567
|
let tmpIconProvider = this.pict.providers['RetoldRemote-Icons'];
|
|
298
568
|
let tmpDocIconHTML = tmpIconProvider ? '<span class="retold-remote-icon retold-remote-icon-lg">' + tmpIconProvider.getIcon('document-large', 64) + '</span>' : '📄';
|
|
@@ -440,6 +710,278 @@ class RetoldRemoteMediaViewerView extends libPictView
|
|
|
440
710
|
});
|
|
441
711
|
}
|
|
442
712
|
|
|
713
|
+
/**
|
|
714
|
+
* Build the HTML shell for the ebook reader.
|
|
715
|
+
*/
|
|
716
|
+
_buildEbookHTML(pURL, pFileName, pFilePath)
|
|
717
|
+
{
|
|
718
|
+
return '<div class="retold-remote-ebook-wrap">'
|
|
719
|
+
+ '<div class="retold-remote-ebook-toc collapsed" id="RetoldRemote-EbookTOC"></div>'
|
|
720
|
+
+ '<div class="retold-remote-ebook-reader">'
|
|
721
|
+
+ '<div class="retold-remote-ebook-content" id="RetoldRemote-EbookContent">'
|
|
722
|
+
+ '<div class="retold-remote-ebook-loading">Loading ebook...</div>'
|
|
723
|
+
+ '</div>'
|
|
724
|
+
+ '<div class="retold-remote-ebook-controls">'
|
|
725
|
+
+ '<button class="retold-remote-ebook-toc-btn" onclick="pict.views[\'RetoldRemote-MediaViewer\'].toggleEbookTOC()">☰ TOC</button>'
|
|
726
|
+
+ '<button class="retold-remote-ebook-page-btn" onclick="pict.views[\'RetoldRemote-MediaViewer\'].ebookPrevPage()">← Prev</button>'
|
|
727
|
+
+ '<button class="retold-remote-ebook-page-btn" onclick="pict.views[\'RetoldRemote-MediaViewer\'].ebookNextPage()">Next →</button>'
|
|
728
|
+
+ '</div>'
|
|
729
|
+
+ '</div>'
|
|
730
|
+
+ '</div>';
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
/**
|
|
734
|
+
* Load and render an ebook using epub.js.
|
|
735
|
+
* For EPUB files, fetch directly. For MOBI files, convert server-side first.
|
|
736
|
+
*
|
|
737
|
+
* @param {string} pContentURL - Content URL for the file
|
|
738
|
+
* @param {string} pFilePath - Relative file path
|
|
739
|
+
*/
|
|
740
|
+
_loadEbookViewer(pContentURL, pFilePath)
|
|
741
|
+
{
|
|
742
|
+
let tmpSelf = this;
|
|
743
|
+
let tmpExtension = pFilePath.replace(/^.*\./, '').toLowerCase();
|
|
744
|
+
|
|
745
|
+
if (tmpExtension === 'mobi')
|
|
746
|
+
{
|
|
747
|
+
// Convert MOBI to EPUB server-side first
|
|
748
|
+
let tmpCapabilities = this.pict.AppData.RetoldRemote.ServerCapabilities || {};
|
|
749
|
+
if (!tmpCapabilities.ebook_convert)
|
|
750
|
+
{
|
|
751
|
+
let tmpContent = document.getElementById('RetoldRemote-EbookContent');
|
|
752
|
+
if (tmpContent)
|
|
753
|
+
{
|
|
754
|
+
tmpContent.innerHTML = '<div class="retold-remote-ebook-loading">'
|
|
755
|
+
+ 'MOBI viewing requires Calibre (ebook-convert) on the server.<br>'
|
|
756
|
+
+ '<a href="' + pContentURL + '" target="_blank" style="color: var(--retold-accent); margin-top: 12px; display: inline-block;">Download file</a>'
|
|
757
|
+
+ '</div>';
|
|
758
|
+
}
|
|
759
|
+
return;
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
let tmpContent = document.getElementById('RetoldRemote-EbookContent');
|
|
763
|
+
if (tmpContent)
|
|
764
|
+
{
|
|
765
|
+
tmpContent.innerHTML = '<div class="retold-remote-ebook-loading">Converting MOBI to EPUB...</div>';
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
let tmpProvider = this.pict.providers['RetoldRemote-Provider'];
|
|
769
|
+
let tmpPathParam = tmpProvider ? tmpProvider._getPathParam(pFilePath) : encodeURIComponent(pFilePath);
|
|
770
|
+
|
|
771
|
+
fetch('/api/media/ebook-convert?path=' + tmpPathParam)
|
|
772
|
+
.then((pResponse) => pResponse.json())
|
|
773
|
+
.then((pData) =>
|
|
774
|
+
{
|
|
775
|
+
if (!pData || !pData.Success)
|
|
776
|
+
{
|
|
777
|
+
throw new Error(pData ? pData.Error : 'Conversion failed.');
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
// Fetch the converted EPUB and render
|
|
781
|
+
let tmpEpubURL = '/api/media/ebook/' + pData.CacheKey + '/' + pData.OutputFilename;
|
|
782
|
+
tmpSelf._renderEpub(tmpEpubURL);
|
|
783
|
+
})
|
|
784
|
+
.catch((pError) =>
|
|
785
|
+
{
|
|
786
|
+
let tmpEl = document.getElementById('RetoldRemote-EbookContent');
|
|
787
|
+
if (tmpEl)
|
|
788
|
+
{
|
|
789
|
+
tmpEl.innerHTML = '<div class="retold-remote-ebook-loading">Failed to convert: '
|
|
790
|
+
+ tmpSelf._escapeHTML(pError.message)
|
|
791
|
+
+ '<br><a href="' + pContentURL + '" target="_blank" style="color: var(--retold-accent); margin-top: 12px; display: inline-block;">Download file</a>'
|
|
792
|
+
+ '</div>';
|
|
793
|
+
}
|
|
794
|
+
});
|
|
795
|
+
}
|
|
796
|
+
else
|
|
797
|
+
{
|
|
798
|
+
// EPUB — render directly
|
|
799
|
+
this._renderEpub(pContentURL);
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
/**
|
|
804
|
+
* Initialize epub.js and render an EPUB into the viewer container.
|
|
805
|
+
*
|
|
806
|
+
* @param {string} pEpubURL - URL to fetch the EPUB from
|
|
807
|
+
*/
|
|
808
|
+
_renderEpub(pEpubURL)
|
|
809
|
+
{
|
|
810
|
+
let tmpSelf = this;
|
|
811
|
+
|
|
812
|
+
// Check that epub.js is available
|
|
813
|
+
if (typeof (window) === 'undefined' || typeof (window.ePub) !== 'function')
|
|
814
|
+
{
|
|
815
|
+
let tmpEl = document.getElementById('RetoldRemote-EbookContent');
|
|
816
|
+
if (tmpEl)
|
|
817
|
+
{
|
|
818
|
+
tmpEl.innerHTML = '<div class="retold-remote-ebook-loading">epub.js library not loaded.</div>';
|
|
819
|
+
}
|
|
820
|
+
return;
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
// Destroy any previous book instance
|
|
824
|
+
if (this._activeBook)
|
|
825
|
+
{
|
|
826
|
+
try { this._activeBook.destroy(); } catch (e) { /* ignore */ }
|
|
827
|
+
this._activeBook = null;
|
|
828
|
+
this._activeRendition = null;
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
let tmpContentEl = document.getElementById('RetoldRemote-EbookContent');
|
|
832
|
+
if (!tmpContentEl)
|
|
833
|
+
{
|
|
834
|
+
return;
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
// Clear loading message
|
|
838
|
+
tmpContentEl.innerHTML = '';
|
|
839
|
+
|
|
840
|
+
// Fetch the EPUB as an ArrayBuffer and open with epub.js
|
|
841
|
+
fetch(pEpubURL)
|
|
842
|
+
.then((pResponse) =>
|
|
843
|
+
{
|
|
844
|
+
if (!pResponse.ok)
|
|
845
|
+
{
|
|
846
|
+
throw new Error('HTTP ' + pResponse.status);
|
|
847
|
+
}
|
|
848
|
+
return pResponse.arrayBuffer();
|
|
849
|
+
})
|
|
850
|
+
.then((pBuffer) =>
|
|
851
|
+
{
|
|
852
|
+
let tmpBook = window.ePub(pBuffer);
|
|
853
|
+
tmpSelf._activeBook = tmpBook;
|
|
854
|
+
|
|
855
|
+
let tmpRendition = tmpBook.renderTo(tmpContentEl,
|
|
856
|
+
{
|
|
857
|
+
width: '100%',
|
|
858
|
+
height: '100%',
|
|
859
|
+
spread: 'none'
|
|
860
|
+
});
|
|
861
|
+
|
|
862
|
+
tmpSelf._activeRendition = tmpRendition;
|
|
863
|
+
|
|
864
|
+
tmpRendition.display();
|
|
865
|
+
|
|
866
|
+
// Apply theme for dark backgrounds
|
|
867
|
+
tmpRendition.themes.default(
|
|
868
|
+
{
|
|
869
|
+
'body':
|
|
870
|
+
{
|
|
871
|
+
'color': 'var(--retold-text-primary, #d4d4d4)',
|
|
872
|
+
'background': 'var(--retold-bg-primary, #1e1e1e)',
|
|
873
|
+
'font-family': 'Georgia, "Times New Roman", serif',
|
|
874
|
+
'line-height': '1.6',
|
|
875
|
+
'padding': '20px 40px'
|
|
876
|
+
},
|
|
877
|
+
'a':
|
|
878
|
+
{
|
|
879
|
+
'color': 'var(--retold-accent, #569cd6)'
|
|
880
|
+
}
|
|
881
|
+
});
|
|
882
|
+
|
|
883
|
+
// Load table of contents
|
|
884
|
+
tmpBook.loaded.navigation.then((pNav) =>
|
|
885
|
+
{
|
|
886
|
+
tmpSelf._renderEbookTOC(pNav.toc);
|
|
887
|
+
});
|
|
888
|
+
})
|
|
889
|
+
.catch((pError) =>
|
|
890
|
+
{
|
|
891
|
+
if (tmpContentEl)
|
|
892
|
+
{
|
|
893
|
+
tmpContentEl.innerHTML = '<div class="retold-remote-ebook-loading">Failed to load ebook: '
|
|
894
|
+
+ tmpSelf._escapeHTML(pError.message) + '</div>';
|
|
895
|
+
}
|
|
896
|
+
});
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
/**
|
|
900
|
+
* Render the table of contents for the ebook.
|
|
901
|
+
*
|
|
902
|
+
* @param {Array} pToc - epub.js navigation TOC array
|
|
903
|
+
*/
|
|
904
|
+
_renderEbookTOC(pToc)
|
|
905
|
+
{
|
|
906
|
+
let tmpTocEl = document.getElementById('RetoldRemote-EbookTOC');
|
|
907
|
+
if (!tmpTocEl || !pToc)
|
|
908
|
+
{
|
|
909
|
+
return;
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
let tmpSelf = this;
|
|
913
|
+
let tmpHTML = '';
|
|
914
|
+
|
|
915
|
+
let tmpBuildItems = function (pItems, pDepth)
|
|
916
|
+
{
|
|
917
|
+
for (let i = 0; i < pItems.length; i++)
|
|
918
|
+
{
|
|
919
|
+
let tmpItem = pItems[i];
|
|
920
|
+
let tmpIndentClass = pDepth > 0 ? ' indent-' + Math.min(pDepth, 2) : '';
|
|
921
|
+
tmpHTML += '<button class="retold-remote-ebook-toc-item' + tmpIndentClass + '" '
|
|
922
|
+
+ 'data-href="' + tmpSelf._escapeHTML(tmpItem.href) + '" '
|
|
923
|
+
+ 'onclick="pict.views[\'RetoldRemote-MediaViewer\'].ebookGoToChapter(this.getAttribute(\'data-href\'))">'
|
|
924
|
+
+ tmpSelf._escapeHTML(tmpItem.label.trim())
|
|
925
|
+
+ '</button>';
|
|
926
|
+
|
|
927
|
+
if (tmpItem.subitems && tmpItem.subitems.length > 0)
|
|
928
|
+
{
|
|
929
|
+
tmpBuildItems(tmpItem.subitems, pDepth + 1);
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
};
|
|
933
|
+
|
|
934
|
+
tmpBuildItems(pToc, 0);
|
|
935
|
+
tmpTocEl.innerHTML = tmpHTML;
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
/**
|
|
939
|
+
* Navigate to a chapter in the ebook by href.
|
|
940
|
+
*
|
|
941
|
+
* @param {string} pHref - Chapter href from the TOC
|
|
942
|
+
*/
|
|
943
|
+
ebookGoToChapter(pHref)
|
|
944
|
+
{
|
|
945
|
+
if (this._activeRendition && pHref)
|
|
946
|
+
{
|
|
947
|
+
this._activeRendition.display(pHref);
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
/**
|
|
952
|
+
* Go to the previous page in the ebook.
|
|
953
|
+
*/
|
|
954
|
+
ebookPrevPage()
|
|
955
|
+
{
|
|
956
|
+
if (this._activeRendition)
|
|
957
|
+
{
|
|
958
|
+
this._activeRendition.prev();
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
/**
|
|
963
|
+
* Go to the next page in the ebook.
|
|
964
|
+
*/
|
|
965
|
+
ebookNextPage()
|
|
966
|
+
{
|
|
967
|
+
if (this._activeRendition)
|
|
968
|
+
{
|
|
969
|
+
this._activeRendition.next();
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
/**
|
|
974
|
+
* Toggle the table of contents sidebar.
|
|
975
|
+
*/
|
|
976
|
+
toggleEbookTOC()
|
|
977
|
+
{
|
|
978
|
+
let tmpTocEl = document.getElementById('RetoldRemote-EbookTOC');
|
|
979
|
+
if (tmpTocEl)
|
|
980
|
+
{
|
|
981
|
+
tmpTocEl.classList.toggle('collapsed');
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
|
|
443
985
|
_buildFallbackHTML(pURL, pFileName)
|
|
444
986
|
{
|
|
445
987
|
let tmpIconProvider = this.pict.providers['RetoldRemote-Icons'];
|
|
@@ -452,10 +994,11 @@ class RetoldRemoteMediaViewerView extends libPictView
|
|
|
452
994
|
}
|
|
453
995
|
|
|
454
996
|
/**
|
|
455
|
-
* Fetch file info and populate the overlay.
|
|
997
|
+
* Fetch file info and populate the overlay and video stats bar.
|
|
456
998
|
*/
|
|
457
999
|
_loadFileInfo(pFilePath)
|
|
458
1000
|
{
|
|
1001
|
+
let tmpSelf = this;
|
|
459
1002
|
let tmpProvider = this.pict.providers['RetoldRemote-Provider'];
|
|
460
1003
|
if (!tmpProvider)
|
|
461
1004
|
{
|
|
@@ -465,46 +1008,102 @@ class RetoldRemoteMediaViewerView extends libPictView
|
|
|
465
1008
|
tmpProvider.fetchMediaProbe(pFilePath,
|
|
466
1009
|
(pError, pData) =>
|
|
467
1010
|
{
|
|
468
|
-
|
|
469
|
-
if (!tmpOverlay || !pData)
|
|
1011
|
+
if (!pData)
|
|
470
1012
|
{
|
|
471
1013
|
return;
|
|
472
1014
|
}
|
|
473
1015
|
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
if (
|
|
477
|
-
{
|
|
478
|
-
tmpHTML += '<div class="retold-remote-fileinfo-row"><span class="retold-remote-fileinfo-label">Size</span><span class="retold-remote-fileinfo-value">' + this._formatFileSize(pData.Size) + '</span></div>';
|
|
479
|
-
}
|
|
480
|
-
if (pData.Width && pData.Height)
|
|
481
|
-
{
|
|
482
|
-
tmpHTML += '<div class="retold-remote-fileinfo-row"><span class="retold-remote-fileinfo-label">Dimensions</span><span class="retold-remote-fileinfo-value">' + pData.Width + ' x ' + pData.Height + '</span></div>';
|
|
483
|
-
}
|
|
484
|
-
if (pData.Duration)
|
|
485
|
-
{
|
|
486
|
-
let tmpMin = Math.floor(pData.Duration / 60);
|
|
487
|
-
let tmpSec = Math.floor(pData.Duration % 60);
|
|
488
|
-
tmpHTML += '<div class="retold-remote-fileinfo-row"><span class="retold-remote-fileinfo-label">Duration</span><span class="retold-remote-fileinfo-value">' + tmpMin + ':' + (tmpSec < 10 ? '0' : '') + tmpSec + '</span></div>';
|
|
489
|
-
}
|
|
490
|
-
if (pData.Codec)
|
|
491
|
-
{
|
|
492
|
-
tmpHTML += '<div class="retold-remote-fileinfo-row"><span class="retold-remote-fileinfo-label">Codec</span><span class="retold-remote-fileinfo-value">' + pData.Codec + '</span></div>';
|
|
493
|
-
}
|
|
494
|
-
if (pData.Format)
|
|
495
|
-
{
|
|
496
|
-
tmpHTML += '<div class="retold-remote-fileinfo-row"><span class="retold-remote-fileinfo-label">Format</span><span class="retold-remote-fileinfo-value">' + pData.Format + '</span></div>';
|
|
497
|
-
}
|
|
498
|
-
if (pData.Modified)
|
|
1016
|
+
// Populate the info overlay
|
|
1017
|
+
let tmpOverlay = document.getElementById('RetoldRemote-FileInfo-Overlay');
|
|
1018
|
+
if (tmpOverlay)
|
|
499
1019
|
{
|
|
500
|
-
tmpHTML
|
|
1020
|
+
let tmpHTML = '';
|
|
1021
|
+
|
|
1022
|
+
if (pData.Size !== undefined)
|
|
1023
|
+
{
|
|
1024
|
+
tmpHTML += '<div class="retold-remote-fileinfo-row"><span class="retold-remote-fileinfo-label">Size</span><span class="retold-remote-fileinfo-value">' + tmpSelf._formatFileSize(pData.Size) + '</span></div>';
|
|
1025
|
+
}
|
|
1026
|
+
if (pData.Width && pData.Height)
|
|
1027
|
+
{
|
|
1028
|
+
tmpHTML += '<div class="retold-remote-fileinfo-row"><span class="retold-remote-fileinfo-label">Dimensions</span><span class="retold-remote-fileinfo-value">' + pData.Width + ' x ' + pData.Height + '</span></div>';
|
|
1029
|
+
}
|
|
1030
|
+
if (pData.Duration)
|
|
1031
|
+
{
|
|
1032
|
+
let tmpMin = Math.floor(pData.Duration / 60);
|
|
1033
|
+
let tmpSec = Math.floor(pData.Duration % 60);
|
|
1034
|
+
tmpHTML += '<div class="retold-remote-fileinfo-row"><span class="retold-remote-fileinfo-label">Duration</span><span class="retold-remote-fileinfo-value">' + tmpMin + ':' + (tmpSec < 10 ? '0' : '') + tmpSec + '</span></div>';
|
|
1035
|
+
}
|
|
1036
|
+
if (pData.Codec)
|
|
1037
|
+
{
|
|
1038
|
+
tmpHTML += '<div class="retold-remote-fileinfo-row"><span class="retold-remote-fileinfo-label">Codec</span><span class="retold-remote-fileinfo-value">' + pData.Codec + '</span></div>';
|
|
1039
|
+
}
|
|
1040
|
+
if (pData.Format)
|
|
1041
|
+
{
|
|
1042
|
+
tmpHTML += '<div class="retold-remote-fileinfo-row"><span class="retold-remote-fileinfo-label">Format</span><span class="retold-remote-fileinfo-value">' + pData.Format + '</span></div>';
|
|
1043
|
+
}
|
|
1044
|
+
if (pData.Modified)
|
|
1045
|
+
{
|
|
1046
|
+
tmpHTML += '<div class="retold-remote-fileinfo-row"><span class="retold-remote-fileinfo-label">Modified</span><span class="retold-remote-fileinfo-value">' + new Date(pData.Modified).toLocaleString() + '</span></div>';
|
|
1047
|
+
}
|
|
1048
|
+
if (pData.Path)
|
|
1049
|
+
{
|
|
1050
|
+
tmpHTML += '<div class="retold-remote-fileinfo-row"><span class="retold-remote-fileinfo-label">Path</span><span class="retold-remote-fileinfo-value">' + pData.Path + '</span></div>';
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
tmpOverlay.innerHTML = tmpHTML;
|
|
501
1054
|
}
|
|
502
|
-
|
|
1055
|
+
|
|
1056
|
+
// Populate the video stats bar (if viewing a video)
|
|
1057
|
+
let tmpStatsBar = document.getElementById('RetoldRemote-VideoStats');
|
|
1058
|
+
if (tmpStatsBar)
|
|
503
1059
|
{
|
|
504
|
-
|
|
505
|
-
|
|
1060
|
+
let tmpStatsHTML = '';
|
|
1061
|
+
|
|
1062
|
+
if (pData.Duration)
|
|
1063
|
+
{
|
|
1064
|
+
let tmpMin = Math.floor(pData.Duration / 60);
|
|
1065
|
+
let tmpSec = Math.floor(pData.Duration % 60);
|
|
1066
|
+
tmpStatsHTML += '<span><span class="retold-remote-video-stat-label">Duration</span> <span class="retold-remote-video-stat-value">' + tmpMin + ':' + (tmpSec < 10 ? '0' : '') + tmpSec + '</span></span>';
|
|
1067
|
+
}
|
|
1068
|
+
if (pData.Width && pData.Height)
|
|
1069
|
+
{
|
|
1070
|
+
tmpStatsHTML += '<span><span class="retold-remote-video-stat-label">Resolution</span> <span class="retold-remote-video-stat-value">' + pData.Width + '×' + pData.Height + '</span></span>';
|
|
1071
|
+
}
|
|
1072
|
+
if (pData.Codec)
|
|
1073
|
+
{
|
|
1074
|
+
tmpStatsHTML += '<span><span class="retold-remote-video-stat-label">Codec</span> <span class="retold-remote-video-stat-value">' + pData.Codec + '</span></span>';
|
|
1075
|
+
}
|
|
1076
|
+
if (pData.Bitrate)
|
|
1077
|
+
{
|
|
1078
|
+
let tmpBitrate = pData.Bitrate;
|
|
1079
|
+
let tmpBitrateStr;
|
|
1080
|
+
if (tmpBitrate >= 1000000)
|
|
1081
|
+
{
|
|
1082
|
+
tmpBitrateStr = (tmpBitrate / 1000000).toFixed(1) + ' Mbps';
|
|
1083
|
+
}
|
|
1084
|
+
else if (tmpBitrate >= 1000)
|
|
1085
|
+
{
|
|
1086
|
+
tmpBitrateStr = Math.round(tmpBitrate / 1000) + ' kbps';
|
|
1087
|
+
}
|
|
1088
|
+
else
|
|
1089
|
+
{
|
|
1090
|
+
tmpBitrateStr = tmpBitrate + ' bps';
|
|
1091
|
+
}
|
|
1092
|
+
tmpStatsHTML += '<span><span class="retold-remote-video-stat-label">Bitrate</span> <span class="retold-remote-video-stat-value">' + tmpBitrateStr + '</span></span>';
|
|
1093
|
+
}
|
|
1094
|
+
if (pData.Size !== undefined)
|
|
1095
|
+
{
|
|
1096
|
+
tmpStatsHTML += '<span><span class="retold-remote-video-stat-label">Size</span> <span class="retold-remote-video-stat-value">' + tmpSelf._formatFileSize(pData.Size) + '</span></span>';
|
|
1097
|
+
}
|
|
506
1098
|
|
|
507
|
-
|
|
1099
|
+
// Preserve the Explore and VLC buttons if they exist
|
|
1100
|
+
let tmpExploreBtn = tmpStatsBar.querySelector('.retold-remote-explore-btn');
|
|
1101
|
+
let tmpExploreHTML = tmpExploreBtn ? tmpExploreBtn.outerHTML : '';
|
|
1102
|
+
let tmpVLCBtn = tmpStatsBar.querySelector('.retold-remote-vlc-btn');
|
|
1103
|
+
let tmpVLCHTML = tmpVLCBtn ? tmpVLCBtn.outerHTML : '';
|
|
1104
|
+
|
|
1105
|
+
tmpStatsBar.innerHTML = tmpStatsHTML + tmpExploreHTML + tmpVLCHTML;
|
|
1106
|
+
}
|
|
508
1107
|
});
|
|
509
1108
|
}
|
|
510
1109
|
|