@cloudron/pankow 4.2.3 → 4.3.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.
@@ -1,10 +1,10 @@
1
1
 
2
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 }">
3
+ <div ref="rootEl" @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
4
  <div class="image-layer" ref="imageContainer" @scroll="onScroll($event)">
5
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
6
  <img :src="entry.fullFileUrl" v-if="getFileTypeGroup(entry) === 'image'" draggable="false"/>
7
- <video v-if="getFileTypeGroup(entry) !== 'image'" ref="videoElements" controls>
7
+ <video v-if="getFileTypeGroup(entry) !== 'image'" :ref="addVideoRef" controls>
8
8
  <source :src="entry.fullFileUrl" />
9
9
  </video>
10
10
  </div>
@@ -17,8 +17,10 @@
17
17
  </div>
18
18
  </template>
19
19
 
20
- <script>
20
+ <script setup>
21
21
 
22
+ import { ref, computed, onBeforeUpdate, onMounted, useTemplateRef } from 'vue';
23
+ import { useRouter } from 'vue-router';
22
24
  import { getFileTypeGroup } from '../utils.js';
23
25
  import Button from '../components/Button.vue';
24
26
 
@@ -27,144 +29,151 @@ let startY = 0;
27
29
  const dismissDistanceThreshold = 150;
28
30
  const dismissTriggerThreshold = 50;
29
31
 
