@christianriedl/media 1.0.281 → 1.0.283

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@christianriedl/media",
3
- "version": "1.0.281",
3
+ "version": "1.0.283",
4
4
  "description": "RIC media interfaces",
5
5
 
6
6
  "main": "dist/index.js",
@@ -18,9 +18,9 @@
18
18
  "author": "Christian Riedl",
19
19
  "license": "ISC",
20
20
  "dependencies": {
21
- "@christianriedl/utils": "^1.0.168",
22
- "@christianriedl/rest": "^1.0.89",
23
- "@christianriedl/epg": "^1.0.43",
21
+ "@christianriedl/utils": "^1.0.171",
22
+ "@christianriedl/rest": "^1.0.90",
23
+ "@christianriedl/epg": "^1.0.44",
24
24
  "buffer": "^6.0.3"
25
25
  },
26
26
  "devDependencies": {
@@ -58,7 +58,7 @@
58
58
  </script>
59
59
 
60
60
  <template>
61
- <v-row density="compact" :class="[place.color,'place']" :style="heightStyle" >
61
+ <v-row density="compact" :class="[place.color,'place']" :style="heightStyle" gap="0" >
62
62
  <v-col v-if="place.vorne" cols="12" @click="selectVorne">
63
63
  <p :class="vorne" v-html="textVorne"></p>
64
64
  </v-col>
@@ -70,6 +70,9 @@
70
70
  </template>
71
71
 
72
72
  <style scoped>
73
+ p {
74
+ margin: 0;
75
+ }
73
76
  .place {
74
77
  border-style: solid;
75
78
  border-color: black;
@@ -15,7 +15,7 @@
15
15
  </script>
16
16
 
17
17
  <template>
18
- <v-row v-if="props.ismobile" density="compact" class="align-center" :class="classObject" @click="onSelect">
18
+ <v-row v-if="props.ismobile" density="compact" class="align-center" :class="classObject" @click="onSelect" gap="0">
19
19
  <v-col cols="12" >
20
20
  <p class="font-weight-bold">{{book.givenName + ' ' + book.surName}}</p>
21
21
  </v-col>
@@ -23,7 +23,7 @@
23
23
  <p>{{book.title}}</p>
24
24
  </v-col>
25
25
  </v-row>
26
- <v-row v-else density="compact" class="align-center" :class="classObject" @click="onSelect" >
26
+ <v-row v-else density="compact" class="align-center" :class="classObject" @click="onSelect" gap="0">
27
27
  <v-col cols="4" >
28
28
  <p class="font-weight-bold">{{book.givenName + ' ' + book.surName}}</p>
29
29
  </v-col>
@@ -0,0 +1,282 @@
1
+ <script setup lang="ts">
2
+ import { ref, reactive, computed, onMounted, onUnmounted, nextTick } from 'vue'
3
+
4
+ const props = defineProps<{ contentW: number, contentH: number, maxScale?: number, wheelSensitivity?: number}>()
5
+
6
+ interface ZoomPanState {
7
+ scale: number
8
+ tx: number // translateX
9
+ ty: number // translateY
10
+ }
11
+
12
+ const containerRef = ref<HTMLElement>()
13
+ const imgRef = ref<HTMLImageElement>()
14
+ const state = reactive<ZoomPanState>({ scale: 1, tx: 0, ty: 0 })
15
+
16
+ // Damit Touch-Pan nicht mit scale=1 kollidiert
17
+ //const isPanning = computed(() => state.scale > 1.001)
18
+ const isPanning = computed(() => {
19
+ const fitScale = _imgW > 0 ? Math.min(_containerW / _imgW, _containerH / _imgH) : 1
20
+ return state.scale > fitScale + 0.001
21
+ })
22
+ const transform = computed(() => `translate(${state.tx}px, ${state.ty}px) scale(${state.scale})`)
23
+ const dragging = ref(false);
24
+ const cursor = computed(() => {
25
+ if (dragging.value) return 'grabbing'
26
+ if (isPanning.value) return 'grab' // scale > 1, Maus oben
27
+ return 'default'
28
+ })
29
+ let maxScale = props.maxScale ?? 1; // 1:1 Pixel
30
+ let wheelSensitivity = props.wheelSensitivity ?? 0.001;
31
+
32
+ // ── Touch (Pinch + Pan) ───────────────────────────────────────────────
33
+ let lastTouchDist = 0
34
+ let lastTouchMid = { x: 0, y: 0 }
35
+ let lastPan = { x: 0, y: 0 }
36
+
37
+ let _containerW = 0, _containerH = 0, _imgW = 0, _imgH = 0
38
+
39
+ // Mouse-Drag Pan (Desktop)
40
+ let lastMouse = { x: 0, y: 0 }
41
+ let _resizeTimer: ReturnType<typeof setTimeout> | null = null
42
+
43
+ // If not working : Handle Fullscreen seperately
44
+ function setDimensionsDebounced(cw: number, ch: number, iw: number, ih: number) {
45
+ if (_resizeTimer) clearTimeout(_resizeTimer)
46
+ _resizeTimer = setTimeout(() => {
47
+ _resizeTimer = null
48
+ console.log(`${cw}x${ch} at ${new Date().getTime()}`);
49
+ setDimensions(cw, ch, iw, ih)
50
+ }, 50)
51
+ }
52
+ onMounted(() => {
53
+ const ro = new ResizeObserver(([entry]) => {
54
+ const { width, height } = entry.contentRect
55
+ setDimensionsDebounced(width, height, props.contentW, props.contentH)
56
+ })
57
+ ro.observe(containerRef.value!)
58
+ onUnmounted(() => {
59
+ if (_resizeTimer) clearTimeout(_resizeTimer)
60
+ ro.disconnect()
61
+ onMouseUp();
62
+ })
63
+ });
64
+
65
+ function onMouseDown(e: MouseEvent) {
66
+ if (e.button !== 0) return // nur linke Taste
67
+ if (!isPanning.value) return
68
+ dragging.value = true
69
+ lastMouse = { x: e.clientX, y: e.clientY }
70
+ window.addEventListener('mousemove', onMouseMove)
71
+ window.addEventListener('mouseup', onMouseUp)
72
+ console.log ("mousedown")
73
+ }
74
+ function onMouseMove(e: MouseEvent) {
75
+ if (!dragging.value) return
76
+ pan(e.clientX - lastMouse.x, e.clientY - lastMouse.y)
77
+ lastMouse.x = e.clientX;
78
+ lastMouse.y = e.clientY;
79
+ console.log ("mousemove")
80
+ }
81
+ function onMouseUp() {
82
+ if (!dragging.value) return
83
+ console.log ("mouseup")
84
+ dragging.value = false
85
+ window.removeEventListener('mousemove', onMouseMove)
86
+ window.removeEventListener('mouseup', onMouseUp)
87
+ }
88
+
89
+ // ── Pivot-Zoom (Kern-Arithmetik) ──────────────────────────────────────
90
+ /*
91
+ function zoomAt(pivotX: number, pivotY: number, delta: number) {
92
+ const newScale = Math.min(maxScale, Math.max(minScale, state.scale * delta))
93
+ const ratio = newScale / state.scale
94
+
95
+ // Pivot bleibt fix: Punkt unter Maus/Finger darf sich nicht verschieben
96
+ state.tx = pivotX - (pivotX - state.tx) * ratio
97
+ state.ty = pivotY - (pivotY - state.ty) * ratio
98
+ state.scale = newScale
99
+
100
+ clampTranslate()
101
+ }
102
+ */
103
+ function zoomAt(pivotX: number, pivotY: number, delta: number) {
104
+ const fitScale = Math.min(_containerW / _imgW, _containerH / _imgH)
105
+ const newScale = Math.min(maxScale, Math.max(fitScale, state.scale * delta))
106
+ const ratio = newScale / state.scale
107
+
108
+ // Pivot von Container-Koordinaten auf center/center umrechnen
109
+ const px = pivotX - _containerW / 2
110
+ const py = pivotY - _containerH / 2
111
+
112
+ state.tx = px - (px - state.tx) * ratio
113
+ state.ty = py - (py - state.ty) * ratio
114
+ state.scale = newScale
115
+
116
+ clampTranslate()
117
+ }
118
+ // Pan
119
+ function pan(dx: number, dy: number) {
120
+ state.tx += dx
121
+ state.ty += dy
122
+ clampTranslate()
123
+ }
124
+
125
+ // ── Wheel ─────────────────────────────────────────────────────────────
126
+ function onWheel(e: WheelEvent) {
127
+ e.preventDefault()
128
+ const rect = (e.currentTarget as HTMLElement).getBoundingClientRect()
129
+ const pivotX = e.clientX - rect.left
130
+ const pivotY = e.clientY - rect.top
131
+ const delta = 1 - e.deltaY * wheelSensitivity
132
+ zoomAt(pivotX, pivotY, delta)
133
+ }
134
+
135
+ function getTouchMid(touches: TouchList, rect: DOMRect) {
136
+ const t0 = touches[0], t1 = touches[1]
137
+ return {
138
+ x: ((t0.clientX + t1.clientX) / 2) - rect.left,
139
+ y: ((t0.clientY + t1.clientY) / 2) - rect.top,
140
+ }
141
+ }
142
+
143
+ function getTouchDist(touches: TouchList) : number {
144
+ const dx = touches[0].clientX - touches[1].clientX
145
+ const dy = touches[0].clientY - touches[1].clientY
146
+ return Math.hypot(dx, dy)
147
+ }
148
+
149
+ function onTouchStart(e: TouchEvent) {
150
+ const rect = (e.currentTarget as HTMLElement).getBoundingClientRect()
151
+ if (e.touches.length === 2) {
152
+ lastTouchDist = getTouchDist(e.touches)
153
+ lastTouchMid = getTouchMid(e.touches, rect)
154
+ }
155
+ else if (e.touches.length === 1 && isPanning.value) {
156
+ lastPan = { x: e.touches[0].clientX, y: e.touches[0].clientY }
157
+ }
158
+ }
159
+
160
+ function onTouchMove(e: TouchEvent) {
161
+ const rect = (e.currentTarget as HTMLElement).getBoundingClientRect()
162
+
163
+ if (e.touches.length === 2) {
164
+ e.preventDefault() // kein Browser-Scroll
165
+ const dist = getTouchDist(e.touches)
166
+ const mid = getTouchMid(e.touches, rect)
167
+ zoomAt(mid.x, mid.y, dist / lastTouchDist) // Pinch-Zoom
168
+ // Gleichzeitig Pan (Mittelpunkt hat sich verschoben)
169
+ state.tx += mid.x - lastTouchMid.x
170
+ state.ty += mid.y - lastTouchMid.y
171
+ lastTouchDist = dist
172
+ lastTouchMid = mid
173
+ clampTranslate()
174
+ }
175
+ else if (e.touches.length === 1 && isPanning.value) {
176
+ e.preventDefault() // kein Carousel-Swipe
177
+ state.tx += e.touches[0].clientX - lastPan.x
178
+ state.ty += e.touches[0].clientY - lastPan.y
179
+ lastPan = { x: e.touches[0].clientX, y: e.touches[0].clientY }
180
+ clampTranslate()
181
+ }
182
+ // scale === 1 → kein preventDefault() → Carousel-Swipe funktioniert
183
+ }
184
+
185
+ function fitToContainer() {
186
+ const scaleX = _containerW / _imgW
187
+ const scaleY = _containerH / _imgH
188
+ state.scale = Math.min(scaleX, scaleY)
189
+ state.tx = 0 // center/center zentriert automatisch
190
+ state.ty = 0
191
+ }
192
+ // ── Translate begrenzen (Bild darf Container nicht verlassen) ─────────
193
+ /*
194
+ function setDimensions(cw: number, ch: number, iw: number, ih: number) {
195
+ _containerW = cw; _containerH = ch; _imgW = iw; _imgH = ih
196
+ }
197
+ */
198
+ function setDimensions(cw: number, ch: number, iw: number, ih: number) {
199
+ const wasInitialized = _containerW > 0
200
+
201
+ if (wasInitialized) {
202
+ // Bildmitte in center/center-Koordinaten — tx/ty ist bereits relativ zur Mitte
203
+ const cx = state.tx / state.scale
204
+ const cy = state.ty / state.scale
205
+
206
+ const fitScale = Math.min(_containerW / _imgW, _containerH / _imgH)
207
+ if (state.scale < fitScale) state.scale = fitScale
208
+
209
+ state.tx = cx * state.scale
210
+ state.ty = cy * state.scale
211
+ clampTranslate()
212
+ } else {
213
+ _containerW = cw; _containerH = ch; _imgW = iw; _imgH = ih
214
+ fitToContainer()
215
+ }
216
+ }
217
+ /*
218
+ function clampTranslate() {
219
+ const scaledW = _imgW * state.scale
220
+ const scaledH = _imgH * state.scale
221
+ const maxTx = Math.max(0, (scaledW - _containerW) / 2)
222
+ const maxTy = Math.max(0, (scaledH - _containerH) / 2)
223
+ console.log (`before scale:${state.scale} x:${state.tx} y:${state.ty}` )
224
+ state.tx = Math.min(maxTx, Math.max(-maxTx, state.tx))
225
+ state.ty = Math.min(maxTy, Math.max(-maxTy, state.ty))
226
+ console.log (`after scale:${state.scale} x:${state.tx} y:${state.ty}` )
227
+ }
228
+ */
229
+ function clampTranslate() {
230
+ const scaledW = _imgW * state.scale
231
+ const scaledH = _imgH * state.scale
232
+
233
+ console.log (`before scale:${state.scale} x:${state.tx} y:${state.ty}` )
234
+ if (scaledW >= _containerW) {
235
+ const maxTx = (scaledW - _containerW) / 2
236
+ state.tx = Math.min(maxTx, Math.max(-maxTx, state.tx))
237
+ } else {
238
+ state.tx = 0 // kleiner als Container → zentriert = 0
239
+ }
240
+
241
+ if (scaledH >= _containerH) {
242
+ const maxTy = (scaledH - _containerH) / 2
243
+ state.ty = Math.min(maxTy, Math.max(-maxTy, state.ty))
244
+ } else {
245
+ state.ty = 0
246
+ }
247
+ console.log (`after scale:${state.scale} x:${state.tx} y:${state.ty}` )
248
+ }
249
+ </script>
250
+
251
+ <template>
252
+ <div
253
+ ref="containerRef"
254
+ class="zoom-pan-container"
255
+ :style="{ cursor }"
256
+ @wheel.prevent="onWheel"
257
+ @touchstart="onTouchStart"
258
+ @touchmove="onTouchMove"
259
+ @mousedown="onMouseDown"
260
+ >
261
+ <!--
262
+ <div :style="{ transform, transformOrigin: 'center center', width: contentW + 'px', height: contentH + 'px' }">
263
+ <slot />
264
+ </div>
265
+ -->
266
+ <!-- Dieses div ist immer 100% des Containers — transformOrigin: center center stimmt -->
267
+ <div :style="{ transform, transformOrigin: 'center center', width: '100%', height: '100%' }">
268
+ <!-- Slot-Inhalt absolut zentriert -->
269
+ <div style="position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%)">
270
+ <slot />
271
+ </div>
272
+ </div>
273
+ </div>
274
+ </template>
275
+
276
+ <style scoped>
277
+ .zoom-pan-container {
278
+ overflow: hidden;
279
+ width: 100%;
280
+ height: 100%;
281
+ }
282
+ </style>
@@ -434,7 +434,7 @@
434
434
  <v-btn prepend-icon="$gps" class="bg-office" @click="places">Wo</v-btn>
435
435
  </v-col>
436
436
  </v-row>
437
- <v-row v-if="isMobile" density="compact" class="bg-office align-center" >
437
+ <v-row v-if="isMobile" density="compact" class="bg-office align-center" gap="0" >
438
438
  <v-col cols="6">
439
439
  <v-combobox v-model="selectedAuthor" :items="authors" item-value="id" item-title="sN" hide-details density="compact" single-line @update:modelValue="authorNameChanged"></v-combobox>
440
440
  </v-col>
@@ -442,7 +442,7 @@
442
442
  <v-text-field v-model="currentAuthor.givenName" hide-details density="compact" @update:modelValue="fieldChanged" @update:focused="givenNameFocused"></v-text-field>
443
443
  </v-col>
444
444
  </v-row>
445
- <v-row v-else density="compact" class="bg-office align-center">
445
+ <v-row v-else density="compact" class="bg-office align-center" gap="0">
446
446
  <v-col cols="2">
447
447
  <v-combobox v-model="selectedAuthor" :items="authors" item-value="id" item-title="sN" hide-details density="compact" single-line @update:modelValue="authorNameChanged"></v-combobox>
448
448
  </v-col>
@@ -471,11 +471,11 @@
471
471
  </v-col>
472
472
  </v-row>
473
473
  <v-divider></v-divider>
474
- <v-row v-if="isMobile" density="compact" class="bg-office align-center" >
474
+ <v-row v-if="isMobile" density="compact" class="bg-office align-center" gap="0" >
475
475
  <v-col cols="9">TITEL</v-col>
476
476
  <v-col cols="3">JAHR</v-col>
477
477
  </v-row>
478
- <v-row v-else density="compact" class="bg-office align-center">
478
+ <v-row v-else density="compact" class="bg-office align-center" gap="0">
479
479
  <v-col cols="6">
480
480
  <v-btn v-if="sortedByYear" class="bg-office" @click="sortByTitle" icon="$down"></v-btn>
481
481
  TITEL
@@ -217,13 +217,13 @@
217
217
  <v-card class="bg-office">
218
218
  <v-card-title>{{path}}</v-card-title>
219
219
  <v-container v-if="isMobile" fluid>
220
- <v-row v-for="dir in directoryNames" :key="dir">
220
+ <v-row v-for="dir in directoryNames" :key="dir" density="compact" class="align-center">
221
221
  <v-col cols="10"><p class="font-weight-medium" @click.stop="onSelect(dir)">{{dir}}</p></v-col>
222
222
  <v-col cols="1"><v-btn v-if="isAdmin" icon="$share" class="bg-office" density="compact" variant="flat" @click.stop="shareDir(dir)"></v-btn></v-col>
223
223
  <v-col cols="1"><v-btn v-if="isAdmin" icon="$delete" class="bg-office" density="compact" variant="flat" @click.stop="deleteDirOrFile(dir)"></v-btn></v-col>
224
224
  <v-divider />
225
225
  </v-row>
226
- <v-row v-for="file in fileNames" :key="file" class="text-decoration-underline text-primary">
226
+ <v-row v-for="file in fileNames" :key="file" density="compact" class="text-decoration-underline text-primary align-center">
227
227
  <v-col cols="9"><p class="font-weight-medium text-decoration-underline text-primary" @click.stop="download(file)">{{file}}</p></v-col>
228
228
  <v-col cols="1"><v-btn v-if="isAdmin" icon="$send" class="bg-office" density="compact" variant="flat" @click.stop="sendFile(file)"></v-btn></v-col>
229
229
  <v-col cols="1"><v-btn v-if="isAdmin" icon="$share" class="bg-office" density="compact" variant="flat" @click.stop="shareFile(file)"></v-btn></v-col>
@@ -250,13 +250,13 @@
250
250
  </v-col>
251
251
  </v-row>
252
252
  <file-upload v-if="uploadVisible" :isdocument="true" :directory="path" @done="uploadDone"></file-upload>
253
- <v-row v-for="dir in directoryInfos" :key="dir.name">
253
+ <v-row v-for="dir in directoryInfos" :key="dir.name" density="compact" class="align-center">
254
254
  <v-col cols="10"><p class="font-weight-medium" @click.stop="onSelect(dir.name)">{{dir.name}}<span>{{info(dir)}}</span></p></v-col>
255
255
  <v-col cols="1"><v-btn v-if="isAdmin" icon="$share" class="bg-office" density="compact" variant="flat" @click.stop="shareDir(dir.name)"></v-btn></v-col>
256
256
  <v-col cols="1"><v-btn v-if="isAdmin" icon="$delete" class="bg-office" density="compact" variant="flat" @click.stop="deleteDirOrFile(dir.name)"></v-btn></v-col>
257
257
  <v-divider />
258
258
  </v-row>
259
- <v-row v-for="file in fileInfos" :key="file.name" class="text-decoration-underline text-primary">
259
+ <v-row v-for="file in fileInfos" :key="file.name" density="compact" class="text-decoration-underline text-primary align-center">
260
260
  <v-col cols="8"><p class="font-weight-medium text-decoration-underline text-primary" @click.stop="download(file.name, file.folder)">{{file.name}}<span>{{info(file)}}</span></p></v-col>
261
261
  <v-col cols="1"><v-btn v-if="isAdmin" icon="$brain" :class="vectorStoreClass(file.name)" density="compact" variant="flat" @click.stop="vectorStore(file.name)"></v-btn></v-col>
262
262
  <v-col cols="1"><v-btn v-if="isAdmin" icon="$send" class="bg-office" density="compact" variant="flat" @click.stop="sendFile(file.name, file.folder)"></v-btn></v-col>
@@ -276,3 +276,9 @@
276
276
  </v-dialog>
277
277
  </template>
278
278
 
279
+ <style scoped>
280
+ .v-row--density-compact {
281
+ --v-col-gap-x: 0px;
282
+ --v-col-gap-y: 0px;
283
+ }
284
+ </style>
@@ -350,10 +350,10 @@
350
350
  <v-progress-linear v-if="playingTrack" v-model="positionLength" color="blue" height="25"><strong>{{positionText}}</strong></v-progress-linear>
351
351
  </v-card>
352
352
  <file-upload v-if="uploadVisible" accept="audio/mpeg, audio/flac"></file-upload>
353
- <v-card :height="listHeight" class="overflow-y-auto bg-media">
353
+ <v-card :height="listHeight" density="compact" class="overflow-y-auto bg-media">
354
354
  <v-list lines="two" class="bg-media">
355
355
  <v-list-item-group v-model="itemIndex">
356
- <v-list-item v-for="(item,index) in items" :key="item.dlnaid" :title="item.title" :subtitle="item.subTitle"
356
+ <v-list-item v-for="(item,index) in items" :key="item.dlnaid" density="compact" :title="item.title" :subtitle="item.subTitle"
357
357
  active-color="blue" :active="index == playIndex" @click="listItem(item)">
358
358
  <template v-slot:prepend>
359
359
  <v-avatar rounded="0" size="x-large" v-if="item.thumbnailUrl">
@@ -105,10 +105,10 @@
105
105
  </v-btn>
106
106
  </v-card-actions>
107
107
  </v-card>
108
- <v-card :max-height="listHeight" class="overflow-y-auto bg-media">
109
- <v-list lines="two" class="bg-media">
108
+ <v-card :max-height="listHeight" density="compact" class="overflow-y-auto bg-media">
109
+ <v-list lines="two" density="compact" class="bg-media">
110
110
  <v-list-item-group v-model="itemIndex" color="primary">
111
- <v-list-item v-for="(item,index) in items" :key="item.dlnaid" :title="item.title"
111
+ <v-list-item v-for="(item,index) in items" :key="item.dlnaid" :title="item.title" density="compact"
112
112
  active-color="blue" :active="index == playIndex"
113
113
  :subtitle="item.subTitle" @click="listItem(item)">
114
114
  <template v-slot:prepend>
@@ -184,10 +184,10 @@
184
184
  </v-btn>
185
185
  </v-card-actions>
186
186
  </v-card>
187
- <v-card :max-height="listHeight" class="overflow-y-auto bg-media">
188
- <v-list lines="two" class="bg-media">
187
+ <v-card :max-height="listHeight" density="compact" class="overflow-y-auto bg-media">
188
+ <v-list lines="two" density="compact" class="bg-media">
189
189
  <v-list-item-group v-model="itemIndex" color="primary">
190
- <v-list-item v-for="item in items" :key="item.dlnaid" :title="item.title" :subtitle="item.subTitle" @click="listItem(item)">
190
+ <v-list-item v-for="item in items" :key="item.dlnaid" density="compact" :title="item.title" :subtitle="item.subTitle" @click="listItem(item)">
191
191
  <template v-slot:prepend>
192
192
  <v-avatar rounded="0" size="x-large" v-if="item.thumbnailUrl">
193
193
  <v-img :src="item.thumbnailUrl" :cover="false"></v-img>
@@ -11,6 +11,7 @@
11
11
  import PhotoMetaData from '../components/PhotoMetaData.vue';
12
12
  import PhotoMap from '../components/PhotoMap.vue';
13
13
  import Mail from '@christianriedl/office/src/components/Mail.vue';
14
+ import ImageZoomer from "@christianriedl/media/src/components/ImageZoomer.vue";
14
15
 
15
16
  const router = useRouter();
16
17
  const route = useRoute();
@@ -42,9 +43,15 @@
42
43
  const showSendMail = ref(false);
43
44
  const attachment = ref("");
44
45
  const smallAttachment = ref("");
46
+ const showName = ref(false);
45
47
  const zoom = 14;
46
48
  let album: IMediaFolder;
49
+ let currentItem: IPictureFile = {} as IPictureFile;
50
+ let currentHeight = 0;
51
+ let currentWidth = 0;
47
52
  let origUrl = "";
53
+ let windowWidth = 0;
54
+ let windowHeight = 0;
48
55
  let location: string;
49
56
  let mouseDownTime: number = 0;
50
57
  let mouseTimer: number = -1;
@@ -52,15 +59,18 @@
52
59
  let nextIndex: number = -1;
53
60
  let keepBlobs: boolean = true;
54
61
  let mediaUrlLength = mediaService.mediaUrl.length;
55
- const showName = ref(false);
56
62
 
57
63
  watch([appState.bodyHeight, appState.pageWidth], () => {
64
+ windowHeight = window.screen.height;
65
+ windowWidth = window.screen.width;
58
66
  width.value = appState.pageWidth.value;
59
67
  height.value = appState.bodyHeight.value;
60
68
  landscape.value = width.value > height.value;
61
69
  }, { immediate: true });
62
70
 
63
71
  async function start(): Promise<boolean> {
72
+ windowHeight = window.screen.height;
73
+ windowWidth = window.screen.width;
64
74
  window.document.addEventListener('keydown', onKey);
65
75
  appState.navigate.value = onNavigate;
66
76
  if (carousel.value)
@@ -97,7 +107,7 @@
97
107
  items.splice(0, items.length, ...list);
98
108
  if (route.query.start) {
99
109
  const start = route.query.start.toString();
100
- const idx = items.findIndex((item) => item.dlnaid == start);
110
+ const idx = items.findIndex((it) => it.dlnaid == start);
101
111
  if (idx >= 0) {
102
112
  index.value = idx;
103
113
  }
@@ -246,11 +256,10 @@
246
256
  showInfo.value = false;
247
257
  }
248
258
  else {
249
- const item = items[index.value];
250
- const folder = album ? album : mediaService.getFolder(item.dlnaParentId);
251
- origUrl = mediaService.getPhotoUrl(folder.url, item.url, '0x0x0');
252
- hasCoordinates.value = !!(item.flags & EPictureFlags.Has_Gps);
253
- headerText.value = "Metadaten - " + item.name;
259
+ const folder = album ? album : mediaService.getFolder(currentItem.dlnaParentId);
260
+ origUrl = mediaService.getPhotoUrl(folder.url, currentItem.url, '0x0x0');
261
+ hasCoordinates.value = !!(currentItem.flags & EPictureFlags.Has_Gps);
262
+ headerText.value = "Metadaten - " + currentItem.name;
254
263
  showInfo.value = true;
255
264
  }
256
265
  break;
@@ -318,11 +327,10 @@
318
327
  showInfo.value = false;
319
328
  }
320
329
  async function onSendEmail() {
321
- const item = items[index.value];
322
- smallAttachment.value = getUrl(item);
323
- const info = await mediaService.getMediaInfo(item.dlnaParentId, item.dlnaid);
330
+ smallAttachment.value = getUrl(currentItem);
331
+ const info = await mediaService.getMediaInfo(currentItem.dlnaParentId, currentItem.dlnaid);
324
332
  if (!info)
325
- window.alert("Not found: " + item.url);
333
+ window.alert("Not found: " + currentItem.url);
326
334
  else {
327
335
  attachment.value = info.name;
328
336
  showSendMail.value = true;
@@ -339,26 +347,36 @@
339
347
  function onUpdate() { // Carousel updated
340
348
  showInfo.value = false;
341
349
  clearName();
342
- const item = items[index.value];
350
+ currentItem = items[index.value];
351
+ if (MediaHelper.mustRotate(currentItem)) {
352
+ currentWidth = currentItem.height;
353
+ currentHeight = currentItem.width;
354
+ }
355
+ else {
356
+ currentWidth = currentItem.width;
357
+ currentHeight = currentItem.height;
358
+ }
359
+ const folder = album ? album : mediaService.getFolder(currentItem.dlnaParentId);
360
+ origUrl = mediaService.getPhotoUrl(folder.url, currentItem.url, '0x0x0');
343
361
  if (mediaAppConfig.useImagePreload) {
344
- if (index.value != nextIndex && !item.blob) {
345
- createBlob(item);
362
+ if (index.value != nextIndex && !currentItem.blob) {
363
+ createBlob(currentItem);
346
364
  }
347
365
  }
348
366
  else {
349
- item.blob = getUrl(item);
367
+ currentItem.blob = getUrl(currentItem);
350
368
  }
351
369
  if (playerService.currentPlayer) {
352
370
  const request = {
353
371
  playerName: playerService.currentPlayer.playerName,
354
- folderId: item.dlnaParentId,
355
- mediaId: item.dlnaid,
372
+ folderId: currentItem.dlnaParentId,
373
+ mediaId: currentItem.dlnaid,
356
374
  trackNo: -1,
357
375
  withStreamTitle: false
358
376
  };
359
377
  const rc = playerService!.play(request).then(() => { });
360
378
  }
361
- nameTimer = window.setTimeout(() => { onLoad(item); }, 200);
379
+ nameTimer = window.setTimeout(() => { onLoad(currentItem); }, 200);
362
380
  }
363
381
  function onLoad(item: IPictureFile) { // img loaded or cached
364
382
  if (mediaAppConfig.useImagePreload) {
@@ -391,8 +409,11 @@
391
409
  </script>
392
410
 
393
411
  <template>
394
- <v-container fluid :style="heightStyle" class="bg-grey-darken-3 pa-0">
395
- <v-carousel ref="carousel" hide-delimiters :show-arrows="false" v-model="index"
412
+ <v-container ref="carousel" fluid :style="heightStyle" class="bg-grey-darken-3 pa-0">
413
+ <image-zoomer v-if="appState.fullScreen.value" :contentW="currentWidth" :contentH="currentHeight" >
414
+ <img :src="origUrl" :width="currentWidth" :height="currentHeight" draggable="false">
415
+ </image-zoomer>
416
+ <v-carousel v-else hide-delimiters :show-arrows="false" v-model="index"
396
417
  :width="width" :height="height" :cycle="cycle" :interval="interval"
397
418
  @click="onClick" @mousedown="onMouseDown" @update:modelValue="onUpdate">
398
419
  <v-carousel-item v-for="item in items" :key="item.dlnaid" :src="item.blob" :alt="item.name" :width="getWidth(item)" :height="getHeight(item)"
@@ -254,8 +254,8 @@
254
254
  </v-card-actions>
255
255
  </v-card>
256
256
  <file-upload v-if="uploadVisible" accept="image/jpeg"></file-upload>
257
- <v-card :height="listHeight" class="overflow-y-auto bg-media" >
258
- <v-list v-if="showAlbums" class="bg-media">
257
+ <v-card :height="listHeight" density="compact" class="overflow-y-auto bg-media" >
258
+ <v-list v-if="showAlbums" density="compact" class="bg-media">
259
259
  <v-list-item v-for="item in items" :key="item.dlnaid" :title="item.info" density="compact" @click.stop="showFolder(item)">
260
260
  <template v-slot:append>
261
261
  <v-btn color="grey" variant="tonal" icon="$share" @click.stop.prevent="onShare(item)"></v-btn>
@@ -222,12 +222,12 @@
222
222
  </v-card-actions>
223
223
  </v-card>
224
224
  <file-upload v-if="uploadVisible" accept="image/jpeg"></file-upload>
225
- <v-card :max-height="listHeight" class="overflow-y-auto bg-media" ref="scrollElement" v-scroll.self="onScroll">
226
- <v-list v-if="!grouped">
225
+ <v-card :max-height="listHeight" class="overflow-y-auto bg-media" ref="scrollElement" density="compact" v-scroll.self="onScroll">
226
+ <v-list v-if="!grouped" density="compact" >
227
227
  <v-list-item v-for="item in items" :key="item.dlnaid" :title="item.info" density="compact" @click.stop="showFolder(item)">
228
228
  </v-list-item>
229
229
  </v-list>
230
- <v-list v-else v-model:opened="open">
230
+ <v-list v-else v-model:opened="open" density="compact" >
231
231
  <v-list-group v-for="item in items" :key="item.dlnaid" :value="item.name" @click.stop="listExpand(item)">
232
232
  <template v-slot:activator="{ props }">
233
233
  <v-list-item v-bind="props" :title="item.info" :value="item.info" density="compact"></v-list-item>
@@ -181,10 +181,10 @@
181
181
  </v-btn>
182
182
  </v-card-actions>
183
183
  </v-card>
184
- <v-card :max-height="listHeight" class="overflow-y-auto bg-media">
185
- <v-list lines="two" class="bg-media">
184
+ <v-card :max-height="listHeight" density="compact" class="overflow-y-auto bg-media">
185
+ <v-list lines="two" density="compact" class="bg-media">
186
186
  <v-list-item-group v-model="itemIndex" color="primary">
187
- <v-list-item v-for="item in items" :key="item.dlnaid" :title="item.title" :subtitle="item.subTitle" @click="listItem(item)">
187
+ <v-list-item v-for="item in items" :key="item.dlnaid" density="compact" :title="item.title" :subtitle="item.subTitle" @click="listItem(item)">
188
188
  <template v-if="item.url" v-slot:append>
189
189
  <v-btn :disabled="!item.description" color="grey" variant="tonal" icon="$info" @click.stop.prevent="onInfo(item)"></v-btn>
190
190
  <v-btn color="grey" variant="tonal" icon="$share" @click.stop.prevent="onShare(item)"></v-btn>
@@ -62,6 +62,8 @@
62
62
  </script>
63
63
 
64
64
  <template>
65
- <video v-if="ready" ref="videoElement" controls autoplay :src="url" :width="width" :height="height" @error="onError">
66
- </video>
65
+ <div class="bg-grey-darken-3">
66
+ <video v-if="ready" ref="videoElement" controls autoplay :src="url" :width="width" :height="height" @error="onError">
67
+ </video>
68
+ </div>
67
69
  </template>
@@ -308,11 +308,11 @@
308
308
  </v-card-actions>
309
309
  </v-card>
310
310
  <file-upload v-if="uploadVisible" accept="video/mp4"></file-upload>
311
- <v-card :max-height="listHeight" class="overflow-y-auto bg-media">
311
+ <v-card :max-height="listHeight" density="compact" class="overflow-y-auto bg-media">
312
312
  <v-card-subtitle v-if="testRunning">{{testText}}</v-card-subtitle>
313
- <v-list lines="two" class="bg-media">
313
+ <v-list lines="two" density="compact" class="bg-media">
314
314
  <v-list-item-group v-model="itemIndex">
315
- <v-list-item v-for="item in items" :key="item.dlnaid" :title="item.title" :subtitle="item.subTitle" @click="listItem(item)">
315
+ <v-list-item v-for="item in items" :key="item.dlnaid" density="compact" :title="item.title" :subtitle="item.subTitle" @click="listItem(item)">
316
316
  <template v-slot:prepend>
317
317
  <v-avatar rounded="0" size="x-large" v-if="item.thumbnailUrl">
318
318
  <v-img :src="item.thumbnailUrl"></v-img>