@cloudron/pankow 3.1.8

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.
Files changed (62) hide show
  1. package/.gitlab-ci.yml +30 -0
  2. package/.jshintrc +8 -0
  3. package/LICENSE +21 -0
  4. package/README.md +20 -0
  5. package/components/BottomBar.vue +22 -0
  6. package/components/Breadcrumb.vue +64 -0
  7. package/components/Button.vue +243 -0
  8. package/components/ButtonGroup.vue +37 -0
  9. package/components/Checkbox.vue +112 -0
  10. package/components/Dialog.vue +178 -0
  11. package/components/DirectoryView.vue +772 -0
  12. package/components/DirectoryViewListItem.vue +412 -0
  13. package/components/EmailInput.vue +22 -0
  14. package/components/FileUploader.vue +204 -0
  15. package/components/FormGroup.vue +26 -0
  16. package/components/Icon.vue +12 -0
  17. package/components/InputDialog.vue +170 -0
  18. package/components/InputGroup.vue +32 -0
  19. package/components/MainLayout.vue +63 -0
  20. package/components/Menu.vue +284 -0
  21. package/components/MenuItem.vue +106 -0
  22. package/components/MenuItemLink.vue +52 -0
  23. package/components/MultiSelect.vue +202 -0
  24. package/components/Notification.vue +163 -0
  25. package/components/NumberInput.vue +31 -0
  26. package/components/OfflineBanner.vue +47 -0
  27. package/components/PasswordInput.vue +86 -0
  28. package/components/Popover.vue +185 -0
  29. package/components/ProgressBar.vue +75 -0
  30. package/components/Radiobutton.vue +128 -0
  31. package/components/SideBar.vue +104 -0
  32. package/components/SingleSelect.vue +190 -0
  33. package/components/Spinner.vue +67 -0
  34. package/components/Switch.vue +94 -0
  35. package/components/TabView.vue +161 -0
  36. package/components/TableView.vue +187 -0
  37. package/components/TagInput.vue +104 -0
  38. package/components/TextInput.vue +58 -0
  39. package/components/TopBar.vue +88 -0
  40. package/fallbackImage.js +29 -0
  41. package/fetcher.js +81 -0
  42. package/gallery/CustomMenuItem.vue +40 -0
  43. package/gallery/DirectoryViewDemo.vue +73 -0
  44. package/gallery/Index.vue +790 -0
  45. package/gallery/folder.svg +151 -0
  46. package/gallery/index.html +25 -0
  47. package/gallery/index.js +10 -0
  48. package/gallery/logo.png +0 -0
  49. package/gallery/vite.config.mjs +9 -0
  50. package/gestures.js +60 -0
  51. package/index.js +86 -0
  52. package/logo.png +0 -0
  53. package/logo.svg +78 -0
  54. package/package.json +26 -0
  55. package/style.css +351 -0
  56. package/tooltip.js +83 -0
  57. package/utils.js +383 -0
  58. package/viewers/GenericViewer.vue +84 -0
  59. package/viewers/ImageViewer.vue +239 -0
  60. package/viewers/PdfViewer.vue +82 -0
  61. package/viewers/TextViewer.vue +221 -0
  62. package/viewers.js +11 -0
