retold-remote 0.0.1
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/LICENSE +21 -0
- package/css/retold-remote.css +83 -0
- package/html/codejar.js +511 -0
- package/html/index.html +23 -0
- package/package.json +68 -0
- package/server.js +43 -0
- package/source/Pict-Application-RetoldRemote-Configuration.json +7 -0
- package/source/Pict-Application-RetoldRemote.js +622 -0
- package/source/Pict-RetoldRemote-Bundle.js +14 -0
- package/source/cli/RetoldRemote-CLI-Program.js +15 -0
- package/source/cli/RetoldRemote-CLI-Run.js +3 -0
- package/source/cli/RetoldRemote-Server-Setup.js +257 -0
- package/source/cli/commands/RetoldRemote-Command-Serve.js +87 -0
- package/source/providers/Pict-Provider-GalleryFilterSort.js +597 -0
- package/source/providers/Pict-Provider-GalleryNavigation.js +819 -0
- package/source/providers/Pict-Provider-RetoldRemote.js +273 -0
- package/source/providers/Pict-Provider-RetoldRemoteIcons.js +640 -0
- package/source/providers/Pict-Provider-RetoldRemoteTheme.js +879 -0
- package/source/server/RetoldRemote-MediaService.js +536 -0
- package/source/server/RetoldRemote-PathRegistry.js +121 -0
- package/source/server/RetoldRemote-ThumbnailCache.js +89 -0
- package/source/server/RetoldRemote-ToolDetector.js +78 -0
- package/source/views/PictView-Remote-Gallery.js +1437 -0
- package/source/views/PictView-Remote-ImageViewer.js +363 -0
- package/source/views/PictView-Remote-Layout.js +420 -0
- package/source/views/PictView-Remote-MediaViewer.js +530 -0
- package/source/views/PictView-Remote-SettingsPanel.js +318 -0
- package/source/views/PictView-Remote-TopBar.js +206 -0
- package/web-application/codejar.js +511 -0
- package/web-application/css/retold-remote.css +83 -0
- package/web-application/index.html +23 -0
- package/web-application/js/pict.min.js +12 -0
- package/web-application/js/pict.min.js.map +1 -0
- package/web-application/retold-remote.compatible.js +5764 -0
- package/web-application/retold-remote.compatible.js.map +1 -0
- package/web-application/retold-remote.compatible.min.js +120 -0
- package/web-application/retold-remote.compatible.min.js.map +1 -0
- package/web-application/retold-remote.js +5763 -0
- package/web-application/retold-remote.js.map +1 -0
- package/web-application/retold-remote.min.js +120 -0
- package/web-application/retold-remote.min.js.map +1 -0
package/server.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Retold Remote -- Standalone Server Entry Point
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
* npm run build (build client bundles first)
|
|
6
|
+
* npm start (start this server)
|
|
7
|
+
* Open http://localhost:8086
|
|
8
|
+
*
|
|
9
|
+
* Or point to a specific media folder:
|
|
10
|
+
* node server.js [path-to-media-folder]
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const libPath = require('path');
|
|
14
|
+
const libSetupServer = require('./source/cli/RetoldRemote-Server-Setup.js');
|
|
15
|
+
|
|
16
|
+
let tmpContentPath = process.argv[2]
|
|
17
|
+
? libPath.resolve(process.argv[2])
|
|
18
|
+
: libPath.join(__dirname, 'content');
|
|
19
|
+
|
|
20
|
+
let tmpPort = parseInt(process.env.PORT, 10) || 8086;
|
|
21
|
+
let tmpHashedFilenames = (process.env.RETOLD_HASHED_FILENAMES === 'true');
|
|
22
|
+
|
|
23
|
+
libSetupServer(
|
|
24
|
+
{
|
|
25
|
+
ContentPath: tmpContentPath,
|
|
26
|
+
DistPath: libPath.join(__dirname, 'web-application'),
|
|
27
|
+
Port: tmpPort,
|
|
28
|
+
HashedFilenames: tmpHashedFilenames
|
|
29
|
+
},
|
|
30
|
+
function (pError, pServerInfo)
|
|
31
|
+
{
|
|
32
|
+
if (pError)
|
|
33
|
+
{
|
|
34
|
+
console.error('Failed to start server:', pError.message);
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}
|
|
37
|
+
pServerInfo.Fable.log.info('==========================================================');
|
|
38
|
+
pServerInfo.Fable.log.info(` Retold Remote running on http://localhost:${pServerInfo.Port}`);
|
|
39
|
+
pServerInfo.Fable.log.info('==========================================================');
|
|
40
|
+
pServerInfo.Fable.log.info(` Content path: ${tmpContentPath}`);
|
|
41
|
+
pServerInfo.Fable.log.info(` Browse: http://localhost:${pServerInfo.Port}/`);
|
|
42
|
+
pServerInfo.Fable.log.info('==========================================================');
|
|
43
|
+
});
|
|
@@ -0,0 +1,622 @@
|
|
|
1
|
+
const libContentEditorApplication = require('retold-content-system').PictContentEditor;
|
|
2
|
+
|
|
3
|
+
const libPictSectionFileBrowser = require('pict-section-filebrowser');
|
|
4
|
+
|
|
5
|
+
// Providers
|
|
6
|
+
const libProviderRetoldRemote = require('./providers/Pict-Provider-RetoldRemote.js');
|
|
7
|
+
const libProviderGalleryNavigation = require('./providers/Pict-Provider-GalleryNavigation.js');
|
|
8
|
+
const libProviderGalleryFilterSort = require('./providers/Pict-Provider-GalleryFilterSort.js');
|
|
9
|
+
const libProviderRetoldRemoteIcons = require('./providers/Pict-Provider-RetoldRemoteIcons.js');
|
|
10
|
+
const libProviderRetoldRemoteTheme = require('./providers/Pict-Provider-RetoldRemoteTheme.js');
|
|
11
|
+
|
|
12
|
+
// Views (replace parent views)
|
|
13
|
+
const libViewLayout = require('./views/PictView-Remote-Layout.js');
|
|
14
|
+
const libViewTopBar = require('./views/PictView-Remote-TopBar.js');
|
|
15
|
+
const libViewSettingsPanel = require('./views/PictView-Remote-SettingsPanel.js');
|
|
16
|
+
|
|
17
|
+
// Views (new)
|
|
18
|
+
const libViewGallery = require('./views/PictView-Remote-Gallery.js');
|
|
19
|
+
const libViewMediaViewer = require('./views/PictView-Remote-MediaViewer.js');
|
|
20
|
+
const libViewImageViewer = require('./views/PictView-Remote-ImageViewer.js');
|
|
21
|
+
|
|
22
|
+
// Application configuration
|
|
23
|
+
const _DefaultConfiguration = require('./Pict-Application-RetoldRemote-Configuration.json');
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Retold Remote Application
|
|
27
|
+
*
|
|
28
|
+
* A NAS media browser that extends ContentEditorApplication from
|
|
29
|
+
* retold-content-system. Replaces the text editor views with gallery
|
|
30
|
+
* and media viewer views while keeping the sidebar file browser and
|
|
31
|
+
* file navigation infrastructure intact.
|
|
32
|
+
*/
|
|
33
|
+
class RetoldRemoteApplication extends libContentEditorApplication
|
|
34
|
+
{
|
|
35
|
+
constructor(pFable, pOptions, pServiceHash)
|
|
36
|
+
{
|
|
37
|
+
let tmpOptions = Object.assign({}, _DefaultConfiguration, pOptions);
|
|
38
|
+
super(pFable, tmpOptions, pServiceHash);
|
|
39
|
+
|
|
40
|
+
// Replace parent views with media-focused versions.
|
|
41
|
+
// Re-registering with the same ViewIdentifier replaces the parent's view.
|
|
42
|
+
this.pict.addView('ContentEditor-Layout', libViewLayout.default_configuration, libViewLayout);
|
|
43
|
+
this.pict.addView('ContentEditor-TopBar', libViewTopBar.default_configuration, libViewTopBar);
|
|
44
|
+
|
|
45
|
+
// Add new views
|
|
46
|
+
this.pict.addView('RetoldRemote-Gallery', libViewGallery.default_configuration, libViewGallery);
|
|
47
|
+
this.pict.addView('RetoldRemote-MediaViewer', libViewMediaViewer.default_configuration, libViewMediaViewer);
|
|
48
|
+
this.pict.addView('RetoldRemote-ImageViewer', libViewImageViewer.default_configuration, libViewImageViewer);
|
|
49
|
+
this.pict.addView('RetoldRemote-SettingsPanel', libViewSettingsPanel.default_configuration, libViewSettingsPanel);
|
|
50
|
+
|
|
51
|
+
// Add new providers
|
|
52
|
+
this.pict.addProvider('RetoldRemote-Provider', libProviderRetoldRemote.default_configuration, libProviderRetoldRemote);
|
|
53
|
+
this.pict.addProvider('RetoldRemote-GalleryNavigation', libProviderGalleryNavigation.default_configuration, libProviderGalleryNavigation);
|
|
54
|
+
this.pict.addProvider('RetoldRemote-GalleryFilterSort', libProviderGalleryFilterSort.default_configuration, libProviderGalleryFilterSort);
|
|
55
|
+
this.pict.addProvider('RetoldRemote-Icons', libProviderRetoldRemoteIcons.default_configuration, libProviderRetoldRemoteIcons);
|
|
56
|
+
this.pict.addProvider('RetoldRemote-Theme', libProviderRetoldRemoteTheme.default_configuration, libProviderRetoldRemoteTheme);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
onAfterInitializeAsync(fCallback)
|
|
60
|
+
{
|
|
61
|
+
// Expose pict on window for inline onclick handlers
|
|
62
|
+
if (typeof (window) !== 'undefined')
|
|
63
|
+
{
|
|
64
|
+
window.pict = this.pict;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Initialize RetoldRemote-specific state
|
|
68
|
+
this.pict.AppData.RetoldRemote =
|
|
69
|
+
{
|
|
70
|
+
ActiveMode: 'gallery', // 'gallery' or 'viewer'
|
|
71
|
+
Theme: 'twilight', // Theme key (e.g. 'twilight', 'neo-tokyo')
|
|
72
|
+
ViewMode: 'list', // 'gallery' or 'list'
|
|
73
|
+
ThumbnailSize: 'medium', // 'small', 'medium', 'large'
|
|
74
|
+
RawFileList: [], // Unfiltered server response
|
|
75
|
+
GalleryItems: [], // Filtered+sorted file list (single source of truth)
|
|
76
|
+
GalleryCursorIndex: 0, // Currently highlighted item
|
|
77
|
+
GalleryFilter: 'all', // 'all', 'images', 'video', 'audio', 'documents'
|
|
78
|
+
SearchQuery: '',
|
|
79
|
+
ServerCapabilities: {}, // From /api/media/capabilities
|
|
80
|
+
FolderSummary: null, // From /api/media/folder-summary
|
|
81
|
+
CurrentViewerFile: '', // File being viewed
|
|
82
|
+
CurrentViewerMediaType: '', // Media type of viewed file
|
|
83
|
+
HashedFilenames: false, // From /api/remote/settings
|
|
84
|
+
ShowHiddenFiles: false,
|
|
85
|
+
DistractionFreeShowNav: true,
|
|
86
|
+
ImageFitMode: 'auto',
|
|
87
|
+
SidebarCollapsed: false,
|
|
88
|
+
SidebarWidth: 250,
|
|
89
|
+
|
|
90
|
+
// Filter state
|
|
91
|
+
FilterState:
|
|
92
|
+
{
|
|
93
|
+
MediaType: 'all',
|
|
94
|
+
Extensions: [], // e.g. ['png', 'jpg'] -- empty = all
|
|
95
|
+
SizeMin: null, // bytes or null
|
|
96
|
+
SizeMax: null,
|
|
97
|
+
DateModifiedAfter: null, // ISO date string or null
|
|
98
|
+
DateModifiedBefore: null,
|
|
99
|
+
DateCreatedAfter: null,
|
|
100
|
+
DateCreatedBefore: null
|
|
101
|
+
},
|
|
102
|
+
|
|
103
|
+
// Sort state
|
|
104
|
+
SortField: 'folder-first', // 'name', 'folder-first', 'created', 'modified'
|
|
105
|
+
SortDirection: 'asc', // 'asc', 'desc'
|
|
106
|
+
|
|
107
|
+
// Filter panel UI
|
|
108
|
+
FilterPanelOpen: false,
|
|
109
|
+
|
|
110
|
+
// Saved filter presets
|
|
111
|
+
FilterPresets: [] // [{ Name, FilterState, SortField, SortDirection }]
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
// Load persisted settings
|
|
115
|
+
this._loadRemoteSettings();
|
|
116
|
+
|
|
117
|
+
// Apply the loaded theme (must happen after _loadRemoteSettings sets Theme)
|
|
118
|
+
let tmpThemeProvider = this.pict.providers['RetoldRemote-Theme'];
|
|
119
|
+
if (tmpThemeProvider)
|
|
120
|
+
{
|
|
121
|
+
tmpThemeProvider.applyTheme(this.pict.AppData.RetoldRemote.Theme);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Initialize parent state (ContentEditor AppData)
|
|
125
|
+
this.pict.AppData.ContentEditor =
|
|
126
|
+
{
|
|
127
|
+
CurrentFile: '',
|
|
128
|
+
ActiveEditor: 'markdown',
|
|
129
|
+
IsDirty: false,
|
|
130
|
+
IsSaving: false,
|
|
131
|
+
IsLoading: false,
|
|
132
|
+
Files: [],
|
|
133
|
+
Document: { Segments: [{ Content: '' }] },
|
|
134
|
+
CodeContent: '',
|
|
135
|
+
SaveStatus: '',
|
|
136
|
+
SaveStatusClass: '',
|
|
137
|
+
AutoSegmentMarkdown: false,
|
|
138
|
+
AutoSegmentDepth: 1,
|
|
139
|
+
AutoContentPreview: false,
|
|
140
|
+
MarkdownEditingControls: true,
|
|
141
|
+
MarkdownWordWrap: true,
|
|
142
|
+
CodeWordWrap: false,
|
|
143
|
+
SidebarCollapsed: this.pict.AppData.RetoldRemote.SidebarCollapsed,
|
|
144
|
+
SidebarWidth: this.pict.AppData.RetoldRemote.SidebarWidth,
|
|
145
|
+
AutoPreviewImages: true,
|
|
146
|
+
AutoPreviewVideo: false,
|
|
147
|
+
AutoPreviewAudio: false,
|
|
148
|
+
ShowHiddenFiles: this.pict.AppData.RetoldRemote.ShowHiddenFiles,
|
|
149
|
+
TopicsFilePath: ''
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
// Render the layout shell
|
|
153
|
+
this.pict.views['ContentEditor-Layout'].render();
|
|
154
|
+
|
|
155
|
+
// Render the topbar
|
|
156
|
+
this.pict.views['ContentEditor-TopBar'].render();
|
|
157
|
+
|
|
158
|
+
let tmpSelf = this;
|
|
159
|
+
|
|
160
|
+
// Wire up file selection from the file browser sidebar
|
|
161
|
+
let tmpListProvider = this.pict.providers['Pict-FileBrowser-List'];
|
|
162
|
+
if (tmpListProvider)
|
|
163
|
+
{
|
|
164
|
+
let tmpOriginalSelectFile = tmpListProvider.selectFile.bind(tmpListProvider);
|
|
165
|
+
tmpListProvider.selectFile = function (pFileEntry)
|
|
166
|
+
{
|
|
167
|
+
tmpOriginalSelectFile(pFileEntry);
|
|
168
|
+
if (pFileEntry && pFileEntry.Type === 'file')
|
|
169
|
+
{
|
|
170
|
+
tmpSelf.navigateToFile(pFileEntry.Path);
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Wire up folder navigation
|
|
176
|
+
let tmpBrowseProvider = this.pict.providers['Pict-FileBrowser-Browse'];
|
|
177
|
+
if (tmpBrowseProvider)
|
|
178
|
+
{
|
|
179
|
+
let tmpOriginalNavigate = tmpBrowseProvider.navigateToFolder.bind(tmpBrowseProvider);
|
|
180
|
+
tmpBrowseProvider.navigateToFolder = function (pPath)
|
|
181
|
+
{
|
|
182
|
+
tmpOriginalNavigate(pPath);
|
|
183
|
+
tmpSelf.loadFileList(pPath);
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Fetch server capabilities and remote settings
|
|
188
|
+
let tmpProvider = this.pict.providers['RetoldRemote-Provider'];
|
|
189
|
+
if (tmpProvider)
|
|
190
|
+
{
|
|
191
|
+
tmpProvider.fetchCapabilities(
|
|
192
|
+
(pError, pCapabilities) =>
|
|
193
|
+
{
|
|
194
|
+
if (!pError && pCapabilities)
|
|
195
|
+
{
|
|
196
|
+
tmpSelf.pict.AppData.RetoldRemote.ServerCapabilities = pCapabilities;
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
tmpProvider.fetchRemoteSettings(
|
|
201
|
+
(pError, pSettings) =>
|
|
202
|
+
{
|
|
203
|
+
if (!pError && pSettings)
|
|
204
|
+
{
|
|
205
|
+
tmpSelf.pict.AppData.RetoldRemote.HashedFilenames = !!(pSettings.HashedFilenames);
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Bind keyboard navigation
|
|
211
|
+
let tmpNavProvider = this.pict.providers['RetoldRemote-GalleryNavigation'];
|
|
212
|
+
if (tmpNavProvider)
|
|
213
|
+
{
|
|
214
|
+
tmpNavProvider.bindKeyboardNavigation();
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Sync hidden files setting and load initial file list
|
|
218
|
+
this.syncHiddenFilesSetting(() =>
|
|
219
|
+
{
|
|
220
|
+
tmpSelf.loadFileList(null, () =>
|
|
221
|
+
{
|
|
222
|
+
tmpSelf.resolveHash();
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
// Do NOT call super.onAfterInitializeAsync because we have replaced
|
|
227
|
+
// the full initialization flow above. Instead call the grandparent's callback.
|
|
228
|
+
// The parent's onAfterInitializeAsync tries to render editors and load topics
|
|
229
|
+
// which we don't need.
|
|
230
|
+
if (typeof (fCallback) === 'function')
|
|
231
|
+
{
|
|
232
|
+
return fCallback();
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Override _getMediaType to add document category.
|
|
238
|
+
*
|
|
239
|
+
* @param {string} pExtension - Lowercase extension without dot
|
|
240
|
+
* @returns {string} 'image', 'video', 'audio', 'document', or 'other'
|
|
241
|
+
*/
|
|
242
|
+
_getMediaType(pExtension)
|
|
243
|
+
{
|
|
244
|
+
let tmpDocumentExtensions = { 'pdf': true, 'epub': true, 'mobi': true };
|
|
245
|
+
if (tmpDocumentExtensions[pExtension])
|
|
246
|
+
{
|
|
247
|
+
return 'document';
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
let tmpTextExtensions =
|
|
251
|
+
{
|
|
252
|
+
'js': true, 'mjs': true, 'cjs': true, 'ts': true, 'tsx': true, 'jsx': true,
|
|
253
|
+
'py': true, 'rb': true, 'java': true, 'c': true, 'cpp': true, 'h': true, 'hpp': true,
|
|
254
|
+
'cs': true, 'go': true, 'rs': true, 'php': true, 'sh': true, 'bash': true, 'zsh': true,
|
|
255
|
+
'pl': true, 'r': true, 'swift': true, 'kt': true, 'scala': true, 'lua': true,
|
|
256
|
+
'json': true, 'xml': true, 'yaml': true, 'yml': true, 'toml': true,
|
|
257
|
+
'ini': true, 'cfg': true, 'conf': true, 'env': true, 'properties': true,
|
|
258
|
+
'md': true, 'markdown': true, 'txt': true, 'csv': true, 'tsv': true, 'log': true,
|
|
259
|
+
'html': true, 'htm': true, 'css': true, 'scss': true, 'sass': true, 'less': true,
|
|
260
|
+
'sql': true, 'graphql': true, 'gql': true,
|
|
261
|
+
'makefile': true, 'dockerfile': true, 'gitignore': true, 'editorconfig': true,
|
|
262
|
+
'htaccess': true, 'npmrc': true, 'eslintrc': true, 'prettierrc': true
|
|
263
|
+
};
|
|
264
|
+
if (tmpTextExtensions[pExtension])
|
|
265
|
+
{
|
|
266
|
+
return 'text';
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return super._getMediaType(pExtension);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Override navigateToFile to route to media viewer instead of editor.
|
|
274
|
+
*
|
|
275
|
+
* @param {string} pFilePath - Relative file path
|
|
276
|
+
*/
|
|
277
|
+
navigateToFile(pFilePath)
|
|
278
|
+
{
|
|
279
|
+
if (!pFilePath)
|
|
280
|
+
{
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
let tmpEditorType = this.getEditorTypeForFile(pFilePath);
|
|
285
|
+
let tmpExtension = pFilePath.replace(/^.*\./, '').toLowerCase();
|
|
286
|
+
let tmpMediaType = this._getMediaType(tmpExtension);
|
|
287
|
+
let tmpRemote = this.pict.AppData.RetoldRemote;
|
|
288
|
+
|
|
289
|
+
// Update the hash (use hashed identifier when available)
|
|
290
|
+
let tmpFragProvider = this.pict.providers['RetoldRemote-Provider'];
|
|
291
|
+
let tmpFragId = tmpFragProvider ? tmpFragProvider.getFragmentIdentifier(pFilePath) : pFilePath;
|
|
292
|
+
window.location.hash = '#/view/' + tmpFragId;
|
|
293
|
+
|
|
294
|
+
// Update parent state for compatibility
|
|
295
|
+
this.pict.AppData.ContentEditor.CurrentFile = pFilePath;
|
|
296
|
+
this.pict.AppData.ContentEditor.ActiveEditor = tmpEditorType;
|
|
297
|
+
|
|
298
|
+
if (tmpEditorType === 'binary')
|
|
299
|
+
{
|
|
300
|
+
// Route binary files to the media viewer
|
|
301
|
+
let tmpViewer = this.pict.views['RetoldRemote-MediaViewer'];
|
|
302
|
+
if (tmpViewer)
|
|
303
|
+
{
|
|
304
|
+
tmpViewer.showMedia(pFilePath, tmpMediaType);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
else if (tmpMediaType === 'text')
|
|
308
|
+
{
|
|
309
|
+
// Text/code files: show inline in media viewer
|
|
310
|
+
let tmpViewer = this.pict.views['RetoldRemote-MediaViewer'];
|
|
311
|
+
if (tmpViewer)
|
|
312
|
+
{
|
|
313
|
+
tmpViewer.showMedia(pFilePath, 'text');
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
else if (tmpMediaType === 'other')
|
|
317
|
+
{
|
|
318
|
+
// Unknown files: show fallback view
|
|
319
|
+
let tmpViewer = this.pict.views['RetoldRemote-MediaViewer'];
|
|
320
|
+
if (tmpViewer)
|
|
321
|
+
{
|
|
322
|
+
tmpViewer.showMedia(pFilePath, 'document');
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
else
|
|
326
|
+
{
|
|
327
|
+
// Default: use media viewer
|
|
328
|
+
let tmpViewer = this.pict.views['RetoldRemote-MediaViewer'];
|
|
329
|
+
if (tmpViewer)
|
|
330
|
+
{
|
|
331
|
+
tmpViewer.showMedia(pFilePath, tmpMediaType);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Update the topbar
|
|
336
|
+
let tmpTopBar = this.pict.views['ContentEditor-TopBar'];
|
|
337
|
+
if (tmpTopBar)
|
|
338
|
+
{
|
|
339
|
+
tmpTopBar.updateInfo();
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Override loadFileList to also populate the gallery and fetch folder summary.
|
|
345
|
+
*/
|
|
346
|
+
loadFileList(pPath, fCallback)
|
|
347
|
+
{
|
|
348
|
+
let tmpCallback = (typeof (fCallback) === 'function') ? fCallback :
|
|
349
|
+
(typeof (pPath) === 'function') ? pPath : () => {};
|
|
350
|
+
let tmpSelf = this;
|
|
351
|
+
|
|
352
|
+
let tmpPath = (typeof (pPath) === 'string') ? pPath : null;
|
|
353
|
+
if (tmpPath === null && this.pict.AppData.PictFileBrowser && this.pict.AppData.PictFileBrowser.CurrentLocation)
|
|
354
|
+
{
|
|
355
|
+
tmpPath = this.pict.AppData.PictFileBrowser.CurrentLocation;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
let tmpURL = '/api/filebrowser/list';
|
|
359
|
+
if (tmpPath && tmpPath.length > 0)
|
|
360
|
+
{
|
|
361
|
+
let tmpMediaProvider = tmpSelf.pict.providers['RetoldRemote-Provider'];
|
|
362
|
+
if (tmpMediaProvider)
|
|
363
|
+
{
|
|
364
|
+
tmpURL += '?path=' + tmpMediaProvider._getPathParam(tmpPath);
|
|
365
|
+
}
|
|
366
|
+
else
|
|
367
|
+
{
|
|
368
|
+
tmpURL += '?path=' + encodeURIComponent(tmpPath);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
fetch(tmpURL)
|
|
373
|
+
.then((pResponse) =>
|
|
374
|
+
{
|
|
375
|
+
// Capture the folder hash header before parsing JSON
|
|
376
|
+
let tmpFolderHash = pResponse.headers.get('X-Retold-Folder-Hash');
|
|
377
|
+
if (tmpFolderHash && tmpPath)
|
|
378
|
+
{
|
|
379
|
+
let tmpHashProvider = tmpSelf.pict.providers['RetoldRemote-Provider'];
|
|
380
|
+
if (tmpHashProvider)
|
|
381
|
+
{
|
|
382
|
+
tmpHashProvider.registerHash(tmpPath, tmpFolderHash);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
return pResponse.json();
|
|
386
|
+
})
|
|
387
|
+
.then((pFileList) =>
|
|
388
|
+
{
|
|
389
|
+
tmpSelf.pict.AppData.PictFileBrowser = tmpSelf.pict.AppData.PictFileBrowser || {};
|
|
390
|
+
tmpSelf.pict.AppData.PictFileBrowser.FileList = pFileList || [];
|
|
391
|
+
tmpSelf.pict.AppData.PictFileBrowser.CurrentLocation = (typeof (pPath) === 'string') ? pPath : (tmpSelf.pict.AppData.PictFileBrowser.CurrentLocation || '');
|
|
392
|
+
|
|
393
|
+
// Register hashes from file list entries (when hashed filenames is on)
|
|
394
|
+
let tmpHashProvider = tmpSelf.pict.providers['RetoldRemote-Provider'];
|
|
395
|
+
if (tmpHashProvider && Array.isArray(pFileList))
|
|
396
|
+
{
|
|
397
|
+
for (let i = 0; i < pFileList.length; i++)
|
|
398
|
+
{
|
|
399
|
+
if (pFileList[i].Hash && pFileList[i].Path)
|
|
400
|
+
{
|
|
401
|
+
tmpHashProvider.registerHash(pFileList[i].Path, pFileList[i].Hash);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// Render the file browser sidebar
|
|
407
|
+
let tmpFileBrowserView = tmpSelf.pict.views['Pict-FileBrowser'];
|
|
408
|
+
if (tmpFileBrowserView)
|
|
409
|
+
{
|
|
410
|
+
tmpFileBrowserView.render();
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
let tmpListDetailView = tmpSelf.pict.views['Pict-FileBrowser-ListDetail'];
|
|
414
|
+
if (tmpListDetailView)
|
|
415
|
+
{
|
|
416
|
+
tmpListDetailView.render();
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// Populate raw file list and run filter pipeline
|
|
420
|
+
let tmpRemote = tmpSelf.pict.AppData.RetoldRemote;
|
|
421
|
+
tmpRemote.RawFileList = pFileList || [];
|
|
422
|
+
tmpRemote.ActiveMode = 'gallery';
|
|
423
|
+
tmpRemote.SearchQuery = '';
|
|
424
|
+
|
|
425
|
+
// Reset advanced filters on folder change (preserve MediaType preference)
|
|
426
|
+
tmpRemote.FilterState =
|
|
427
|
+
{
|
|
428
|
+
MediaType: tmpRemote.FilterState.MediaType,
|
|
429
|
+
Extensions: [],
|
|
430
|
+
SizeMin: null,
|
|
431
|
+
SizeMax: null,
|
|
432
|
+
DateModifiedAfter: null,
|
|
433
|
+
DateModifiedBefore: null,
|
|
434
|
+
DateCreatedAfter: null,
|
|
435
|
+
DateCreatedBefore: null
|
|
436
|
+
};
|
|
437
|
+
|
|
438
|
+
// Show the gallery, hide the viewer
|
|
439
|
+
let tmpGalleryContainer = document.getElementById('RetoldRemote-Gallery-Container');
|
|
440
|
+
let tmpViewerContainer = document.getElementById('RetoldRemote-Viewer-Container');
|
|
441
|
+
if (tmpGalleryContainer) tmpGalleryContainer.style.display = '';
|
|
442
|
+
if (tmpViewerContainer) tmpViewerContainer.style.display = 'none';
|
|
443
|
+
|
|
444
|
+
// Run the filter+sort pipeline (sets GalleryItems, resets cursor, renders gallery)
|
|
445
|
+
let tmpFilterSort = tmpSelf.pict.providers['RetoldRemote-GalleryFilterSort'];
|
|
446
|
+
if (tmpFilterSort)
|
|
447
|
+
{
|
|
448
|
+
tmpFilterSort.applyFilterSort();
|
|
449
|
+
}
|
|
450
|
+
else
|
|
451
|
+
{
|
|
452
|
+
// Fallback if provider not ready
|
|
453
|
+
tmpRemote.GalleryItems = pFileList || [];
|
|
454
|
+
tmpRemote.GalleryCursorIndex = 0;
|
|
455
|
+
let tmpGalleryView = tmpSelf.pict.views['RetoldRemote-Gallery'];
|
|
456
|
+
if (tmpGalleryView)
|
|
457
|
+
{
|
|
458
|
+
tmpGalleryView.renderGallery();
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// Update the hash (use hashed identifier when available)
|
|
463
|
+
let tmpCurrentPath = tmpSelf.pict.AppData.PictFileBrowser.CurrentLocation || '';
|
|
464
|
+
let tmpBrowseFragProvider = tmpSelf.pict.providers['RetoldRemote-Provider'];
|
|
465
|
+
let tmpBrowseFragId = (tmpBrowseFragProvider && tmpCurrentPath) ? tmpBrowseFragProvider.getFragmentIdentifier(tmpCurrentPath) : tmpCurrentPath;
|
|
466
|
+
window.location.hash = tmpBrowseFragId ? '#/browse/' + tmpBrowseFragId : '#/browse/';
|
|
467
|
+
|
|
468
|
+
// Fetch folder summary for topbar info
|
|
469
|
+
let tmpMediaProvider = tmpSelf.pict.providers['RetoldRemote-Provider'];
|
|
470
|
+
if (tmpMediaProvider)
|
|
471
|
+
{
|
|
472
|
+
tmpMediaProvider.fetchFolderSummary(tmpCurrentPath,
|
|
473
|
+
(pError, pSummary) =>
|
|
474
|
+
{
|
|
475
|
+
if (!pError && pSummary)
|
|
476
|
+
{
|
|
477
|
+
tmpRemote.FolderSummary = pSummary;
|
|
478
|
+
let tmpTopBar = tmpSelf.pict.views['ContentEditor-TopBar'];
|
|
479
|
+
if (tmpTopBar)
|
|
480
|
+
{
|
|
481
|
+
tmpTopBar.updateLocation();
|
|
482
|
+
tmpTopBar.updateInfo();
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
else
|
|
488
|
+
{
|
|
489
|
+
let tmpTopBar = tmpSelf.pict.views['ContentEditor-TopBar'];
|
|
490
|
+
if (tmpTopBar)
|
|
491
|
+
{
|
|
492
|
+
tmpTopBar.updateLocation();
|
|
493
|
+
tmpTopBar.updateInfo();
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
return tmpCallback();
|
|
498
|
+
})
|
|
499
|
+
.catch((pError) =>
|
|
500
|
+
{
|
|
501
|
+
tmpSelf.log.error(`Failed to load file list: ${pError.message}`);
|
|
502
|
+
return tmpCallback();
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
/**
|
|
507
|
+
* Override resolveHash to handle gallery and viewer routes.
|
|
508
|
+
*/
|
|
509
|
+
resolveHash()
|
|
510
|
+
{
|
|
511
|
+
let tmpHash = (window.location.hash || '').replace(/^#\/?/, '');
|
|
512
|
+
|
|
513
|
+
if (!tmpHash)
|
|
514
|
+
{
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
let tmpParts = tmpHash.split('/');
|
|
519
|
+
let tmpFragProvider = this.pict.providers['RetoldRemote-Provider'];
|
|
520
|
+
|
|
521
|
+
if (tmpParts[0] === 'browse')
|
|
522
|
+
{
|
|
523
|
+
let tmpRawPath = tmpParts.slice(1).join('/');
|
|
524
|
+
// Resolve hash token to actual path if needed
|
|
525
|
+
let tmpPath = tmpFragProvider ? tmpFragProvider.resolveFragmentIdentifier(tmpRawPath) : tmpRawPath;
|
|
526
|
+
let tmpCurrentPath = (this.pict.AppData.PictFileBrowser && this.pict.AppData.PictFileBrowser.CurrentLocation) || '';
|
|
527
|
+
if (tmpPath !== tmpCurrentPath)
|
|
528
|
+
{
|
|
529
|
+
this.loadFileList(tmpPath);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
else if (tmpParts[0] === 'view' && tmpParts.length >= 2)
|
|
533
|
+
{
|
|
534
|
+
let tmpRawPath = tmpParts.slice(1).join('/');
|
|
535
|
+
// Resolve hash token to actual path if needed
|
|
536
|
+
let tmpFilePath = tmpFragProvider ? tmpFragProvider.resolveFragmentIdentifier(tmpRawPath) : tmpRawPath;
|
|
537
|
+
if (this.pict.AppData.ContentEditor.CurrentFile === tmpFilePath)
|
|
538
|
+
{
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
541
|
+
this.navigateToFile(tmpFilePath);
|
|
542
|
+
}
|
|
543
|
+
else if (tmpParts[0] === 'edit' && tmpParts.length >= 2)
|
|
544
|
+
{
|
|
545
|
+
let tmpRawPath = tmpParts.slice(1).join('/');
|
|
546
|
+
let tmpFilePath = tmpFragProvider ? tmpFragProvider.resolveFragmentIdentifier(tmpRawPath) : tmpRawPath;
|
|
547
|
+
this.navigateToFile(tmpFilePath);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
/**
|
|
552
|
+
* Save RetoldRemote settings to localStorage.
|
|
553
|
+
*/
|
|
554
|
+
saveSettings()
|
|
555
|
+
{
|
|
556
|
+
try
|
|
557
|
+
{
|
|
558
|
+
let tmpRemote = this.pict.AppData.RetoldRemote;
|
|
559
|
+
let tmpSettings =
|
|
560
|
+
{
|
|
561
|
+
Theme: tmpRemote.Theme,
|
|
562
|
+
ViewMode: tmpRemote.ViewMode,
|
|
563
|
+
ThumbnailSize: tmpRemote.ThumbnailSize,
|
|
564
|
+
GalleryFilter: tmpRemote.GalleryFilter,
|
|
565
|
+
ShowHiddenFiles: tmpRemote.ShowHiddenFiles,
|
|
566
|
+
DistractionFreeShowNav: tmpRemote.DistractionFreeShowNav,
|
|
567
|
+
ImageFitMode: tmpRemote.ImageFitMode,
|
|
568
|
+
SidebarCollapsed: tmpRemote.SidebarCollapsed,
|
|
569
|
+
SidebarWidth: tmpRemote.SidebarWidth,
|
|
570
|
+
SortField: tmpRemote.SortField,
|
|
571
|
+
SortDirection: tmpRemote.SortDirection,
|
|
572
|
+
FilterPresets: tmpRemote.FilterPresets,
|
|
573
|
+
FilterPanelOpen: tmpRemote.FilterPanelOpen
|
|
574
|
+
};
|
|
575
|
+
localStorage.setItem('retold-remote-settings', JSON.stringify(tmpSettings));
|
|
576
|
+
}
|
|
577
|
+
catch (pError)
|
|
578
|
+
{
|
|
579
|
+
// localStorage may not be available
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
/**
|
|
584
|
+
* Load RetoldRemote settings from localStorage.
|
|
585
|
+
*/
|
|
586
|
+
_loadRemoteSettings()
|
|
587
|
+
{
|
|
588
|
+
try
|
|
589
|
+
{
|
|
590
|
+
let tmpStored = localStorage.getItem('retold-remote-settings');
|
|
591
|
+
if (tmpStored)
|
|
592
|
+
{
|
|
593
|
+
let tmpSettings = JSON.parse(tmpStored);
|
|
594
|
+
let tmpRemote = this.pict.AppData.RetoldRemote;
|
|
595
|
+
|
|
596
|
+
if (tmpSettings.Theme) tmpRemote.Theme = tmpSettings.Theme;
|
|
597
|
+
if (tmpSettings.ViewMode) tmpRemote.ViewMode = tmpSettings.ViewMode;
|
|
598
|
+
if (tmpSettings.ThumbnailSize) tmpRemote.ThumbnailSize = tmpSettings.ThumbnailSize;
|
|
599
|
+
if (tmpSettings.GalleryFilter)
|
|
600
|
+
{
|
|
601
|
+
tmpRemote.GalleryFilter = tmpSettings.GalleryFilter;
|
|
602
|
+
tmpRemote.FilterState.MediaType = tmpSettings.GalleryFilter;
|
|
603
|
+
}
|
|
604
|
+
if (typeof (tmpSettings.ShowHiddenFiles) === 'boolean') tmpRemote.ShowHiddenFiles = tmpSettings.ShowHiddenFiles;
|
|
605
|
+
if (typeof (tmpSettings.DistractionFreeShowNav) === 'boolean') tmpRemote.DistractionFreeShowNav = tmpSettings.DistractionFreeShowNav;
|
|
606
|
+
if (tmpSettings.ImageFitMode) tmpRemote.ImageFitMode = tmpSettings.ImageFitMode;
|
|
607
|
+
if (typeof (tmpSettings.SidebarCollapsed) === 'boolean') tmpRemote.SidebarCollapsed = tmpSettings.SidebarCollapsed;
|
|
608
|
+
if (tmpSettings.SidebarWidth) tmpRemote.SidebarWidth = tmpSettings.SidebarWidth;
|
|
609
|
+
if (tmpSettings.SortField) tmpRemote.SortField = tmpSettings.SortField;
|
|
610
|
+
if (tmpSettings.SortDirection) tmpRemote.SortDirection = tmpSettings.SortDirection;
|
|
611
|
+
if (Array.isArray(tmpSettings.FilterPresets)) tmpRemote.FilterPresets = tmpSettings.FilterPresets;
|
|
612
|
+
if (typeof (tmpSettings.FilterPanelOpen) === 'boolean') tmpRemote.FilterPanelOpen = tmpSettings.FilterPanelOpen;
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
catch (pError)
|
|
616
|
+
{
|
|
617
|
+
// localStorage may not be available
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
module.exports = RetoldRemoteApplication;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Retold Remote -- Browser Bundle Entry
|
|
3
|
+
*
|
|
4
|
+
* Exports the RetoldRemote application class for browser consumption.
|
|
5
|
+
*/
|
|
6
|
+
module.exports =
|
|
7
|
+
{
|
|
8
|
+
RetoldRemoteApplication: require('./Pict-Application-RetoldRemote.js')
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
if (typeof (window) !== 'undefined')
|
|
12
|
+
{
|
|
13
|
+
window.RetoldRemoteApplication = module.exports.RetoldRemoteApplication;
|
|
14
|
+
}
|