@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
@@ -0,0 +1,412 @@
1
+ <template>
2
+ <div class="row-wrapper"
3
+ :draggable="!rename"
4
+ @mouseup="onSelect($event)"
5
+ @drop="onDrop($event)"
6
+ @dragover="onDragOver($event)"
7
+ @dragexit="onDragExit($event)"
8
+ >
9
+
10
+ <div class="row" :class="{ 'focused': item.focused, 'selected': item.selected, 'drop-target-active': dropTargetActive }">
11
+ <div v-if="!visible" class="col icon"></div>
12
+ <div v-if="!visible" class="col label"></div>
13
+
14
+ <div v-if="visible" class="col icon">
15
+ <img :src="item.previewUrl || item.icon" ref="iconImage" @error="iconError($event)"/>
16
+ <i v-show="typeof showShare === 'string' && item[showShare]" class="fa-solid fa-share-nodes is-shared"></i>
17
+ </div>
18
+
19
+ <div v-if="visible && !rename" class="col label">
20
+ <a v-show="item.href" class="open-action" @dblclick.stop @click="onOpen($event)" :href="item.href">{{ item.name }} {{ item.target ? `-> ${item.target}` : '' }}</a>
21
+ <span v-show="!item.href">{{ item.name }} {{ item.target ? `→ ${item.target}` : '' }}</span>
22
+ </div>
23
+ <div v-if="visible && rename" class="col label rename">
24
+ <TextInput
25
+ ref="renameInput"
26
+ v-model="newName"
27
+ :disabled="renameBusy"
28
+ @blur="onRenameEnd"
29
+ @dblclick.stop
30
+ @keydown.enter.stop="onRenameSubmit"
31
+ @keydown.esc.stop="onRenameEnd"
32
+ @keydown.stop
33
+ />
34
+ </div>
35
+
36
+ <div v-if="visible && showStar" class="col star">
37
+ <Icon :icon="`${item.star ? 'fa-solid' : 'fa-regular'} fa-star`" class="star-icon" :class="{ 'star-visible': item.star }" @dblclick.stop.prevent @click.stop.prevent="onToggleStar" />
38
+ </div>
39
+ <div v-if="visible && showOwner" class="col owner">{{ item.owner }}</div>
40
+ <div v-if="visible && showSize" class="col size">{{ prettyFileSize(item.size) }}</div>
41
+ <div v-if="visible && showModified" class="col modified" :title="item.modified.toLocaleString()">{{ prettyDate(item.modified) }}</div>
42
+ <div v-if="visible" class="col actions">
43
+ <div @mouseup.stop="onActionMenu($event)"><i class="fa-solid fa-ellipsis-vertical"/></div>
44
+ </div>
45
+ </div>
46
+ </div>
47
+ </template>
48
+
49
+ <script>
50
+
51
+ import { prettyDate, prettyFileSize } from '../utils.js';
52
+
53
+ import Icon from './Icon.vue';
54
+ import Menu from './Menu.vue';
55
+ import TextInput from './TextInput.vue';
56
+
57
+ export default {
58
+ name: 'DirectoryViewListItem',
59
+ emits: [ 'activated', 'action-menu' ],
60
+ components: {
61
+ Icon,
62
+ Menu,
63
+ TextInput
64
+ },
65
+ props: {
66
+ item: Object,
67
+ showOwner: {
68
+ type: Boolean,
69
+ default: false
70
+ },
71
+ showShare: {
72
+ // if String its the name of the property for existing share indicator
73
+ type: [ Boolean, String ],
74
+ default: false
75
+ },
76
+ showSize: {
77
+ type: Boolean,
78
+ default: false
79
+ },
80
+ showStar: {
81
+ type: Boolean,
82
+ default: false
83
+ },
84
+ showModified: {
85
+ type: Boolean,
86
+ default: false
87
+ },
88
+ fallbackIcon: String,
89
+ renameHandler: {
90
+ type: Function,
91
+ default() {
92
+ console.warn('Missing renameHandler for DirectoryViewItem');
93
+ }
94
+ },
95
+ starHandler: {
96
+ type: Function,
97
+ default() {
98
+ console.warn('Missing starHandler for DirectoryViewItem');
99
+ }
100
+ },
101
+ selectHandler: {
102
+ type: Function,
103
+ default() {
104
+ console.warn('Missing selectHandler for DirectoryViewItem');
105
+ }
106
+ },
107
+ dropHandler: {
108
+ type: Function,
109
+ default() {
110
+ console.warn('Missing dropHandler for DirectoryViewItem');
111
+ }
112
+ },
113
+ canDropHandler: {
114
+ type: Function,
115
+ default() {
116
+ console.warn('Missing canDropHandler for DirectoryViewItem');
117
+ }
118
+ }
119
+ },
120
+ data() {
121
+ return {
122
+ visible: false,
123
+ rename: false,
124
+ renameBusy: false,
125
+ newName: '',
126
+ dropTargetActive: false,
127
+ previewRetries: 0
128
+ };
129
+ },
130
+ methods: {
131
+ prettyFileSize,
132
+ prettyDate,
133
+ highlight() {
134
+ this.$el.classList.add('pankow-directory-view-highlight-animation');
135
+ setTimeout(() => { this.$el.classList.remove('pankow-directory-view-highlight-animation'); }, 4000);
136
+ },
137
+ onToggleStar() {
138
+ this.starHandler(this.item);
139
+ },
140
+ onActionMenu(event) {
141
+ this.onSelect(event, true);
142
+ this.$emit('action-menu', this.item, event);
143
+ },
144
+ onRenameBegin() {
145
+ this.rename = true;
146
+ this.newName = this.item.name;
147
+
148
+ // wait one eventloop for input to be there
149
+ setTimeout(() => {
150
+ const elem = this.$refs.renameInput.$el;
151
+ elem.focus();
152
+
153
+ // select name without extension
154
+ if (typeof elem.selectionStart !== 'undefined') {
155
+ elem.selectionStart = 0;
156
+ elem.selectionEnd = this.item.name.lastIndexOf('.');
157
+ }
158
+ }, 0);
159
+ },
160
+ onRenameEnd() {
161
+ this.rename = false;
162
+ this.renameBusy = false;
163
+ this.newName = '';
164
+ },
165
+ async onRenameSubmit() {
166
+ if (!this.newName) return;
167
+
168
+ this.renameBusy = true;
169
+ await this.renameHandler(this.item, this.newName);
170
+ this.onRenameEnd();
171
+ },
172
+ onOpen(event) {
173
+ // if we have a composed click use default handler to open new tab
174
+ if (event.ctrlKey || event.metaKey) return;
175
+
176
+ event.preventDefault();
177
+ event.stopPropagation();
178
+
179
+ this.$emit('activated', this.item);
180
+ },
181
+ onSelect(event, actionMenu = false) {
182
+ if ((event.button === 2 || actionMenu) && event.ctrlKey) {
183
+ this.selectHandler(this.item, event, true);
184
+ } else {
185
+ this.selectHandler(this.item, event);
186
+ }
187
+ },
188
+ onDrop(event) {
189
+ if (!this.canDropHandler(this.item)) return;
190
+
191
+ event.stopPropagation();
192
+
193
+ this.dropHandler(this.item, event);
194
+ this.dropTargetActive = false;
195
+ },
196
+ onDragOver(event) {
197
+ if (!this.canDropHandler(this.item)) return;
198
+
199
+ event.preventDefault();
200
+ event.stopPropagation();
201
+
202
+ event.dataTransfer.dropEffect = 'move';
203
+ this.dropTargetActive = true;
204
+ },
205
+ onDragExit(event) {
206
+ if (!this.item.isDirectory) return;
207
+
208
+ this.dropTargetActive = false;
209
+ },
210
+ iconError(event) {
211
+ event.target.src = this.fallbackIcon;
212
+
213
+ setTimeout(() => {
214
+ if (this.previewRetries > 5) return;
215
+ ++this.previewRetries
216
+
217
+ event.target.src = this.item.previewUrl || this.item.icon;
218
+ }, 1000);
219
+ }
220
+ },
221
+ mounted() {
222
+ const observer = new IntersectionObserver(result => {
223
+ if (result[0].isIntersecting) {
224
+ this.visible = true;
225
+ observer.unobserve(this.$el);
226
+ }
227
+ });
228
+
229
+ observer.observe(this.$el);
230
+ }
231
+ };
232
+
233
+ </script>
234
+
235
+ <style scoped>
236
+
237
+ .row-wrapper {
238
+ --background-color-hover: #ededed;
239
+ --background-color-selected: #dbedfb;
240
+ --border-color-focus: #b3cfe5;
241
+ }
242
+
243
+ @media (prefers-color-scheme: dark) {
244
+ .row-wrapper {
245
+ --background-color-hover: rgba(255, 255, 255, 0.1);
246
+ --background-color-selected: rgba(255, 255, 255, 0.2);
247
+ --border-color-focus: rgba(255, 255, 255, 0.3);
248
+ }
249
+ }
250
+
251
+ .row-wrapper {
252
+ width: 100%;
253
+ padding: 2px 0;
254
+ margin-bottom: 5px;
255
+ }
256
+
257
+ .row {
258
+ display: flex;
259
+ overflow: hidden;
260
+ width: 100%;
261
+ border-radius: 3px;
262
+ transition: all 100ms;
263
+ border: 2px solid transparent;
264
+ }
265
+
266
+ .row-wrapper:hover > .row {
267
+ background-color: var(--background-color-hover);
268
+ }
269
+
270
+ .row.drop-target-active {
271
+ border: 2px solid var(--pankow-color-primary);
272
+ }
273
+
274
+ .row.selected {
275
+ background-color: var(--background-color-selected) !important;
276
+ }
277
+
278
+ .row.focused {
279
+ border: solid 2px var(--border-color-focus);
280
+ }
281
+
282
+ .col {
283
+ white-space: nowrap;
284
+ }
285
+
286
+ .icon {
287
+ height: 45px;
288
+ width: 45px;
289
+ padding: 5px;
290
+ }
291
+
292
+ .icon > img {
293
+ width: 35px;
294
+ height: 35px;
295
+ object-fit: cover;
296
+ }
297
+
298
+ .star-icon {
299
+ color: #ffcb00;
300
+ padding: 10px;
301
+ visibility: hidden;
302
+ cursor: pointer;
303
+ }
304
+
305
+ .star-icon:hover {
306
+ transform: scale(1.5);
307
+ transform-origin: center center;
308
+ }
309
+
310
+ .row.focused .star-icon,
311
+ .row.selected .star-icon,
312
+ .row-wrapper:hover > .row .star-icon {
313
+ visibility: visible;
314
+ }
315
+
316
+ .star-visible {
317
+ visibility: visible !important;
318
+ }
319
+
320
+ .is-shared {
321
+ position: relative;
322
+ right: 13px;
323
+ bottom: 0;
324
+ font-size: 1.2rem
325
+ }
326
+
327
+ .label {
328
+ padding-left: 5px;
329
+ flex-grow: 1;
330
+ overflow: hidden;
331
+ text-overflow: ellipsis;
332
+ margin: auto;
333
+ display: flex;
334
+ }
335
+
336
+ .label > button {
337
+ display: none;
338
+ margin: auto 10px;
339
+ }
340
+
341
+ .label:hover > button {
342
+ display: block;
343
+ }
344
+
345
+ .label.rename > button {
346
+ display: block;
347
+ }
348
+
349
+ .size {
350
+ width: 100px;
351
+ margin: auto;
352
+ }
353
+
354
+ .star {
355
+ width: 80px;
356
+ margin: auto;
357
+ text-align: center;
358
+ }
359
+
360
+ .owner {
361
+ width: 100px;
362
+ margin: auto;
363
+ }
364
+
365
+ .modified {
366
+ width: 100px;
367
+ margin: auto;
368
+ }
369
+
370
+ .actions {
371
+ display: flex;
372
+ margin: auto;
373
+ margin-right: 10px;
374
+ }
375
+
376
+ .actions > div {
377
+ padding: 0 10px;
378
+ cursor: pointer;
379
+ }
380
+
381
+ .open-action {
382
+ cursor: pointer;
383
+ }
384
+
385
+ .open-action:hover {
386
+ text-decoration: underline;
387
+ }
388
+
389
+
390
+ </style>
391
+
392
+ <style>
393
+
394
+ .pankow-directory-view-highlight-animation {
395
+ animation: pankow-directory-view-highlight-animation-breath 0.8s 4;
396
+ }
397
+
398
+ @keyframes pankow-directory-view-highlight-animation-breath {
399
+ 0% {
400
+ opacity: 1;
401
+ }
402
+
403
+ 50% {
404
+ opacity: 0.5;
405
+ }
406
+
407
+ 100% {
408
+ opacity: 1;
409
+ }
410
+ }
411
+
412
+ </style>
@@ -0,0 +1,22 @@
1
+ <script setup>
2
+
3
+ const model = defineModel();
4
+ const props = defineProps({
5
+ placeholder: String,
6
+ readonly: {
7
+ type: Boolean,
8
+ default: false
9
+ },
10
+ });
11
+
12
+ </script>
13
+
14
+ <template>
15
+ <input class="pankow-text-input" type="email" :placeholder="placeholder" :readonly="readonly" :value="model" @input="$emit('update:modelValue', $event.target.value)"/>
16
+ </template>
17
+
18
+ <style>
19
+
20
+ /* using TextInput styles */
21
+
22
+ </style>
@@ -0,0 +1,204 @@
1
+ <template>
2
+ <div class="file-uploader" v-show="uploadInProgress">
3
+ <div class="label">{{ tr('filemanager.uploader.uploading') }} {{ activeFile ? activeFile.name : '' }} <span style="float: right">{{ prettyUploadSpeed }}</span></div>
4
+ <div style="display: flex;">
5
+ <ProgressBar :value="parseInt((this.uploadedSize * 100) / this.size) || 0" style="flex-grow: 1"></ProgressBar>
6
+ <Button danger small tool @click="onCancelUpload" icon="fa-solid fa-xmark"></Button>
7
+ </div>
8
+
9
+ <input type="file" ref="uploadFileInput" style="display: none" multiple/>
10
+ <input type="file" ref="uploadFolderInput" style="display: none" multiple webkitdirectory directory/>
11
+
12
+ <InputDialog ref="inputDialog" />
13
+ </div>
14
+ </template>
15
+
16
+ <script>
17
+
18
+ import ProgressBar from './ProgressBar.vue';
19
+ import Button from './Button.vue';
20
+ import Icon from './Icon.vue';
21
+ import InputDialog from './InputDialog.vue';
22
+
23
+ import { translation } from '../utils.js';
24
+
25
+ export default {
26
+ name: 'FileUploader',
27
+ components: {
28
+ ProgressBar,
29
+ Button,
30
+ Icon,
31
+ InputDialog
32
+ },
33
+ emits: [ 'finished' ],
34
+ props: {
35
+ uploadHandler: {
36
+ type: Function,
37
+ default() { console.warn('Missing uploadHandler for FileUploader'); }
38
+ },
39
+ cancelHandler: {
40
+ type: Function,
41
+ default() { console.warn('Missing cancelHandler for FileUploader'); }
42
+ },
43
+ jobPreFlightCheckHandler: {
44
+ type: Function,
45
+ default(job) { console.warn('Missing jobPreFlightCheckHandler for FileUploader'); return true; }
46
+ },
47
+ tr: {
48
+ type: Function,
49
+ default(id) { console.warn('Missing tr for FileUploader, using fallback.'); return translation(id); }
50
+ },
51
+ },
52
+ data() {
53
+ return {
54
+ activeTargetFolder: '/', // keep folder state for upload dialogs
55
+ uploadInProgress: false,
56
+ activeJob: null,
57
+ activeFile: {},
58
+ pendingJobs: [],
59
+ size: 0,
60
+ uploadedSize: 0,
61
+ uploadSpeed: 0
62
+ };
63
+ },
64
+ computed: {
65
+ prettyUploadSpeed() {
66
+ if (this.uploadSpeed < 1024) return this.uploadSpeed + ' Kb/s';
67
+ else return (this.uploadSpeed/1024).toFixed(2) + ' Mb/s';
68
+ }
69
+ },
70
+ methods: {
71
+ beforeUnloadListener(event) {
72
+ event.preventDefault();
73
+ return window.confirm(this.tr('filemanager.uploader.exitWarning'));
74
+ },
75
+ onUploadFile(targetFolder) {
76
+ this.activeTargetFolder = targetFolder;
77
+ this.$refs.uploadFileInput.click();
78
+ },
79
+ onUploadFolder(targetFolder) {
80
+ this.activeTargetFolder = targetFolder;
81
+ this.$refs.uploadFolderInput.click();
82
+ },
83
+ async onCancelUpload() {
84
+ const confirmed = await this.$refs.inputDialog.confirm({
85
+ message: `Really cancel upload?`,
86
+ confirmStyle: 'danger',
87
+ confirmLabel: 'Yes',
88
+ rejectLabel: 'No'
89
+ });
90
+
91
+ if (!confirmed) return;
92
+
93
+ this.cancelHandler();
94
+ this.uploadFinished();
95
+ },
96
+ uploadFinished() {
97
+ this.progess = 0;
98
+ this.size = 0;
99
+ this.uploadedSize = 0;
100
+ this.activeFile = {};
101
+ this.activeJob = null;
102
+ this.uploadInProgress = false;
103
+
104
+ window.removeEventListener('beforeunload', this.beforeUnloadListener, { capture: true });
105
+
106
+ this.$emit('finished');
107
+ },
108
+ async uploadNextJob() {
109
+ if (this.pendingJobs.length === 0) return this.uploadFinished();
110
+
111
+ this.activeJob = this.pendingJobs.pop();
112
+
113
+ if (!(await this.jobPreFlightCheckHandler(this.activeJob))) return this.uploadNextJob();
114
+
115
+ this.uploadNextFile();
116
+ },
117
+ async uploadNextFile() {
118
+ if (!this.activeJob || this.activeJob.pendingFiles.length === 0) return this.uploadNextJob();
119
+
120
+ this.activeFile = this.activeJob.pendingFiles.pop();
121
+
122
+ try {
123
+ let uploadedSoFar = 0;
124
+ let lastTimestamp = Date.now();
125
+
126
+ await this.uploadHandler(this.activeJob.targetFolder, this.activeFile, (event) => {
127
+ if (event.direction === 'upload') {
128
+ const timeDiff = Date.now() - lastTimestamp;
129
+ const loadedDiff = event.loaded - uploadedSoFar;
130
+
131
+ this.uploadedSize += loadedDiff;
132
+ this.uploadSpeed = parseInt(loadedDiff / (timeDiff / 1024) / 1024);
133
+
134
+ uploadedSoFar = event.loaded;
135
+ lastTimestamp = Date.now();
136
+ }
137
+ });
138
+ } catch (e) {
139
+ console.error(`Failed to upload ${this.activeFile.name}`, e);
140
+ }
141
+
142
+ this.uploadNextFile();
143
+ },
144
+ startUpload() {
145
+ if (this.uploadInProgress) return;
146
+ this.uploadInProgress = true;
147
+
148
+ window.addEventListener('beforeunload', this.beforeUnloadListener, { capture: true });
149
+
150
+ this.uploadNextJob();
151
+ },
152
+ addFiles(files, targetFolder) {
153
+ if (!files || !files.length) return;
154
+
155
+ let folder = files[0].webkitRelativePath ? files[0].webkitRelativePath : null;
156
+ if (folder && folder.indexOf('/') !== -1) folder = folder.split('/')[0];
157
+
158
+ const job = {
159
+ folder,
160
+ targetFolder,
161
+ pendingFiles: []
162
+ };
163
+
164
+ // update overall size and convert to real Array
165
+ for (let i = 0; i < files.length; ++i) {
166
+ this.size += files[i].size;
167
+ job.pendingFiles.push(files[i]);
168
+ }
169
+
170
+ this.pendingJobs.push(job);
171
+
172
+ this.startUpload();
173
+ }
174
+ },
175
+ mounted() {
176
+ this.$refs.uploadFileInput.addEventListener('change', (e) => {
177
+ this.addFiles(e.target.files || [], this.activeTargetFolder);
178
+ });
179
+
180
+ this.$refs.uploadFolderInput.addEventListener('change', (e) => {
181
+ this.addFiles(e.target.files || [], this.activeTargetFolder);
182
+ });
183
+ }
184
+ };
185
+
186
+ </script>
187
+
188
+ <style scoped>
189
+
190
+ .label {
191
+ padding-bottom: 5px;
192
+ }
193
+
194
+ </style>
195
+
196
+ <style>
197
+
198
+ .file-uploader {
199
+ width: 100%;
200
+ padding: 10px;
201
+ background-color: var(--pankow-color-background);
202
+ }
203
+
204
+ </style>
@@ -0,0 +1,26 @@
1
+ <script setup>
2
+
3
+ const props = defineProps({
4
+ hasError: {
5
+ type: Boolean,
6
+ default: false
7
+ },
8
+ });
9
+
10
+ </script>
11
+
12
+ <template>
13
+ <div class="pankow-form-group" :class="{ 'has-error': hasError }">
14
+ <slot></slot>
15
+ </div>
16
+ </template>
17
+
18
+ <style>
19
+
20
+ .pankow-form-group {
21
+ display: flex;
22
+ flex-direction: column;
23
+ align-content: stretch;
24
+ }
25
+
26
+ </style>
@@ -0,0 +1,12 @@
1
+ <script setup>
2
+
3
+ const props = defineProps({
4
+ icon: String
5
+ });
6
+
7
+ </script>
8
+
9
+ <template>
10
+ <i :class="icon"></i>
11
+ </template>
12
+