package/utils.js ADDED
@@ -0,0 +1,383 @@
1
+
2
+ import { filesize } from 'filesize';
3
+ import { customRef } from 'vue';
4
+ import moment from 'moment';
5
+
6
+ // https://vuejs.org/api/reactivity-advanced.html#customref
7
+ function useDebouncedRef(value, delay = 300) {
8
+ let timeout
9
+ return customRef((track, trigger) => {
10
+ return {
11
+ get() {
12
+ track()
13
+ return value
14
+ },
15
+ set(newValue) {
16
+ clearTimeout(timeout)
17
+ timeout = setTimeout(() => {
18
+ value = newValue
19
+ trigger()
20
+ }, delay)
21
+ }
22
+ }
23
+ })
24
+ }
25
+
26
+ function getFileTypeGroup(item) {
27
+ if (typeof item.mimeType !== 'string') throw 'item must have mimeType string property';
28
+ return item.mimeType.split('/')[0];
29
+ }
30
+
31
+ function isValidDomainOrURL(domain) {
32
+ try {
33
+ domain = new URL(input).hostname;
34
+ } catch (e) {}
35
+
36
+ return isValidDomain(domain);
37
+ }
38
+
39
+ function isValidDomain(domain) {
40
+ const domainRegex = /^([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}$/;
41
+ return domainRegex.test(domain);
42
+ }
43
+
44
+ // this currently does not match: "john.doe"@example.com, user@[192.168.1.1], john.doe(comment)@example.com or 用户@例子.世界
45
+ function isValidEmail(email) {
46
+ if (!email || typeof email !== 'string') return false;
47
+
48
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
49
+ return emailRegex.test(email);
50
+ }
51
+
52
+
53
+ // https://en.wikipedia.org/wiki/Binary_prefix
54
+ // binary units (IEC) 1024 based
55
+ function prettyBinarySize(size, fallback) {
56
+ if (!size) return fallback || 0;
57
+ if (size === -1) return 'Unlimited';
58
+
59
+ // we can also use KB here (JEDEC)
60
+ var i = Math.floor(Math.log(size) / Math.log(1024));
61
+ return (size / Math.pow(1024, i)).toFixed(3) * 1 + ' ' + ['B', 'KiB', 'MiB', 'GiB', 'TiB'][i];
62
+ }
63
+
64
+ // decimal units (SI) 1000 based
65
+ function prettyDecimalSize(size, fallback) {
66
+ if (!size) return fallback || 0;
67
+
68
+ var i = Math.floor(Math.log(size) / Math.log(1000));
69
+ return (size / Math.pow(1000, i)).toFixed(2) * 1 + ' ' + ['B', 'kB', 'MB', 'GB', 'TB'][i];
70
+ }
71
+
72
+ // this will print a human friendly datetime offset from now
73
+ function prettyDate(value) {
74
+ if (!value) return 'never';
75
+ return moment(value).fromNow();
76
+ }
77
+
78
+ function prettyLongDate(value) {
79
+ if (!value) return 'unknown';
80
+
81
+ const date = new Date(value);
82
+ return date.toLocaleString();
83
+ }
84
+
85
+ function prettyFileSize(value) {
86
+ if (typeof value !== 'number') return 'unknown';
87
+
88
+ return filesize(value);
89
+ }
90
+
91
+ function prettyEmailAddresses(addresses) {
92
+ if (!addresses) return '';
93
+ if (addresses === '<>') return '<>';
94
+ if (Array.isArray(addresses)) return addresses.map(function (a) { return a.slice(1, -1); }).join(', ');
95
+ return addresses.slice(1, -1);
96
+ }
97
+
98
+ function sanitize(path) {
99
+ path = '/' + path;
100
+ return path.replace(/\/+/g, '/');
101
+ }
102
+
103
+ function pathJoin() {
104
+ return sanitize(Array.from(arguments).join('/'));
105
+ }
106
+
107
+ function download(entries, name) {
108
+ if (!entries.length) return;
109
+
110
+ if (entries.length === 1) {
111
+ if (entries[0].share) window.location.href = '/api/v1/shares/' + entries[0].share.id + '?type=download&path=' + encodeURIComponent(entries[0].filePath);
112
+ else window.location.href = '/api/v1/files?type=download&path=' + encodeURIComponent(entries[0].filePath);
113
+
114
+ return;
115
+ }
116
+
117
+ const params = new URLSearchParams();
118
+
119
+ // be a bit smart about the archive name and folder tree
120
+ const folderPath = entries[0].filePath.slice(0, -entries[0].fileName.length);
121
+ const archiveName = name || folderPath.slice(folderPath.slice(0, -1).lastIndexOf('/')+1).slice(0, -1);
122
+ params.append('name', archiveName);
123
+ params.append('skipPath', folderPath);
124
+
125
+ params.append('entries', JSON.stringify(entries.map(function (entry) {
126
+ return {
127
+ filePath: entry.filePath,
128
+ shareId: entry.share ? entry.share.id : undefined
129
+ };
130
+ })));
131
+
132
+ window.location.href = '/api/v1/download?' + params.toString();
133
+ }
134
+
135
+ // simple extension detection, does not work with double extension like .tar.gz
136
+ function getExtension(entry) {
137
+ if (entry.isFile) return entry.fileName.slice(entry.fileName.lastIndexOf('.') + 1);
138
+ return '';
139
+ }
140
+
141
+ function copyToClipboard(value) {
142
+ var elem = document.createElement('input');
143
+ elem.value = value;
144
+ document.body.append(elem);
145
+ elem.select();
146
+ document.execCommand('copy');
147
+ elem.remove();
148
+ }
149
+
150
+ function urlSearchQuery() {
151
+ return decodeURIComponent(window.location.search).slice(1).split('&').map(function (item) { return item.split('='); }).reduce(function (o, k) { o[k[0]] = k[1]; return o; }, {});
152
+ }
153
+
154
+ // those paths contain the internal type and path reference eg. shares/:shareId/folder/filename or files/folder/filename
155
+ function parseResourcePath(resourcePath) {
156
+ var result = {
157
+ type: '',
158
+ path: '',
159
+ shareId: '',
160
+ apiPath: '',
161
+ resourcePath: ''
162
+ };
163
+
164
+ if (resourcePath.indexOf('files/') === 0) {
165
+ result.type = 'files';
166
+ result.path = resourcePath.slice('files'.length) || '/';
167
+ result.apiPath = '/api/v1/files';
168
+ result.resourcePath = result.type + result.path;
169
+ } else if (resourcePath.indexOf('shares/') === 0) {
170
+ result.type = 'shares';
171
+ result.shareId = resourcePath.split('/')[1];
172
+ result.path = resourcePath.slice((result.type + '/' + result.shareId).length) || '/';
173
+ result.apiPath = '/api/v1/shares/' + result.shareId;
174
+ // without shareId we show the root (share listing)
175
+ result.resourcePath = result.type + '/' + (result.shareId ? (result.shareId + result.path) : '');
176
+ } else {
177
+ console.error('Unknown resource path', resourcePath);
178
+ }
179
+
180
+ return result;
181
+ }
182
+
183
+ function getEntryIdentifier(entry) {
184
+ return (entry.share ? (entry.share.id + '/') : '') + entry.filePath;
185
+ }
186
+
187
+ function entryListSort(list, prop, desc) {
188
+ var tmp = list.sort(function (a, b) {
189
+ var av = a[prop];
190
+ var bv = b[prop];
191
+
192
+ if (typeof av === 'string') return (av.toUpperCase() < bv.toUpperCase()) ? -1 : 1;
193
+ else return (av < bv) ? -1 : 1;
194
+ });
195
+
196
+ if (desc) return tmp;
197
+ return tmp.reverse();
198
+ }
199
+
200
+ function sleep(ms) {
201
+ return new Promise(resolve => setTimeout(resolve, ms));
202
+ }
203
+
204
+ // this is from the Cloudron dashboard translation project en.json
205
+ const fallbackTranslations = {
206
+ "main": {
207
+ "dialog": {
208
+ "close": "Close"
209
+ }
210
+ },
211
+ "filemanager": {
212
+ "title": "File Manager",
213
+ "removeDialog": {
214
+ "reallyDelete": "Really delete the following?"
215
+ },
216
+ "newDirectoryDialog": {
217
+ "title": "New Folder",
218
+ "create": "Create"
219
+ },
220
+ "newFileDialog": {
221
+ "title": "New File",
222
+ "create": "Create"
223
+ },
224
+ "renameDialog": {
225
+ "title": "Rename {{ fileName }}",
226
+ "newName": "New Name",
227
+ "rename": "Rename"
228
+ },
229
+ "chownDialog": {
230
+ "title": "Change ownership",
231
+ "newOwner": "New Owner",
232
+ "change": "Change Owner",
233
+ "recursiveCheckbox": "Change ownership recursively"
234
+ },
235
+ "uploadingDialog": {
236
+ "title": "Uploading files ({{ countDone }}/{{ count }})",
237
+ "errorAlreadyExists": "One or more files already exist.",
238
+ "errorFailed": "Failed to upload one or more files. Please try again.",
239
+ "closeWarning": "Do not refresh the page until upload has finished.",
240
+ "retry": "Retry",
241
+ "overwrite": "Overwrite"
242
+ },
243
+ "extractDialog": {
244
+ "title": "Extracting {{ fileName }}",
245
+ "closeWarning": "Do not refresh the page until extract has finished."
246
+ },
247
+ "textEditorCloseDialog": {
248
+ "title": "File has unsaved changes",
249
+ "details": "Your changes will be lost if you don't save them",
250
+ "dontSave": "Don't Save"
251
+ },
252
+ "notFound": "Not found",
253
+ "toolbar": {
254
+ "new": "New",
255
+ "upload": "Upload",
256
+ "newFile": "New File",
257
+ "newFolder": "New Folder",
258
+ "uploadFolder": "Upload Folder",
259
+ "uploadFile": "Upload File",
260
+ "restartApp": "Restart App",
261
+ "openTerminal": "Open Terminal",
262
+ "openLogs": "Open Logs"
263
+ },
264
+ "list": {
265
+ "name": "Name",
266
+ "size": "Size",
267
+ "owner": "Owner",
268
+ "empty": "No files",
269
+ "symlink": "symlink to {{ target }}",
270
+ "menu": {
271
+ "rename": "Rename",
272
+ "chown": "Change Ownership",
273
+ "extract": "Extract Here",
274
+ "download": "Download",
275
+ "delete": "Delete",
276
+ "edit": "Edit",
277
+ "cut": "Cut",
278
+ "copy": "Copy",
279
+ "paste": "Paste",
280
+ "selectAll": "Select All",
281
+ "share": "Share",
282
+ "open": "Open"
283
+ },
284
+ "mtime": "Modified"
285
+ },
286
+ "extract": {
287
+ "error": "Failed to extract: {{ message }}"
288
+ },
289
+ "newDirectory": {
290
+ "errorAlreadyExists": "Already exists"
291
+ },
292
+ "newFile": {
293
+ "errorAlreadyExists": "Already exists"
294
+ },
295
+ "status": {
296
+ "restartingApp": "restarting app"
297
+ },
298
+ "uploader": {
299
+ "uploading": "Uploading",
300
+ "exitWarning": "Upload still in progress. Really close this page?"
301
+ },
302
+ "textEditor": {
303
+ "undo": "Undo",
304
+ "redo": "Redo",
305
+ "save": "Save"
306
+ },
307
+ "extractionInProgress": "Extraction in progress",
308
+ "pasteInProgress": "Pasting in progress",
309
+ "deleteInProgress": "Deletion in progress"
310
+ }
311
+ };
312
+
313
+ function translation(id) {
314
+ let value;
315
+ try {
316
+ value = id.split('.').reduce((a, b) => a[b], fallbackTranslations);
317
+ } catch (e) {
318
+ console.warn(`Translation ${id} does not exist`);
319
+ value = id;
320
+ }
321
+
322
+ return value;
323
+ }
324
+
325
+ function uuidv4() {
326
+ return '10000000-1000-4000-8000-100000000000'.replace(/[018]/g, c =>
327
+ (c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16)
328
+ );
329
+ }
330
+
331
+ // named exports
332
+ export {
333
+ getFileTypeGroup,
334
+ translation,
335
+ useDebouncedRef,
336
+ isValidDomain,
337
+ isValidDomainOrURL,
338
+ isValidEmail,
339
+ prettyBinarySize,
340
+ prettyDecimalSize,
341
+ prettyDate,
342
+ prettyLongDate,
343
+ prettyFileSize,
344
+ prettyEmailAddresses,
345
+ sanitize,
346
+ pathJoin,
347
+ download,
348
+ getExtension,
349
+ copyToClipboard,
350
+ urlSearchQuery,
351
+ parseResourcePath,
352
+ getEntryIdentifier,
353
+ entryListSort,
354
+ sleep,
355
+ uuidv4
356
+ };
357
+
358
+ // default export
359
+ export default {
360
+ getFileTypeGroup,
361
+ translation,
362
+ useDebouncedRef,
363
+ isValidDomain,
364
+ isValidDomainOrURL,
365
+ isValidEmail,
366
+ prettyBinarySize,
367
+ prettyDecimalSize,
368
+ prettyDate,
369
+ prettyLongDate,
370
+ prettyFileSize,
371
+ prettyEmailAddresses,
372
+ sanitize,
373
+ pathJoin,
374
+ download,
375
+ getExtension,
376
+ copyToClipboard,
377
+ urlSearchQuery,
378
+ parseResourcePath,
379
+ getEntryIdentifier,
380
+ entryListSort,
381
+ sleep,
382
+ uuidv4
383
+ };
@@ -0,0 +1,84 @@
1
+ <template>
2
+ <MainLayout :gap="false">
3
+ <template #header>
4
+ <TopBar class="navbar" :gap="false">
5
+ <template #left>
6
+ <div>{{ entry.fileName }}</div>
7
+ </template>
8
+ <template #right>
9
+ <Button icon="fa-solid fa-xmark" @click="onClose">{{ tr('main.dialog.close') }}</Button>
10
+ </template>
11
+ </TopBar>
12
+ </template>
13
+ <template #body>
14
+ <div class="content">
15
+ <div>
16
+ <img class="preview-icon" :src="entry.previewUrl" />
17
+ <h1>{{ entry.fileName }}</h1>
18
+ <Button icon="fa-solid fa-download" :href="entry.downloadFileUrl">Download</Button>
19
+ </div>
20
+ </div>
21
+ </template>
22
+ </MainLayout>
23
+ </template>
24
+
25
+ <script>
26
+
27
+ import Button from '../components/Button.vue';
28
+ import MainLayout from '../components/MainLayout.vue';
29
+ import TopBar from '../components/TopBar.vue';
30
+ import utils from '../utils.js';
31
+
32
+ export default {
33
+ name: 'GenericViewer',
34
+ components: {
35
+ Button,
36
+ TopBar,
37
+ MainLayout
38
+ },
39
+ props: {
40
+ tr: {
41
+ type: Function,
42
+ default(id) { console.warn('Missing tr for GenericViewer'); return utils.translation(id); }
43
+ }
44
+ },
45
+ emits: [ 'close' ],
46
+ data() {
47
+ return {
48
+ entry: {}
49
+ };
50
+ },
51
+ methods: {
52
+ async open(entry) {
53
+ if (!entry) return;
54
+
55
+ this.entry = entry;
56
+ },
57
+ onClose() {
58
+ this.$emit('close');
59
+ }
60
+ },
61
+ mounted() {
62
+ }
63
+ };
64
+
65
+ </script>
66
+
67
+ <style scoped>
68
+
69
+ .preview-icon {
70
+ width: 128px;
71
+ height: 128px;
72
+ }
73
+
74
+ .content {
75
+ text-align: center;
76
+ background-color: var(--pankow-color-background);
77
+ width: 100%;
78
+ min-height: 100%;
79
+ display: flex;
80
+ align-items: center;
81
+ justify-content: center;
82
+ }
83
+
84
+ </style>
@@ -0,0 +1,239 @@
1
+
2
+ <template>
3
+ <div @mousemove="onMouseMove" class="full-page" @keyup.esc.stop="onClose" @keyup.left.stop="onPrev" @keyup.right.stop="onNext" tabindex="1" :style="{ opacity: dismissDistance ? (200 - dismissDistance)/100 : 1 }">
4
+ <div class="image-layer" ref="imageContainer" @scroll="onScroll($event)">
5
+ <div class="image" v-for="(entry, index) in entries" :key="entry.id" v-bind:id="`image${index}`" @touchstart="onTouchStart" @touchmove="onTouchMove" @touchend="onTouchEnd" :style="{ marginTop: dismissDistance ? (dismissDistance + 'px') : 0 }">
6
+ <img :src="entry.fullFileUrl" v-if="getFileTypeGroup(entry) === 'image'" draggable="false"/>
7
+ <video v-if="getFileTypeGroup(entry) !== 'image'" ref="videoElements" controls>
8
+ <source :src="entry.fullFileUrl" />
9
+ </video>
10
+ </div>
11
+ </div>
12
+ <Button icon="fa-solid fa-download" v-show="showOverlay && hasDownloadHandler" secondary large plain tool @click="onDownload" class="download-button"/>
13
+ <Button icon="fa-solid fa-xmark" v-show="showOverlay" secondary large plain tool @click="onClose" class="close-button"/>
14
+ <Button icon="fa-solid fa-chevron-left" secondary large plain tool @click="onPrev" v-show="showOverlay && curIndex > 0" class="prev-button"/>
15
+ <Button icon="fa-solid fa-chevron-right" secondary large plain tool @click="onNext" v-show="showOverlay && curIndex < (entries.length-1)" class="next-button"/>
16
+ <video style="display: none" ref="typeTestVideo"></video>
17
+ </div>
18
+ </template>
19
+
20
+ <script>
21
+
22
+ import { getFileTypeGroup } from '../utils.js';
23
+ import Button from '../components/Button.vue';
24
+
25
+ function defaultDownloadHandler() {
26
+ console.warn('Missing downloadHandler for ImageViewer');
27
+ }
28
+
29
+ let touchDist = 0;
30
+ let startY = 0;
31
+ const dismissDistanceThreshold = 150;
32
+ const dismissTriggerThreshold = 50;
33
+
34
+ export default {
35
+ name: 'ImageViewer',
36
+ emits: [ 'close', 'saved' ],
37
+ props: {
38
+ downloadHandler: {
39
+ type: Function,
40
+ default: defaultDownloadHandler
41
+ },
42
+ navigationHandler: {
43
+ type: Function,
44
+ default: (toEntry) => {
45
+ this.$router.replace(toEntry.resourcePath);
46
+ }
47
+ }
48
+ },
49
+ components: {
50
+ Button
51
+ },
52
+ data() {
53
+ return {
54
+ showOverlay: false,
55
+ showOverlayTimer: null,
56
+ lockScroll: false,
57
+ curIndex: -1,
58
+ lastMousePosition: {},
59
+ entry: {},
60
+ entries: [],
61
+ dismissDistance: 0
62
+ };
63
+ },
64
+ computed: {
65
+ hasDownloadHandler() { return this.downloadHandler !== defaultDownloadHandler }
66
+ },
67
+ methods: {
68
+ getFileTypeGroup,
69
+ keepOverlayAlive() {
70
+ this.showOverlay = true;
71
+ clearTimeout(this.showOverlayTimer);
72
+ this.showOverlayTimer = setTimeout(() => {
73
+ // avoid hiding if cursor is above a button
74
+ // this crashes sometimes, not yet sure how to better prevent for now "Document.elementFromPoint: Argument 1 is not a finite floating-point value."
75
+ try {
76
+ if (document.elementFromPoint(this.lastMousePosition.x || 0, this.lastMousePosition.y || 0) !== this.$refs.imageContainer) return;
77
+ } catch (e) {
78
+ console.error('ImageViewer crash. lastMousePosition', this.lastMousePosition, e);
79
+ }
80
+
81
+ this.showOverlay = false;
82
+ }, 2000);
83
+ },
84
+ onMouseMove() {
85
+ this.lastMousePosition = { x: event.pageX, y: event.pageY };
86
+ this.keepOverlayAlive();
87
+ },
88
+ canHandle(entry) {
89
+ return (getFileTypeGroup(entry) === 'image' || this.$refs.typeTestVideo.canPlayType(entry.mimeType))
90
+ && !(entry.mimeType === 'image/heif') // supported on apple it seems
91
+ && !(entry.mimeType === 'image/vnd.adobe.photoshop');
92
+ },
93
+ onScroll() {
94
+ if (this.lockScroll) return;
95
+
96
+ for (let i = 1; i < this.entries.length; i++) {
97
+ const elem = document.getElementById('image'+i);
98
+ const rect = elem.getBoundingClientRect();
99
+ const midX = (rect.x+(rect.width/2));
100
+ if (midX >= (rect.width/4) && midX <= 3*(rect.width/4)) {
101
+ this.entry = this.entries[i];
102
+
103
+ if (this.curIndex !== i) {
104
+ this.curIndex = i;
105
+ this.navigationHandler(this.entry);
106
+ }
107
+
108
+ break;
109
+ }
110
+ }
111
+ },
112
+ onTouchStart(event) {
113
+ this.dismissDistance = 0;
114
+ touchDist = 0;
115
+ startY = event.changedTouches[0].pageY;
116
+ },
117
+ onTouchMove(event) {
118
+ touchDist = event.changedTouches[0].pageY - startY // get total dist traveled by finger while in contact with surface
119
+ if (touchDist > dismissTriggerThreshold) this.dismissDistance = touchDist - dismissTriggerThreshold;
120
+ },
121
+ onTouchEnd() {
122
+ if (touchDist > dismissDistanceThreshold) this.onClose();
123
+ touchDist = 0;
124
+ this.dismissDistance = 0;
125
+ },
126
+ open(entry, entries, instant = true) {
127
+ if (!entry || entry.isDirectory || !this.canHandle(entry)) return;
128
+
129
+ this.lockScroll = true;
130
+ this.keepOverlayAlive();
131
+ this.entry = entry;
132
+ this.entries = entries;
133
+ this.curIndex = entries.findIndex((item) => item.fileName === entry.fileName);
134
+
135
+ setTimeout(() => {
136
+ document.getElementById(`image${this.curIndex}`).scrollIntoView({ behavior: instant ? 'instant' : 'smooth' });
137
+ this.$el.focus();
138
+ }, 0);
139
+
140
+ setTimeout(() => {
141
+ this.lockScroll = false;
142
+ }, 500);
143
+ },
144
+ onDownload() {
145
+ if (!this.hasDownloadHandler) return;
146
+ this.downloadHandler(this.entry);
147
+ },
148
+ onClose() {
149
+ this.$emit('close');
150
+
151
+ // pause/stop video and audio playback on close
152
+ if (this.$refs.videoElements && this.$refs.videoElements.length) this.$refs.videoElements.forEach(e => e.pause());
153
+
154
+ // given some time for closing to keep DOM elements alive
155
+ setTimeout(() => {
156
+ this.entry = {};
157
+ }, 500);
158
+ },
159
+ onPrev() {
160
+ if (this.curIndex < 1) return;
161
+ this.navigationHandler(this.entries[this.curIndex-1]);
162
+ this.open(this.entries[this.curIndex-1], this.entries, false);
163
+ },
164
+ onNext() {
165
+ if (this.curIndex >= this.entries.length) return;
166
+ this.navigationHandler(this.entries[this.curIndex+1]);
167
+ this.open(this.entries[this.curIndex+1], this.entries, false);
168
+ }
169
+ },
170
+ mounted() {}
171
+ };
172
+
173
+ </script>
174
+
175
+ <style scoped>
176
+
177
+ .full-page {
178
+ position: absolute;
179
+ top: 0;
180
+ left: 0;
181
+ width: 100%;
182
+ height: 100%;
183
+ background-color: black;
184
+ display: flex;
185
+ flex-direction: column;
186
+ }
187
+
188
+ .download-button {
189
+ position: absolute;
190
+ bottom: 10px;
191
+ right: 10px;
192
+ }
193
+
194
+ .close-button {
195
+ position: absolute;
196
+ top: 10px;
197
+ right: 10px;
198
+ }
199
+
200
+ .prev-button {
201
+ position: absolute;
202
+ top: 50%;
203
+ left: 10px;
204
+ }
205
+
206
+ .next-button {
207
+ position: absolute;
208
+ top: 50%;
209
+ right: 10px;
210
+ }
211
+
212
+ .image-layer {
213
+ display: flex;
214
+ position: fixed;
215
+ height: 100%;
216
+ width: 100%;
217
+ overflow-y: hidden;
218
+ overflow-x: auto;
219
+ scroll-snap-type: x mandatory;
220
+ }
221
+
222
+ .image {
223
+ flex: 0 0 auto;
224
+ width: 100%;
225
+ height: 100%;
226
+ scroll-snap-align: center;
227
+ scroll-snap-stop: always;
228
+ display: inline-block;
229
+ user-select: none;
230
+ }
231
+
232
+ img, video {
233
+ object-fit: contain;
234
+ width: 100%;
235
+ height: 100%;
236
+ user-select: none;
237
+ }
238
+
239
+ </style>