@christianriedl/media 1.0.282 → 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.282",
3
+ "version": "1.0.283",
4
4
  "description": "RIC media interfaces",
5
5
 
6
6
  "main": "dist/index.js",
@@ -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>
@@ -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)"