retold-remote 0.0.22 → 0.0.25
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/css/retold-remote.css +87 -20
- package/docs/README.md +59 -11
- package/docs/_sidebar.md +1 -0
- package/docs/collections.md +30 -0
- package/docs/ebook-reader.md +75 -1
- package/docs/image-explorer.md +27 -1
- package/docs/server-setup.md +28 -18
- package/docs/stack-launcher.md +218 -0
- package/docs/ultravisor-integration.md +2 -0
- package/package.json +10 -7
- package/source/Pict-Application-RetoldRemote.js +2 -0
- package/source/RetoldRemote-ExtensionMaps.js +1 -1
- package/source/cli/RetoldRemote-Server-Setup.js +240 -2
- package/source/cli/RetoldRemote-Stack-Launcher.js +387 -0
- package/source/cli/RetoldRemote-Stack-Run.js +41 -0
- package/source/cli/commands/RetoldRemote-Command-Serve.js +129 -54
- package/source/providers/CollectionManager-AddItems.js +166 -0
- package/source/providers/Pict-Provider-GalleryNavigation.js +46 -0
- package/source/providers/keyboard-handlers/KeyHandler-ImageExplorer.js +5 -0
- package/source/providers/keyboard-handlers/KeyHandler-Viewer.js +23 -0
- package/source/server/RetoldRemote-CollectionExportService.js +696 -0
- package/source/server/RetoldRemote-CollectionService.js +5 -0
- package/source/server/RetoldRemote-EbookService.js +194 -3
- package/source/server/RetoldRemote-SubimageService.js +530 -0
- package/source/server/RetoldRemote-ToolDetector.js +50 -0
- package/source/server/RetoldRemote-UltravisorOperations.js +6 -6
- package/source/views/MediaViewer-EbookViewer.js +419 -1
- package/source/views/MediaViewer-PdfViewer.js +963 -0
- package/source/views/PictView-Remote-CollectionsPanel.js +166 -0
- package/source/views/PictView-Remote-ImageExplorer.js +606 -1
- package/source/views/PictView-Remote-ImageViewer.js +2 -2
- package/source/views/PictView-Remote-Layout.js +12 -0
- package/source/views/PictView-Remote-MediaViewer.js +83 -25
- package/source/views/PictView-Remote-SubimagesPanel.js +353 -0
- package/web-application/css/retold-remote.css +87 -20
- package/web-application/docs/README.md +59 -11
- package/web-application/docs/_sidebar.md +1 -0
- package/web-application/docs/collections.md +30 -0
- package/web-application/docs/ebook-reader.md +75 -1
- package/web-application/docs/image-explorer.md +27 -1
- package/web-application/docs/server-setup.md +28 -18
- package/web-application/docs/stack-launcher.md +218 -0
- package/web-application/docs/ultravisor-integration.md +2 -0
- package/web-application/retold-remote.js +399 -45
- package/web-application/retold-remote.js.map +1 -1
- package/web-application/retold-remote.min.js +13 -12
- package/web-application/retold-remote.min.js.map +1 -1
|
@@ -2,6 +2,7 @@ const libPictView = require('pict-view');
|
|
|
2
2
|
|
|
3
3
|
const _MediaViewerEbookViewer = require('./MediaViewer-EbookViewer');
|
|
4
4
|
const _MediaViewerCodeViewer = require('./MediaViewer-CodeViewer');
|
|
5
|
+
const _MediaViewerPdfViewer = require('./MediaViewer-PdfViewer');
|
|
5
6
|
|
|
6
7
|
const _ViewConfiguration =
|
|
7
8
|
{
|
|
@@ -59,6 +60,7 @@ class RetoldRemoteMediaViewerView extends libPictView
|
|
|
59
60
|
tmpHTML += '<button class="retold-remote-viewer-nav-btn" onclick="pict.providers[\'RetoldRemote-GalleryNavigation\'].closeViewer()" title="Back (Esc)">← Back</button>';
|
|
60
61
|
tmpHTML += '<button class="retold-remote-viewer-nav-btn" onclick="pict.providers[\'RetoldRemote-GalleryNavigation\'].prevFile()" title="Previous (k)">‹ Prev</button>';
|
|
61
62
|
tmpHTML += '<div class="retold-remote-viewer-title">' + this.pict.providers['RetoldRemote-FormattingUtilities'].escapeHTML(tmpFileName) + '</div>';
|
|
63
|
+
tmpHTML += '<button class="retold-remote-viewer-nav-btn" id="RetoldRemote-HeaderExploreBtn" onclick="pict.views[\'RetoldRemote-ImageExplorer\'].showExplorer(pict.AppData.RetoldRemote.CurrentViewerFile)" title="Explore image (e)" style="display:none;">🔍 Explore</button>';
|
|
62
64
|
tmpHTML += '<button class="retold-remote-viewer-nav-btn" onclick="pict.providers[\'RetoldRemote-GalleryNavigation\'].nextFile()" title="Next (j)">Next ›</button>';
|
|
63
65
|
tmpHTML += '<button class="retold-remote-viewer-nav-btn" onclick="pict.providers[\'RetoldRemote-GalleryNavigation\']._toggleFileInfo()" title="Info (i)">ⓘ</button>';
|
|
64
66
|
tmpHTML += '<button class="retold-remote-viewer-nav-btn" onclick="pict.views[\'RetoldRemote-MediaViewer\'].toggleDistractionFree()" title="Distraction-Free (d)">▢</button>';
|
|
@@ -127,7 +129,7 @@ class RetoldRemoteMediaViewerView extends libPictView
|
|
|
127
129
|
this._loadCodeViewer(tmpContentURL, pFilePath);
|
|
128
130
|
}
|
|
129
131
|
|
|
130
|
-
// Load ebook
|
|
132
|
+
// Load document viewers: ebook for epub/mobi, PDF for pdf, convert-then-PDF for others
|
|
131
133
|
if (pMediaType === 'document')
|
|
132
134
|
{
|
|
133
135
|
let tmpExt = pFilePath.replace(/^.*\./, '').toLowerCase();
|
|
@@ -135,7 +137,15 @@ class RetoldRemoteMediaViewerView extends libPictView
|
|
|
135
137
|
{
|
|
136
138
|
this._loadEbookViewer(tmpContentURL, pFilePath);
|
|
137
139
|
}
|
|
138
|
-
|
|
140
|
+
else if (tmpExt === 'pdf')
|
|
141
|
+
{
|
|
142
|
+
this._loadPdfViewer(tmpContentURL, pFilePath);
|
|
143
|
+
}
|
|
144
|
+
else
|
|
145
|
+
{
|
|
146
|
+
// Convertible document types — convert to PDF first, then load PDF viewer
|
|
147
|
+
this._loadConvertedDocumentViewer(pFilePath);
|
|
148
|
+
}
|
|
139
149
|
}
|
|
140
150
|
|
|
141
151
|
// Set up swipe navigation for touch devices
|
|
@@ -533,19 +543,12 @@ class RetoldRemoteMediaViewerView extends libPictView
|
|
|
533
543
|
tmpImg.onclick = function () { pict.views['RetoldRemote-ImageViewer'].toggleZoom(); };
|
|
534
544
|
tmpFragment.appendChild(tmpImg);
|
|
535
545
|
|
|
536
|
-
// Explore button
|
|
537
|
-
let
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
tmpBtn.innerHTML = '🔍 Explore';
|
|
543
|
-
tmpBtn.onclick = function ()
|
|
544
|
-
{
|
|
545
|
-
pict.views['RetoldRemote-ImageExplorer'].showExplorer(
|
|
546
|
-
pict.AppData.RetoldRemote.CurrentViewerFile);
|
|
547
|
-
};
|
|
548
|
-
tmpFragment.appendChild(tmpBtn);
|
|
546
|
+
// Always show the Explore button in the header nav bar for images
|
|
547
|
+
let tmpHeaderExploreBtn = document.getElementById('RetoldRemote-HeaderExploreBtn');
|
|
548
|
+
if (tmpHeaderExploreBtn)
|
|
549
|
+
{
|
|
550
|
+
tmpHeaderExploreBtn.style.display = '';
|
|
551
|
+
}
|
|
549
552
|
|
|
550
553
|
// Dimension badge for large images
|
|
551
554
|
if (pShowExplore && pOrigWidth && pOrigHeight)
|
|
@@ -568,12 +571,6 @@ class RetoldRemoteMediaViewerView extends libPictView
|
|
|
568
571
|
+ 'id="RetoldRemote-ImageViewer-Img" '
|
|
569
572
|
+ 'onload="pict.views[\'RetoldRemote-ImageViewer\'].initImage()" '
|
|
570
573
|
+ 'onclick="pict.views[\'RetoldRemote-ImageViewer\'].toggleZoom()">';
|
|
571
|
-
// Explore button (hidden by default, shown by ImageViewer for large images)
|
|
572
|
-
tmpHTML += '<button class="retold-remote-image-explore-btn" id="RetoldRemote-ImageExploreBtn" '
|
|
573
|
-
+ 'style="display:none" '
|
|
574
|
-
+ 'onclick="pict.views[\'RetoldRemote-ImageExplorer\'].showExplorer(pict.AppData.RetoldRemote.CurrentViewerFile)" '
|
|
575
|
-
+ 'title="Open in deep-zoom explorer (e)">'
|
|
576
|
-
+ '🔍 Explore</button>';
|
|
577
574
|
return tmpHTML;
|
|
578
575
|
}
|
|
579
576
|
|
|
@@ -786,9 +783,7 @@ class RetoldRemoteMediaViewerView extends libPictView
|
|
|
786
783
|
|
|
787
784
|
if (tmpExtension === 'pdf')
|
|
788
785
|
{
|
|
789
|
-
return
|
|
790
|
-
+ 'style="width: 100%; height: 100%; border: none;">'
|
|
791
|
-
+ '</iframe>';
|
|
786
|
+
return this._buildPdfHTML(pURL, pFileName, pFilePath);
|
|
792
787
|
}
|
|
793
788
|
|
|
794
789
|
if (tmpExtension === 'epub' || tmpExtension === 'mobi')
|
|
@@ -796,7 +791,15 @@ class RetoldRemoteMediaViewerView extends libPictView
|
|
|
796
791
|
return this._buildEbookHTML(pURL, pFileName, pFilePath);
|
|
797
792
|
}
|
|
798
793
|
|
|
799
|
-
// For
|
|
794
|
+
// For convertible document types (doc, docx, rtf, odt, wpd, etc.),
|
|
795
|
+
// show the PDF viewer shell — conversion happens async in _loadDocumentViewer
|
|
796
|
+
let tmpConvertibleExts = { 'doc': true, 'docx': true, 'rtf': true, 'odt': true, 'wpd': true, 'wps': true, 'pages': true, 'odp': true, 'ppt': true, 'pptx': true, 'ods': true, 'xls': true, 'xlsx': true };
|
|
797
|
+
if (tmpConvertibleExts[tmpExtension])
|
|
798
|
+
{
|
|
799
|
+
return this._buildPdfHTML(pURL, pFileName, pFilePath);
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
// Unknown document types: show a download link
|
|
800
803
|
let tmpIconProvider = this.pict.providers['RetoldRemote-Icons'];
|
|
801
804
|
let tmpDocIconHTML = tmpIconProvider ? '<span class="retold-remote-icon retold-remote-icon-lg">' + tmpIconProvider.getIcon('document-large', 64) + '</span>' : '📄';
|
|
802
805
|
return '<div style="text-align: center; padding: 40px;">'
|
|
@@ -813,6 +816,60 @@ class RetoldRemoteMediaViewerView extends libPictView
|
|
|
813
816
|
// ebookGoToChapter, ebookPrevPage, ebookNextPage, toggleEbookTOC
|
|
814
817
|
// are in MediaViewer-EbookViewer.js (mixed in below).
|
|
815
818
|
|
|
819
|
+
/**
|
|
820
|
+
* Convert a document (doc, docx, rtf, odt, wpd, etc.) to PDF and load in the PDF viewer.
|
|
821
|
+
* Shows a converting message in the PDF content area while waiting.
|
|
822
|
+
*
|
|
823
|
+
* @param {string} pFilePath - Relative file path
|
|
824
|
+
*/
|
|
825
|
+
_loadConvertedDocumentViewer(pFilePath)
|
|
826
|
+
{
|
|
827
|
+
let tmpSelf = this;
|
|
828
|
+
let tmpProvider = this.pict.providers['RetoldRemote-Provider'];
|
|
829
|
+
let tmpPathParam = tmpProvider ? tmpProvider._getPathParam(pFilePath) : encodeURIComponent(pFilePath);
|
|
830
|
+
|
|
831
|
+
// Show converting message in the PDF content area
|
|
832
|
+
let tmpContent = document.getElementById('RetoldRemote-PdfContent');
|
|
833
|
+
if (tmpContent)
|
|
834
|
+
{
|
|
835
|
+
tmpContent.innerHTML = '<div style="text-align:center;padding:40px;color:var(--retold-text-dim);">'
|
|
836
|
+
+ '<div style="font-size:1.5rem;margin-bottom:12px;">⚙</div>'
|
|
837
|
+
+ 'Converting document to PDF\u2026'
|
|
838
|
+
+ '</div>';
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
fetch('/api/media/doc-convert?path=' + tmpPathParam)
|
|
842
|
+
.then((pResponse) => pResponse.json())
|
|
843
|
+
.then((pData) =>
|
|
844
|
+
{
|
|
845
|
+
if (!pData || !pData.Success)
|
|
846
|
+
{
|
|
847
|
+
throw new Error(pData ? pData.Error : 'Conversion failed.');
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
// Build the PDF URL from the ebook cache (same serving endpoint)
|
|
851
|
+
let tmpPdfURL = '/api/media/ebook/' + pData.CacheKey + '/' + pData.OutputFilename;
|
|
852
|
+
|
|
853
|
+
// Load the PDF viewer with the converted file
|
|
854
|
+
tmpSelf._loadPdfViewer(tmpPdfURL, pFilePath);
|
|
855
|
+
})
|
|
856
|
+
.catch((pError) =>
|
|
857
|
+
{
|
|
858
|
+
if (tmpContent)
|
|
859
|
+
{
|
|
860
|
+
let tmpFmt = tmpSelf.pict.providers['RetoldRemote-FormattingUtilities'];
|
|
861
|
+
tmpContent.innerHTML = '<div style="text-align:center;padding:40px;color:var(--retold-text-dim);">'
|
|
862
|
+
+ '<div style="margin-bottom:12px;color:#e06c75;">Conversion failed</div>'
|
|
863
|
+
+ '<div style="font-size:0.85rem;">' + tmpFmt.escapeHTML(pError.message) + '</div>'
|
|
864
|
+
+ '<div style="margin-top:16px;">'
|
|
865
|
+
+ '<a href="' + (tmpProvider ? tmpProvider.getContentURL(pFilePath) : '/content/' + encodeURIComponent(pFilePath))
|
|
866
|
+
+ '" target="_blank" style="color:var(--retold-accent);font-size:0.85rem;">Download original file</a>'
|
|
867
|
+
+ '</div>'
|
|
868
|
+
+ '</div>';
|
|
869
|
+
}
|
|
870
|
+
});
|
|
871
|
+
}
|
|
872
|
+
|
|
816
873
|
_buildFallbackHTML(pURL, pFileName)
|
|
817
874
|
{
|
|
818
875
|
let tmpIconProvider = this.pict.providers['RetoldRemote-Icons'];
|
|
@@ -942,6 +999,7 @@ class RetoldRemoteMediaViewerView extends libPictView
|
|
|
942
999
|
|
|
943
1000
|
Object.assign(RetoldRemoteMediaViewerView.prototype, _MediaViewerEbookViewer);
|
|
944
1001
|
Object.assign(RetoldRemoteMediaViewerView.prototype, _MediaViewerCodeViewer);
|
|
1002
|
+
Object.assign(RetoldRemoteMediaViewerView.prototype, _MediaViewerPdfViewer);
|
|
945
1003
|
|
|
946
1004
|
RetoldRemoteMediaViewerView.default_configuration = _ViewConfiguration;
|
|
947
1005
|
|
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
const libPictView = require('pict-view');
|
|
2
|
+
|
|
3
|
+
const _ViewConfiguration =
|
|
4
|
+
{
|
|
5
|
+
ViewIdentifier: "RetoldRemote-SubimagesPanel",
|
|
6
|
+
DefaultRenderable: "RetoldRemote-SubimagesPanel",
|
|
7
|
+
DefaultDestinationAddress: "#RetoldRemote-Subimages-Container",
|
|
8
|
+
AutoRender: false,
|
|
9
|
+
|
|
10
|
+
CSS: ``
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Subimages Panel — sidebar tab showing labeled subimage regions
|
|
15
|
+
* for the currently viewed image file.
|
|
16
|
+
*
|
|
17
|
+
* Regions are fetched from the SubimageService API and displayed
|
|
18
|
+
* as a list with label, dimensions, and action buttons.
|
|
19
|
+
*/
|
|
20
|
+
class RetoldRemoteSubimagesPanelView extends libPictView
|
|
21
|
+
{
|
|
22
|
+
constructor(pFable, pOptions, pServiceHash)
|
|
23
|
+
{
|
|
24
|
+
super(pFable, pOptions, pServiceHash);
|
|
25
|
+
this._regions = [];
|
|
26
|
+
this._currentPath = '';
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Render the subimages panel for the current file.
|
|
31
|
+
*/
|
|
32
|
+
render()
|
|
33
|
+
{
|
|
34
|
+
let tmpContainer = document.getElementById('RetoldRemote-Subimages-Container');
|
|
35
|
+
if (!tmpContainer)
|
|
36
|
+
{
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
let tmpRemote = this.pict.AppData.RetoldRemote;
|
|
41
|
+
let tmpFilePath = tmpRemote.CurrentViewerFile;
|
|
42
|
+
let tmpMediaType = tmpRemote.CurrentViewerMediaType;
|
|
43
|
+
|
|
44
|
+
// Show for images and documents (EPUB, PDF, CBZ pages)
|
|
45
|
+
if (!tmpFilePath)
|
|
46
|
+
{
|
|
47
|
+
tmpContainer.innerHTML = '<div style="padding:12px;color:var(--retold-text-dim);font-size:0.82rem;">View a file to see its regions.</div>';
|
|
48
|
+
this._regions = [];
|
|
49
|
+
this._currentPath = '';
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// If the file changed, fetch regions
|
|
54
|
+
if (tmpFilePath !== this._currentPath)
|
|
55
|
+
{
|
|
56
|
+
this._currentPath = tmpFilePath;
|
|
57
|
+
this._fetchAndRender(tmpFilePath, tmpContainer);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Re-render with cached regions (e.g. after add/delete)
|
|
62
|
+
this._renderRegionList(tmpContainer);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Force a refresh from the server for the current file.
|
|
67
|
+
*/
|
|
68
|
+
refresh()
|
|
69
|
+
{
|
|
70
|
+
this._currentPath = '';
|
|
71
|
+
this.render();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Fetch regions from the server and render.
|
|
76
|
+
*
|
|
77
|
+
* @param {string} pFilePath - Relative file path
|
|
78
|
+
* @param {HTMLElement} pContainer - The container to render into
|
|
79
|
+
*/
|
|
80
|
+
_fetchAndRender(pFilePath, pContainer)
|
|
81
|
+
{
|
|
82
|
+
let tmpSelf = this;
|
|
83
|
+
let tmpProvider = this.pict.providers['RetoldRemote-Provider'];
|
|
84
|
+
let tmpPathParam = tmpProvider ? tmpProvider._getPathParam(pFilePath) : encodeURIComponent(pFilePath);
|
|
85
|
+
|
|
86
|
+
pContainer.innerHTML = '<div style="padding:12px;color:var(--retold-text-dim);font-size:0.82rem;">Loading\u2026</div>';
|
|
87
|
+
|
|
88
|
+
fetch('/api/media/subimage-regions?path=' + tmpPathParam)
|
|
89
|
+
.then((pResponse) => pResponse.json())
|
|
90
|
+
.then((pResult) =>
|
|
91
|
+
{
|
|
92
|
+
if (pResult && pResult.Success)
|
|
93
|
+
{
|
|
94
|
+
tmpSelf._regions = pResult.Regions || [];
|
|
95
|
+
}
|
|
96
|
+
else
|
|
97
|
+
{
|
|
98
|
+
tmpSelf._regions = [];
|
|
99
|
+
}
|
|
100
|
+
tmpSelf._renderRegionList(pContainer);
|
|
101
|
+
})
|
|
102
|
+
.catch(() =>
|
|
103
|
+
{
|
|
104
|
+
tmpSelf._regions = [];
|
|
105
|
+
tmpSelf._renderRegionList(pContainer);
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Render the region list into the container.
|
|
111
|
+
*
|
|
112
|
+
* @param {HTMLElement} pContainer - The container element
|
|
113
|
+
*/
|
|
114
|
+
_renderRegionList(pContainer)
|
|
115
|
+
{
|
|
116
|
+
if (!pContainer)
|
|
117
|
+
{
|
|
118
|
+
pContainer = document.getElementById('RetoldRemote-Subimages-Container');
|
|
119
|
+
}
|
|
120
|
+
if (!pContainer)
|
|
121
|
+
{
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Also sync the regions to the image explorer if it's open
|
|
126
|
+
let tmpIEX = this.pict.views['RetoldRemote-ImageExplorer'];
|
|
127
|
+
if (tmpIEX && tmpIEX._currentPath === this._currentPath)
|
|
128
|
+
{
|
|
129
|
+
tmpIEX._savedRegions = this._regions;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
let tmpFmt = this.pict.providers['RetoldRemote-FormattingUtilities'];
|
|
133
|
+
let tmpFileName = (this._currentPath || '').replace(/^.*\//, '');
|
|
134
|
+
|
|
135
|
+
let tmpHTML = '<div class="retold-remote-subimages-panel">';
|
|
136
|
+
|
|
137
|
+
// Header
|
|
138
|
+
tmpHTML += '<div style="padding:8px 10px;font-size:0.78rem;color:var(--retold-text-dim);border-bottom:1px solid var(--retold-border);">';
|
|
139
|
+
tmpHTML += tmpFmt.escapeHTML(tmpFileName);
|
|
140
|
+
tmpHTML += ' — ' + this._regions.length + ' region' + (this._regions.length !== 1 ? 's' : '');
|
|
141
|
+
tmpHTML += '</div>';
|
|
142
|
+
|
|
143
|
+
if (this._regions.length === 0)
|
|
144
|
+
{
|
|
145
|
+
tmpHTML += '<div style="padding:16px 12px;color:var(--retold-text-dim);font-size:0.8rem;text-align:center;">';
|
|
146
|
+
tmpHTML += 'No regions yet.<br>Use selection tools in the viewer to create regions.';
|
|
147
|
+
tmpHTML += '</div>';
|
|
148
|
+
}
|
|
149
|
+
else
|
|
150
|
+
{
|
|
151
|
+
for (let i = 0; i < this._regions.length; i++)
|
|
152
|
+
{
|
|
153
|
+
let tmpRegion = this._regions[i];
|
|
154
|
+
let tmpLabel = tmpRegion.Label || '(unlabeled)';
|
|
155
|
+
let tmpIsText = (tmpRegion.Type === 'text-selection');
|
|
156
|
+
|
|
157
|
+
// Build description based on region type
|
|
158
|
+
let tmpDesc = '';
|
|
159
|
+
if (tmpIsText)
|
|
160
|
+
{
|
|
161
|
+
let tmpPreview = (tmpRegion.SelectedText || '').substring(0, 60);
|
|
162
|
+
if ((tmpRegion.SelectedText || '').length > 60) tmpPreview += '\u2026';
|
|
163
|
+
tmpDesc = tmpPreview || '(no text)';
|
|
164
|
+
if (tmpRegion.PageNumber)
|
|
165
|
+
{
|
|
166
|
+
tmpDesc = 'p.' + tmpRegion.PageNumber + ' \u2014 ' + tmpDesc;
|
|
167
|
+
}
|
|
168
|
+
else if (tmpRegion.ChapterTitle)
|
|
169
|
+
{
|
|
170
|
+
tmpDesc = tmpRegion.ChapterTitle;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
else
|
|
174
|
+
{
|
|
175
|
+
tmpDesc = (tmpRegion.Width || 0) + ' \u00d7 ' + (tmpRegion.Height || 0) + ' px';
|
|
176
|
+
if (tmpRegion.PageNumber)
|
|
177
|
+
{
|
|
178
|
+
tmpDesc = 'p.' + tmpRegion.PageNumber + ' \u2014 ' + tmpDesc;
|
|
179
|
+
}
|
|
180
|
+
else if (tmpRegion.X !== null && tmpRegion.Y !== null)
|
|
181
|
+
{
|
|
182
|
+
tmpDesc += ' at ' + tmpRegion.X + ', ' + tmpRegion.Y;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Type icon
|
|
187
|
+
let tmpTypeIcon = tmpIsText ? '\uD83D\uDCDD' : '\uD83D\uDD32';
|
|
188
|
+
|
|
189
|
+
tmpHTML += '<div class="retold-remote-subimages-item" data-region-id="' + tmpRegion.ID + '">';
|
|
190
|
+
|
|
191
|
+
// Region info
|
|
192
|
+
tmpHTML += '<div class="retold-remote-subimages-item-info">';
|
|
193
|
+
tmpHTML += '<div class="retold-remote-subimages-item-label">' + tmpTypeIcon + ' ' + tmpFmt.escapeHTML(tmpLabel) + '</div>';
|
|
194
|
+
tmpHTML += '<div class="retold-remote-subimages-item-dims">' + tmpFmt.escapeHTML(tmpDesc) + '</div>';
|
|
195
|
+
tmpHTML += '</div>';
|
|
196
|
+
|
|
197
|
+
// Actions
|
|
198
|
+
tmpHTML += '<div class="retold-remote-subimages-item-actions">';
|
|
199
|
+
tmpHTML += '<button onclick="pict.views[\'RetoldRemote-SubimagesPanel\'].navigateToRegion(\'' + tmpRegion.ID + '\')" title="Navigate to region" style="font-size:0.7rem;padding:2px 6px;">🔎</button>';
|
|
200
|
+
tmpHTML += '<button onclick="pict.views[\'RetoldRemote-SubimagesPanel\'].addRegionToCollection(\'' + tmpRegion.ID + '\')" title="Add to collection" style="font-size:0.7rem;padding:2px 6px;">✚</button>';
|
|
201
|
+
tmpHTML += '<button onclick="pict.views[\'RetoldRemote-SubimagesPanel\'].deleteRegion(\'' + tmpRegion.ID + '\')" title="Delete region" style="font-size:0.7rem;padding:2px 6px;">🗑</button>';
|
|
202
|
+
tmpHTML += '</div>';
|
|
203
|
+
|
|
204
|
+
tmpHTML += '</div>';
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
tmpHTML += '</div>';
|
|
209
|
+
pContainer.innerHTML = tmpHTML;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Navigate to a specific region — handles images, EPUB, and PDF.
|
|
214
|
+
*
|
|
215
|
+
* @param {string} pRegionID - The region ID
|
|
216
|
+
*/
|
|
217
|
+
navigateToRegion(pRegionID)
|
|
218
|
+
{
|
|
219
|
+
let tmpRegion = null;
|
|
220
|
+
for (let i = 0; i < this._regions.length; i++)
|
|
221
|
+
{
|
|
222
|
+
if (this._regions[i].ID === pRegionID)
|
|
223
|
+
{
|
|
224
|
+
tmpRegion = this._regions[i];
|
|
225
|
+
break;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
if (!tmpRegion)
|
|
229
|
+
{
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
let tmpRemote = this.pict.AppData.RetoldRemote;
|
|
234
|
+
let tmpMediaViewer = this.pict.views['RetoldRemote-MediaViewer'];
|
|
235
|
+
|
|
236
|
+
// EPUB: navigate to CFI location
|
|
237
|
+
if (tmpRegion.CFI && tmpMediaViewer && tmpMediaViewer._activeRendition)
|
|
238
|
+
{
|
|
239
|
+
tmpMediaViewer._activeRendition.display(tmpRegion.CFI);
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// PDF: navigate to page
|
|
244
|
+
if (tmpRegion.PageNumber && tmpMediaViewer && typeof tmpMediaViewer._renderPdfPage === 'function')
|
|
245
|
+
{
|
|
246
|
+
tmpMediaViewer._renderPdfPage(tmpRegion.PageNumber);
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Image: use image explorer
|
|
251
|
+
let tmpIEX = this.pict.views['RetoldRemote-ImageExplorer'];
|
|
252
|
+
if (tmpRemote.ActiveMode === 'image-explorer' && tmpIEX)
|
|
253
|
+
{
|
|
254
|
+
tmpIEX.zoomToRegion(pRegionID);
|
|
255
|
+
}
|
|
256
|
+
else if (tmpIEX && this._currentPath)
|
|
257
|
+
{
|
|
258
|
+
tmpIEX.showExplorer(this._currentPath);
|
|
259
|
+
setTimeout(() =>
|
|
260
|
+
{
|
|
261
|
+
tmpIEX.zoomToRegion(pRegionID);
|
|
262
|
+
}, 800);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Add a subimage region to the active collection.
|
|
268
|
+
*
|
|
269
|
+
* @param {string} pRegionID - The region ID to add
|
|
270
|
+
*/
|
|
271
|
+
addRegionToCollection(pRegionID)
|
|
272
|
+
{
|
|
273
|
+
let tmpRegion = null;
|
|
274
|
+
for (let i = 0; i < this._regions.length; i++)
|
|
275
|
+
{
|
|
276
|
+
if (this._regions[i].ID === pRegionID)
|
|
277
|
+
{
|
|
278
|
+
tmpRegion = this._regions[i];
|
|
279
|
+
break;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if (!tmpRegion)
|
|
284
|
+
{
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
let tmpCollMgr = this.pict.providers['RetoldRemote-CollectionManager'];
|
|
289
|
+
if (tmpCollMgr)
|
|
290
|
+
{
|
|
291
|
+
let tmpGUID = tmpCollMgr.getQuickAddTargetGUID();
|
|
292
|
+
if (tmpGUID)
|
|
293
|
+
{
|
|
294
|
+
tmpCollMgr.addSubimageToCollection(tmpGUID, tmpRegion, this._currentPath);
|
|
295
|
+
}
|
|
296
|
+
else
|
|
297
|
+
{
|
|
298
|
+
let tmpTopBar = this.pict.views['ContentEditor-TopBar'];
|
|
299
|
+
if (tmpTopBar && typeof tmpTopBar.showAddToCollectionDropdown === 'function')
|
|
300
|
+
{
|
|
301
|
+
tmpTopBar.showAddToCollectionDropdown();
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Delete a subimage region.
|
|
309
|
+
*
|
|
310
|
+
* @param {string} pRegionID - The region ID to delete
|
|
311
|
+
*/
|
|
312
|
+
deleteRegion(pRegionID)
|
|
313
|
+
{
|
|
314
|
+
let tmpIEX = this.pict.views['RetoldRemote-ImageExplorer'];
|
|
315
|
+
if (tmpIEX && tmpIEX._currentPath === this._currentPath)
|
|
316
|
+
{
|
|
317
|
+
// Delegate to the explorer which handles the API call and overlay removal
|
|
318
|
+
tmpIEX.deleteRegion(pRegionID);
|
|
319
|
+
}
|
|
320
|
+
else
|
|
321
|
+
{
|
|
322
|
+
// Delete directly via API
|
|
323
|
+
let tmpSelf = this;
|
|
324
|
+
let tmpProvider = this.pict.providers['RetoldRemote-Provider'];
|
|
325
|
+
let tmpPathParam = tmpProvider ? tmpProvider._getPathParam(this._currentPath) : encodeURIComponent(this._currentPath);
|
|
326
|
+
|
|
327
|
+
fetch('/api/media/subimage-regions/' + encodeURIComponent(pRegionID) + '?path=' + tmpPathParam,
|
|
328
|
+
{
|
|
329
|
+
method: 'DELETE'
|
|
330
|
+
})
|
|
331
|
+
.then((pResponse) => pResponse.json())
|
|
332
|
+
.then((pResult) =>
|
|
333
|
+
{
|
|
334
|
+
if (pResult && pResult.Success)
|
|
335
|
+
{
|
|
336
|
+
tmpSelf._regions = pResult.Regions || [];
|
|
337
|
+
tmpSelf._renderRegionList();
|
|
338
|
+
|
|
339
|
+
let tmpToast = tmpSelf.pict.providers['RetoldRemote-ToastNotification'];
|
|
340
|
+
if (tmpToast)
|
|
341
|
+
{
|
|
342
|
+
tmpToast.showToast('Region deleted');
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
})
|
|
346
|
+
.catch(() => {});
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
RetoldRemoteSubimagesPanelView.default_configuration = _ViewConfiguration;
|
|
352
|
+
|
|
353
|
+
module.exports = RetoldRemoteSubimagesPanelView;
|
|
@@ -2263,26 +2263,7 @@ html, body
|
|
|
2263
2263
|
{
|
|
2264
2264
|
opacity: 1;
|
|
2265
2265
|
}
|
|
2266
|
-
|
|
2267
|
-
{
|
|
2268
|
-
position: absolute;
|
|
2269
|
-
bottom: 16px;
|
|
2270
|
-
right: 16px;
|
|
2271
|
-
background: rgba(40, 44, 52, 0.85);
|
|
2272
|
-
color: #abb2bf;
|
|
2273
|
-
border: 1px solid rgba(255, 255, 255, 0.15);
|
|
2274
|
-
padding: 6px 14px;
|
|
2275
|
-
border-radius: 6px;
|
|
2276
|
-
font-size: 0.82rem;
|
|
2277
|
-
cursor: pointer;
|
|
2278
|
-
z-index: 20;
|
|
2279
|
-
transition: background 0.2s, color 0.2s;
|
|
2280
|
-
}
|
|
2281
|
-
.retold-remote-image-explore-btn:hover
|
|
2282
|
-
{
|
|
2283
|
-
background: rgba(97, 175, 239, 0.3);
|
|
2284
|
-
color: #fff;
|
|
2285
|
-
}
|
|
2266
|
+
/* Explore button overlay styles removed — button moved to nav bar */
|
|
2286
2267
|
.retold-remote-image-large-badge
|
|
2287
2268
|
{
|
|
2288
2269
|
position: absolute;
|
|
@@ -2480,6 +2461,75 @@ html, body
|
|
|
2480
2461
|
text-align: center;
|
|
2481
2462
|
color: var(--retold-text-secondary);
|
|
2482
2463
|
}
|
|
2464
|
+
/* Subimage region overlays */
|
|
2465
|
+
.retold-remote-iex-region-overlay
|
|
2466
|
+
{
|
|
2467
|
+
border: 2px solid rgba(229, 192, 123, 0.85);
|
|
2468
|
+
background: rgba(229, 192, 123, 0.08);
|
|
2469
|
+
pointer-events: none;
|
|
2470
|
+
position: relative;
|
|
2471
|
+
}
|
|
2472
|
+
/* Subimages sidebar panel */
|
|
2473
|
+
.retold-remote-subimages-panel
|
|
2474
|
+
{
|
|
2475
|
+
overflow-y: auto;
|
|
2476
|
+
height: 100%;
|
|
2477
|
+
}
|
|
2478
|
+
.retold-remote-subimages-item
|
|
2479
|
+
{
|
|
2480
|
+
display: flex;
|
|
2481
|
+
align-items: center;
|
|
2482
|
+
justify-content: space-between;
|
|
2483
|
+
padding: 6px 10px;
|
|
2484
|
+
border-bottom: 1px solid var(--retold-border);
|
|
2485
|
+
cursor: default;
|
|
2486
|
+
transition: background 0.15s;
|
|
2487
|
+
}
|
|
2488
|
+
.retold-remote-subimages-item:hover
|
|
2489
|
+
{
|
|
2490
|
+
background: rgba(255, 255, 255, 0.04);
|
|
2491
|
+
}
|
|
2492
|
+
.retold-remote-subimages-item-info
|
|
2493
|
+
{
|
|
2494
|
+
flex: 1;
|
|
2495
|
+
min-width: 0;
|
|
2496
|
+
overflow: hidden;
|
|
2497
|
+
}
|
|
2498
|
+
.retold-remote-subimages-item-label
|
|
2499
|
+
{
|
|
2500
|
+
font-size: 0.82rem;
|
|
2501
|
+
color: var(--retold-text);
|
|
2502
|
+
white-space: nowrap;
|
|
2503
|
+
overflow: hidden;
|
|
2504
|
+
text-overflow: ellipsis;
|
|
2505
|
+
}
|
|
2506
|
+
.retold-remote-subimages-item-dims
|
|
2507
|
+
{
|
|
2508
|
+
font-size: 0.7rem;
|
|
2509
|
+
color: var(--retold-text-dim);
|
|
2510
|
+
margin-top: 1px;
|
|
2511
|
+
}
|
|
2512
|
+
.retold-remote-subimages-item-actions
|
|
2513
|
+
{
|
|
2514
|
+
display: flex;
|
|
2515
|
+
gap: 2px;
|
|
2516
|
+
margin-left: 6px;
|
|
2517
|
+
flex-shrink: 0;
|
|
2518
|
+
}
|
|
2519
|
+
.retold-remote-subimages-item-actions button
|
|
2520
|
+
{
|
|
2521
|
+
background: transparent;
|
|
2522
|
+
border: 1px solid var(--retold-border);
|
|
2523
|
+
border-radius: 3px;
|
|
2524
|
+
color: var(--retold-text-dim);
|
|
2525
|
+
cursor: pointer;
|
|
2526
|
+
transition: color 0.15s, border-color 0.15s;
|
|
2527
|
+
}
|
|
2528
|
+
.retold-remote-subimages-item-actions button:hover
|
|
2529
|
+
{
|
|
2530
|
+
color: var(--retold-text);
|
|
2531
|
+
border-color: var(--retold-accent);
|
|
2532
|
+
}
|
|
2483
2533
|
|
|
2484
2534
|
/* ============================================================
|
|
2485
2535
|
AudioExplorer
|
|
@@ -3515,6 +3565,23 @@ html, body
|
|
|
3515
3565
|
border-color: var(--retold-accent);
|
|
3516
3566
|
color: var(--retold-text-primary);
|
|
3517
3567
|
}
|
|
3568
|
+
.retold-remote-collections-export-btn
|
|
3569
|
+
{
|
|
3570
|
+
padding: 3px 8px;
|
|
3571
|
+
border: 1px solid var(--retold-border);
|
|
3572
|
+
border-radius: 3px;
|
|
3573
|
+
background: transparent;
|
|
3574
|
+
color: var(--retold-text-muted);
|
|
3575
|
+
font-size: 0.72rem;
|
|
3576
|
+
cursor: pointer;
|
|
3577
|
+
font-family: inherit;
|
|
3578
|
+
margin-left: auto;
|
|
3579
|
+
}
|
|
3580
|
+
.retold-remote-collections-export-btn:hover
|
|
3581
|
+
{
|
|
3582
|
+
border-color: var(--retold-accent);
|
|
3583
|
+
color: var(--retold-text-primary);
|
|
3584
|
+
}
|
|
3518
3585
|
/* ---- Item rows ---- */
|
|
3519
3586
|
.retold-remote-collection-item
|
|
3520
3587
|
{
|