30
- export default {
31
- name: 'ImageViewer',
32
- emits: [ 'close', 'saved' ],
33
- props: {
34
- downloadHandler: {
35
- type: Function,
36
- default: null
37
- },
38
- navigationHandler: {
39
- type: Function,
40
- default: (toEntry) => {
41
- this.$router.replace(toEntry.resourcePath);
42
- }
43
- }
44
- },
45
- components: {
46
- Button
47
- },
48
- data() {
49
- return {
50
- showOverlay: false,
51
- showOverlayTimer: null,
52
- lockScroll: false,
53
- curIndex: -1,
54
- lastMousePosition: {},
55
- entry: {},
56
- entries: [],
57
- dismissDistance: 0
58
- };
59
- },
60
- computed: {
61
- hasDownloadHandler() { return typeof this.downloadHandler === 'function' }
62
- },
63
- methods: {
64
- getFileTypeGroup,
65
- keepOverlayAlive() {
66
- this.showOverlay = true;
67
- clearTimeout(this.showOverlayTimer);
68
- this.showOverlayTimer = setTimeout(() => {
69
- // avoid hiding if cursor is above a button
70
- // this crashes sometimes, not yet sure how to better prevent for now "Document.elementFromPoint: Argument 1 is not a finite floating-point value."
71
- try {
72
- if (document.elementFromPoint(this.lastMousePosition.x || 0, this.lastMousePosition.y || 0) !== this.$refs.imageContainer) return;
73
- } catch (e) {
74
- console.error('ImageViewer crash. lastMousePosition', this.lastMousePosition, e);
75
- }
76
-
77
- this.showOverlay = false;
78
- }, 2000);
79
- },
80
- onMouseMove() {
81
- this.lastMousePosition = { x: event.pageX, y: event.pageY };
82
- this.keepOverlayAlive();
83
- },
84
- canHandle(entry) {
85
- return (getFileTypeGroup(entry) === 'image' || this.$refs.typeTestVideo.canPlayType(entry.mimeType))
86
- && !(entry.mimeType === 'image/heif') // supported on apple it seems
87
- && !(entry.mimeType === 'image/vnd.adobe.photoshop');
88
- },
89
- onScroll() {
90
- if (this.lockScroll) return;
91
-
92
- for (let i = 1; i < this.entries.length; i++) {
93
- const elem = document.getElementById('image'+i);
94
- const rect = elem.getBoundingClientRect();
95
- const midX = (rect.x+(rect.width/2));
96
- if (midX >= (rect.width/4) && midX <= 3*(rect.width/4)) {
97
- this.entry = this.entries[i];
98
-
99
- if (this.curIndex !== i) {
100
- this.curIndex = i;
101
- this.navigationHandler(this.entry);
102
- }
103
-
104
- break;
105
- }
106
- }
107
- },
108
- onTouchStart(event) {
109
- this.dismissDistance = 0;
110
- touchDist = 0;
111
- startY = event.changedTouches[0].pageY;
112
- },
113
- onTouchMove(event) {
114
- touchDist = event.changedTouches[0].pageY - startY // get total dist traveled by finger while in contact with surface
115
- if (touchDist > dismissTriggerThreshold) this.dismissDistance = touchDist - dismissTriggerThreshold;
116
- },
117
- onTouchEnd() {
118
- if (touchDist > dismissDistanceThreshold) this.onClose();
119
- touchDist = 0;
120
- this.dismissDistance = 0;
121
- },
122
- open(entry, entries, instant = true) {
123
- if (!entry || entry.isDirectory || !this.canHandle(entry)) return;
124
-
125
- this.lockScroll = true;
126
- this.keepOverlayAlive();
127
- this.entry = entry;
128
- this.entries = entries;
129
- this.curIndex = entries.findIndex((item) => item.fileName === entry.fileName);
130
-
131
- setTimeout(() => {
132
- document.getElementById(`image${this.curIndex}`).scrollIntoView({ behavior: instant ? 'instant' : 'smooth' });
133
- this.$el.focus();
134
- }, 0);
135
-
136
- setTimeout(() => {
137
- this.lockScroll = false;
138
- }, 500);
139
- },
140
- onDownload() {
141
- if (!this.hasDownloadHandler) return;
142
- this.downloadHandler(this.entry);
143
- },
144
- onClose() {
145
- this.$emit('close');
146
-
147
- // pause/stop video and audio playback on close
148
- if (this.$refs.videoElements && this.$refs.videoElements.length) this.$refs.videoElements.forEach(e => e.pause());
149
-
150
- // given some time for closing to keep DOM elements alive
151
- setTimeout(() => {
152
- this.entry = {};
153
- }, 500);
154
- },
155
- onPrev() {
156
- if (this.curIndex < 1) return;
157
- this.navigationHandler(this.entries[this.curIndex-1]);
158
- this.open(this.entries[this.curIndex-1], this.entries, false);
159
- },
160
- onNext() {
161
- if (this.curIndex >= this.entries.length) return;
162
- this.navigationHandler(this.entries[this.curIndex+1]);
163
- this.open(this.entries[this.curIndex+1], this.entries, false);
32
+ const router = useRouter();
33
+ const emit = defineEmits([ 'close', 'saved' ]);
34
+ const props = defineProps({
35
+ downloadHandler: {
36
+ type: Function,
37
+ default: null
38
+ },
39
+ navigationHandler: {
40
+ type: Function,
41
+ default: null
42
+ }
43
+ });
44
+
45
+ const showOverlay = ref(false);
46
+ const showOverlayTimer = ref(null);
47
+ const lockScroll = ref(false);
48
+ const curIndex = ref(-1);
49
+ const lastMousePosition = ref({});
50
+ const entry = ref({});
51
+ const entries = ref([]);
52
+ const dismissDistance = ref(0);
53
+
54
+ const hasDownloadHandler = computed(() => typeof props.downloadHandler === 'function');
55
+ const navigate = (entry) => (props.navigationHandler || ((e) => router.replace(e.resourcePath)))(entry);
56
+
57
+ const rootEl = useTemplateRef('rootEl');
58
+ const imageContainer = useTemplateRef('imageContainer');
59
+ const typeTestVideo = useTemplateRef('typeTestVideo');
60
+ const videoElements = ref([]);
61
+
62
+ function addVideoRef(el) {
63
+ if (el) videoElements.value.push(el);
64
+ }
65
+
66
+ onBeforeUpdate(() => {
67
+ videoElements.value = [];
68
+ });
69
+
70
+ function keepOverlayAlive() {
71
+ showOverlay.value = true;
72
+ clearTimeout(showOverlayTimer.value);
73
+ showOverlayTimer.value = setTimeout(() => {
74
+ try {
75
+ if (document.elementFromPoint(lastMousePosition.value.x || 0, lastMousePosition.value.y || 0) !== imageContainer.value) return;
76
+ } catch (e) {
77
+ console.error('ImageViewer crash. lastMousePosition', lastMousePosition.value, e);
78
+ }
79
+
80
+ showOverlay.value = false;
81
+ }, 2000);
82
+ }
83
+
84
+ function onMouseMove(event) {
85
+ lastMousePosition.value = { x: event.pageX, y: event.pageY };
86
+ keepOverlayAlive();
87
+ }
88
+
89
+ function canHandle(e) {
90
+ return (getFileTypeGroup(e) === 'image' || typeTestVideo.value.canPlayType(e.mimeType))
91
+ && !(e.mimeType === 'image/heif')
92
+ && !(e.mimeType === 'image/vnd.adobe.photoshop');
93
+ }
94
+
95
+ function onScroll() {
96
+ if (lockScroll.value) return;
97
+
98
+ for (let i = 1; i < entries.value.length; i++) {
99
+ const elem = document.getElementById('image'+i);
100
+ const rect = elem.getBoundingClientRect();
101
+ const midX = (rect.x+(rect.width/2));
102
+ if (midX >= (rect.width/4) && midX <= 3*(rect.width/4)) {
103
+ entry.value = entries.value[i];
104
+
105
+ if (curIndex.value !== i) {
106
+ curIndex.value = i;
107
+ navigate(entry.value);
164
108
  }
165
- },
166
- mounted() {}
167
- };
109
+
110
+ break;
111
+ }
112
+ }
113
+ }
114
+
115
+ function onTouchStart(event) {
116
+ dismissDistance.value = 0;
117
+ touchDist = 0;
118
+ startY = event.changedTouches[0].pageY;
119
+ }
120
+
121
+ function onTouchMove(event) {
122
+ touchDist = event.changedTouches[0].pageY - startY;
123
+ if (touchDist > dismissTriggerThreshold) dismissDistance.value = touchDist - dismissTriggerThreshold;
124
+ }
125
+
126
+ function onTouchEnd() {
127
+ if (touchDist > dismissDistanceThreshold) onClose();
128
+ touchDist = 0;
129
+ dismissDistance.value = 0;
130
+ }
131
+
132
+ function open(e, fileList, instant = true) {
133
+ if (!e || e.isDirectory || !canHandle(e)) return;
134
+
135
+ lockScroll.value = true;
136
+ keepOverlayAlive();
137
+ entry.value = e;
138
+ entries.value = fileList;
139
+ curIndex.value = fileList.findIndex((item) => item.fileName === e.fileName);
140
+
141
+ setTimeout(() => {
142
+ document.getElementById(`image${curIndex.value}`).scrollIntoView({ behavior: instant ? 'instant' : 'smooth' });
143
+ rootEl.value.focus();
144
+ }, 0);
145
+
146
+ setTimeout(() => {
147
+ lockScroll.value = false;
148
+ }, 500);
149
+ }
150
+
151
+ function onDownload() {
152
+ if (!hasDownloadHandler.value) return;
153
+ props.downloadHandler(entry.value);
154
+ }
155
+
156
+ function onClose() {
157
+ emit('close');
158
+
159
+ if (videoElements.value && videoElements.value.length) videoElements.value.forEach(e => e.pause());
160
+
161
+ setTimeout(() => {
162
+ entry.value = {};
163
+ }, 500);
164
+ }
165
+
166
+ function onPrev() {
167
+ if (curIndex.value < 1) return;
168
+ navigate(entries.value[curIndex.value-1]);
169
+ open(entries.value[curIndex.value-1], entries.value, false);
170
+ }
171
+
172
+ function onNext() {
173
+ if (curIndex.value >= entries.value.length) return;
174
+ navigate(entries.value[curIndex.value+1]);
175
+ open(entries.value[curIndex.value+1], entries.value, false);
176
+ }
168
177
 
169
178
  </script>
170
179
 
@@ -21,49 +21,41 @@
21
21
  </MainLayout>
22
22
  </template>
23
23
 
24
- <script>
24
+ <script setup>
25
25
 
26
+ import { ref } from 'vue';
26
27
  import Button from '../components/Button.vue';
27
28
  import MainLayout from '../components/MainLayout.vue';
28
29
  import TopBar from '../components/TopBar.vue';
29
30
  import utils from '../utils.js';
30
31
 
31
- export default {
32
- name: 'PdfViewer',
33
- components: {
34
- Button,
35
- MainLayout,
36
- TopBar
37
- },
38
- emits: [ 'close' ],
39
- props: {
40
- tr: {
41
- type: Function,
42
- default(id) { console.warn('Missing tr for PdfViewer'); return utils.translation(id); }
43
- }
44
- },
45
- data() {
46
- return {
47
- entry: null
48
- };
49
- },
50
- methods: {
51
- canHandle(entry) {
52
- return entry.mimeType === 'application/pdf';
53
- },
54
- async open(entry) {
55
- if (!entry || entry.isDirectory || !this.canHandle(entry)) return;
32
+ const emit = defineEmits([ 'close' ]);
56
33
 
57
- this.entry = entry;
58
- },
59
- onClose() {
60
- this.$emit('close');
61
- },
62
- onDownload() {
63
- window.location.href = this.entry.downloadFileUrl;
64
- }
34
+ const props = defineProps({
35
+ tr: {
36
+ type: Function,
37
+ default(id) { console.warn('Missing tr for PdfViewer'); return utils.translation(id); }
65
38
  }
66
- };
39
+ });
40
+
41
+ const entry = ref(null);
42
+
43
+ function canHandle(e) {
44
+ return e.mimeType === 'application/pdf';
45
+ }
46
+
47
+ async function open(e) {
48
+ if (!e || e.isDirectory || !canHandle(e)) return;
49
+ entry.value = e;
50
+ }
51
+
52
+ function onClose() {
53
+ emit('close');
54
+ }
55
+
56
+ function onDownload() {
57
+ window.location.href = entry.value.downloadFileUrl;
58
+ }
67
59
 
68
60
  </script>
69
61
 
@@ -22,8 +22,9 @@
22
22
  </MainLayout>
23
23
  </template>
24
24
 
25
- <script>
25
+ <script setup>
26
26
 
27
+ import { ref, onMounted, onBeforeUnmount, useTemplateRef } from 'vue';
27
28
  import { editor, languages } from 'monaco-editor';
28
29
  import editorWorker from 'monaco-editor/esm/vs/editor/editor.worker?worker';
29
30
  import jsonWorker from 'monaco-editor/esm/vs/language/json/json.worker?worker';
@@ -57,146 +58,143 @@ function getLanguage(filename) {
57
58
  return language ? language.id : '';
58
59
  }
59
60
 
60
- export default {
61
- name: 'TextViewer',
62
- emits: [ 'close' ],
63
- props: {
64
- saveHandler: {
65
- type: Function,
66
- default() { console.warn('Missing saveHandler for TextViewer'); }
67
- },
68
- tr: {
69
- type: Function,
70
- default(id) { console.warn('Missing tr for TextViewer'); return utils.translation(id); }
71
- },
72
- readonly: {
73
- type: Boolean,
74
- default: false
61
+ const emit = defineEmits([ 'close' ]);
62
+ const props = defineProps({
63
+ saveHandler: {
64
+ type: Function,
65
+ default() { console.warn('Missing saveHandler for TextViewer'); }
66
+ },
67
+ tr: {
68
+ type: Function,
69
+ default(id) { console.warn('Missing tr for TextViewer'); return utils.translation(id); }
70
+ },
71
+ readonly: {
72
+ type: Boolean,
73
+ default: false
74
+ }
75
+ });
76
+
77
+ const entry = ref({});
78
+ const isChanged = ref(false);
79
+ const busySave = ref(false);
80
+ const lastSavedVersion = ref(0);
81
+ const undoPossible = ref(false);
82
+ const redoPossible = ref(false);
83
+
84
+ const inputDialog = useTemplateRef('inputDialog');
85
+ const editorView = useTemplateRef('editorView');
86
+
87
+ let editorInstance = null;
88
+
89
+ function canHandle(e) {
90
+ if (e.isBinary) return false;
91
+ return utils.getFileTypeGroup(e) === 'text' ||
92
+ utils.getFileTypeGroup(e) === 'application';
93
+ }
94
+
95
+ function open(e, content) {
96
+ if (!e || e.isDirectory || !canHandle(e)) return;
97
+
98
+ entry.value = e;
99
+
100
+ editorInstance.setModel(editor.createModel(content, getLanguage(e.fileName)));
101
+
102
+ const initialVersion = editorInstance.getModel().getAlternativeVersionId();
103
+ let currentVersion = initialVersion;
104
+ let lastVersion = initialVersion;
105
+ lastSavedVersion.value = initialVersion;
106
+
107
+ editorInstance.onDidChangeModelContent(e => {
108
+ const versionId = editorInstance.getModel().getAlternativeVersionId();
109
+
110
+ isChanged.value = lastSavedVersion.value !== versionId;
111
+
112
+ if (versionId < currentVersion) {
113
+ redoPossible.value = true;
114
+ if (versionId === initialVersion) {
115
+ undoPossible.value = false;
75
116
  }
76
- },
77
- components: {
78
- Button,
79
- InputDialog,
80
- MainLayout,
81
- TopBar,
82
- Spinner
83
- },
84
- data() {
85
- return {
86
- entry: {},
87
- isChanged: false,
88
- busySave: false,
89
- lastSavedVersion: 0
90
- };
91
- },
92
- methods: {
93
- canHandle(entry) {
94
- if (entry.isBinary) return false;
95
- return utils.getFileTypeGroup(entry) === 'text' ||
96
- utils.getFileTypeGroup(entry) === 'application';
97
- },
98
- open(entry, content) {
99
- if (!entry || entry.isDirectory || !this.canHandle(entry)) return;
100
-
101
- this.entry = entry;
102
-
103
- this.editor.setModel(editor.createModel(content, getLanguage(entry.fileName)));
104
-
105
- const initialVersion = this.editor.getModel().getAlternativeVersionId();
106
- let currentVersion = initialVersion;
107
- let lastVersion = initialVersion;
108
- this.lastSavedVersion = initialVersion;
109
-
110
- this.editor.onDidChangeModelContent(e => {
111
- const versionId = this.editor.getModel().getAlternativeVersionId();
112
-
113
- this.isChanged = this.lastSavedVersion !== versionId;
114
-
115
- if (versionId < currentVersion) {
116
- this.redoPossible = true;
117
- // no more undo possible
118
- if (versionId === initialVersion) {
119
- this.undoPossible = false;
120
- }
121
- } else {
122
- if (versionId <= lastVersion) {
123
- // redoing the last change
124
- if (versionId == lastVersion) {
125
- this.redoPossible = false;
126
- }
127
- } else { // adding new change, disable redo when adding new changes
128
- this.redoPossible = false;
129
- if (currentVersion > lastVersion) {
130
- lastVersion = currentVersion;
131
- }
132
- }
133
- this.undoPossible = true;
134
- }
135
- currentVersion = versionId;
136
- });
137
- },
138
- async onClose() {
139
- if (this.isChanged) {
140
- const yes = await this.$refs.inputDialog.confirm({
141
- message: this.tr('filemanager.textEditorCloseDialog.title'),
142
- confirmStyle: 'danger',
143
- confirmLabel: 'Discard changes and close',
144
- rejectLabel: 'Cancel'
145
- });
146
-
147
- if (!yes) return;
148
-
149
- this.editor.setModel(editor.createModel(''));
150
- this.$emit('close');
151
- this.isChanged = false;
152
- } else {
153
- this.editor.setModel(editor.createModel(''));
154
- this.$emit('close');
117
+ } else {
118
+ if (versionId <= lastVersion) {
119
+ if (versionId == lastVersion) {
120
+ redoPossible.value = false;
155
121
  }
156
- },
157
- async onSave() {
158
- this.busySave = true;
159
-
160
- await this.saveHandler(this.entry, this.editor.getValue());
161
-
162
- this.isChanged = false;
163
- this.lastSavedVersion = this.editor.getModel().getAlternativeVersionId();
164
-
165
- this.busySave = false;
166
- this.editor.focus();
167
- },
168
- keydownHandler(event) {
169
- if((navigator.platform.match('Mac') ? event.metaKey : event.ctrlKey) && event.key === 's') {
170
- event.preventDefault();
171
- if (this.isChanged) this.onSave();
122
+ } else {
123
+ redoPossible.value = false;
124
+ if (currentVersion > lastVersion) {
125
+ lastVersion = currentVersion;
172
126
  }
173
- },
174
- beforeunloadHandler(event) {
175
- // this prevents accidental closing or reloading
176
- if (this.isChanged) event.preventDefault();
177
127
  }
178
- },
179
- mounted() {
180
- const theme = (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) ? 'vs-dark' : 'vs-light';
181
-
182
- this.editor = editor.create(this.$refs.editorView, {
183
- automaticLayout: true,
184
- value: '',
185
- theme
186
- });
187
-
188
- window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', event => {
189
- editor.setTheme(event.matches ? 'vs-dark' : 'vs-light');
190
- });
191
-
192
- window.addEventListener('keydown', this.keydownHandler);
193
- window.addEventListener('beforeunload', this.beforeunloadHandler);
194
- },
195
- beforeUnmount() {
196
- window.removeEventListener('keydown', this.keydownHandler);
197
- window.removeEventListener('beforeunload', this.beforeunloadHandler);
128
+ undoPossible.value = true;
198
129
  }
199
- };
130
+ currentVersion = versionId;
131
+ });
132
+ }
133
+
134
+ async function onClose() {
135
+ if (isChanged.value) {
136
+ const yes = await inputDialog.value.confirm({
137
+ message: props.tr('filemanager.textEditorCloseDialog.title'),
138
+ confirmStyle: 'danger',
139
+ confirmLabel: 'Discard changes and close',
140
+ rejectLabel: 'Cancel'
141
+ });
142
+
143
+ if (!yes) return;
144
+
145
+ editorInstance.setModel(editor.createModel(''));
146
+ emit('close');
147
+ isChanged.value = false;
148
+ } else {
149
+ editorInstance.setModel(editor.createModel(''));
150
+ emit('close');
151
+ }
152
+ }
153
+
154
+ async function onSave() {
155
+ busySave.value = true;
156
+
157
+ await props.saveHandler(entry.value, editorInstance.getValue());
158
+
159
+ isChanged.value = false;
160
+ lastSavedVersion.value = editorInstance.getModel().getAlternativeVersionId();
161
+
162
+ busySave.value = false;
163
+ editorInstance.focus();
164
+ }
165
+
166
+ function keydownHandler(event) {
167
+ if((navigator.platform.match('Mac') ? event.metaKey : event.ctrlKey) && event.key === 's') {
168
+ event.preventDefault();
169
+ if (isChanged.value) onSave();
170
+ }
171
+ }
172
+
173
+ function beforeunloadHandler(event) {
174
+ if (isChanged.value) event.preventDefault();
175
+ }
176
+
177
+ onMounted(() => {
178
+ const theme = (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) ? 'vs-dark' : 'vs-light';
179
+
180
+ editorInstance = editor.create(editorView.value, {
181
+ automaticLayout: true,
182
+ value: '',
183
+ theme
184
+ });
185
+
186
+ window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', event => {
187
+ editor.setTheme(event.matches ? 'vs-dark' : 'vs-light');
188
+ });
189
+
190
+ window.addEventListener('keydown', keydownHandler);
191
+ window.addEventListener('beforeunload', beforeunloadHandler);
192
+ });
193
+
194
+ onBeforeUnmount(() => {
195
+ window.removeEventListener('keydown', keydownHandler);
196
+ window.removeEventListener('beforeunload', beforeunloadHandler);
197
+ });
200
198
 
201
199
  </script>
202
200