@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 +4 -4
- package/src/components/BookPlace.vue +4 -1
- package/src/components/BookSearchLine.vue +2 -2
- package/src/components/ImageZoomer.vue +282 -0
- package/src/views/BooksPage.vue +4 -4
- package/src/views/DocumentsPage.vue +10 -4
- package/src/views/MusicPage.vue +2 -2
- package/src/views/OnlineRadiosPage.vue +3 -3
- package/src/views/OnlineTVsPage.vue +3 -3
- package/src/views/PhotoAlbumPage.vue +41 -20
- package/src/views/PhotosGridPage.vue +2 -2
- package/src/views/PhotosPage.vue +3 -3
- package/src/views/RecordedVideosPage.vue +3 -3
- package/src/views/VideoPage.vue +4 -2
- package/src/views/VideosPage.vue +3 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@christianriedl/media",
|
|
3
|
-
"version": "1.0.
|
|
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.
|
|
22
|
-
"@christianriedl/rest": "^1.0.
|
|
23
|
-
"@christianriedl/epg": "^1.0.
|
|
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>
|
package/src/views/BooksPage.vue
CHANGED
|
@@ -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>
|
package/src/views/MusicPage.vue
CHANGED
|
@@ -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((
|
|
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
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
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
|
-
|
|
322
|
-
|
|
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: " +
|
|
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
|
-
|
|
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 && !
|
|
345
|
-
createBlob(
|
|
362
|
+
if (index.value != nextIndex && !currentItem.blob) {
|
|
363
|
+
createBlob(currentItem);
|
|
346
364
|
}
|
|
347
365
|
}
|
|
348
366
|
else {
|
|
349
|
-
|
|
367
|
+
currentItem.blob = getUrl(currentItem);
|
|
350
368
|
}
|
|
351
369
|
if (playerService.currentPlayer) {
|
|
352
370
|
const request = {
|
|
353
371
|
playerName: playerService.currentPlayer.playerName,
|
|
354
|
-
folderId:
|
|
355
|
-
mediaId:
|
|
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(
|
|
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-
|
|
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>
|
package/src/views/PhotosPage.vue
CHANGED
|
@@ -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>
|
package/src/views/VideoPage.vue
CHANGED
|
@@ -62,6 +62,8 @@
|
|
|
62
62
|
</script>
|
|
63
63
|
|
|
64
64
|
<template>
|
|
65
|
-
<
|
|
66
|
-
|
|
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>
|
package/src/views/VideosPage.vue
CHANGED
|
@@ -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>
|