@christianriedl/media 1.0.91 → 1.0.92

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/dist/iMedia.d.ts CHANGED
@@ -195,6 +195,7 @@ export interface IVideoInfo {
195
195
  }
196
196
  export interface IMediaInfo {
197
197
  durationSeconds: number;
198
+ nameDurationMS: number;
198
199
  videoStream: IVideoInfo;
199
200
  audioStreams: IAudioInfo[];
200
201
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@christianriedl/media",
3
- "version": "1.0.91",
3
+ "version": "1.0.92",
4
4
  "description": "RIC media interfaces",
5
5
 
6
6
  "main": "dist/index.js",
@@ -40,7 +40,7 @@
40
40
  <v-text-field name="directory" label="Name (Directory)" type="text" v-model="name" density="compact" hide-details></v-text-field>
41
41
  </v-col>
42
42
  <v-col cols="4">
43
- <v-file-input show-size clearable multiple v-model="files" :accept="props.accept" label="Select File"
43
+ <v-file-input show-size clearable multiple hide-details v-model="files" :accept="props.accept" label="Select File"
44
44
  prepend-icon="" append-icon="$upload" @click:append="onUpload">
45
45
  </v-file-input>
46
46
  </v-col>
@@ -33,6 +33,8 @@
33
33
  let gpsLat: number[];
34
34
  let mouseDownTime: number = 0;
35
35
  let mouseTimer: number = -1;
36
+ let nameTimer: number = -1;
37
+ const showName = ref(false);
36
38
 
37
39
  watch([appState.bodyHeight, appState.pageWidth], () => {
38
40
  width.value = appState.pageWidth.value;
@@ -68,6 +70,7 @@
68
70
  }
69
71
  }
70
72
  url.value = photos.Url;
73
+ onUpdate();
71
74
  }
72
75
  else {
73
76
  nextTick(() => router.back());
@@ -194,12 +197,12 @@
194
197
  case "ArrowLeft":
195
198
  if (index.value > 0)
196
199
  index.value--;
197
- infoDialog.value = false;
200
+ onUpdate();
198
201
  break;
199
202
  case "ArrowRight":
200
203
  if (index.value < items.length - 1)
201
204
  index.value++;
202
- infoDialog.value = false;
205
+ onUpdate();
203
206
  break;
204
207
  case "KeyI": // Info
205
208
  if (infoDialog.value) {
@@ -364,15 +367,38 @@
364
367
  document.body.removeChild(anchor);
365
368
  infoDialog.value = false;
366
369
  }
370
+ function clearName() { // also called by VImg::loadstart
371
+ showName.value = false;
372
+ if (nameTimer >= 0) {
373
+ window.clearTimeout(nameTimer);
374
+ nameTimer = -1;
375
+ }
376
+ }
377
+ function onUpdate() { // Carousel updated
378
+ infoDialog.value = false;
379
+ clearName();
380
+ const item = items[index.value];
381
+ nameTimer = window.setTimeout(() => { onLoad(item); }, 200);
382
+ }
383
+ function onLoad(item: IPictureFile) { // img loaded or cached
384
+ clearName();
385
+ const idx = item.Url.lastIndexOf('.');
386
+ const name = item.Url.substr(0, idx);
387
+ if (!name.includes(item.Name)) {
388
+ showName.value = true;
389
+ nameTimer = window.setTimeout(() => { clearName(); }, mediaAppConfig.nameDurationMS);
390
+ }
391
+ }
367
392
  </script>
368
393
 
369
394
  <template>
370
395
  <v-container fluid :style="heightStyle" class="bg-grey-darken-3 pa-0">
371
396
  <v-carousel ref="carousel" hide-delimiters :show-arrows="false" v-model="index"
372
397
  :width="width" :height="height" :cycle="cycle" :interval="interval"
373
- @click="onClick" @mousedown="onMouseDown" @update:modelValue="infoDialog = false">
374
- <v-carousel-item v-for="item in items" :key="item.DLNAID">
375
- <img :src="getUrl(item)" :alt="item.Name" :width="getWidth(item)" :height="getHeight(item)" />
398
+ @click="onClick" @mousedown="onMouseDown" @update:modelValue="onUpdate">
399
+ <v-carousel-item v-for="item in items" :key="item.DLNAID" :src="getUrl(item)" :alt="item.Name" :width="getWidth(item)" :height="getHeight(item)"
400
+ @load="onLoad(item)" @loadstart="clearName()">
401
+ <h2 v-if="showName" class="imgtext">{{item.Name}}</h2>
376
402
  </v-carousel-item>
377
403
  </v-carousel>
378
404
  <v-dialog v-model="infoDialog" :fullscreen="isMobile" >
@@ -441,4 +467,11 @@
441
467
  margin-left: auto;
442
468
  margin-right: auto;
443
469
  }
470
+ .imgtext {
471
+ position:absolute;
472
+ top:0px;
473
+ width:100%;
474
+ text-align:center;
475
+ color:magenta;
476
+ }
444
477
  </style>
@@ -17,7 +17,7 @@
17
17
  const router = useRouter();
18
18
  const open = ref<string[]>([]);
19
19
 
20
- const items: IMediaFolder[] = reactive([]);
20
+ const items = ref<IMediaFolder[]>([]);
21
21
  const selected = reactive<IPhotoSelection>(mediaService.photoSelection);
22
22
  const listHeight = ref(0);
23
23
  const listhead = ref<ComponentPublicInstance|null>(null);
@@ -181,7 +181,7 @@
181
181
  itemIndex.value = i;
182
182
  }
183
183
  }
184
- items.splice(0, items.length, ...folders);
184
+ items.value = folders;
185
185
  }
186
186
  function onScroll(ev: Event) {
187
187
  const scrollTop = (ev.target as Element).scrollTop;
@@ -0,0 +1,286 @@
1
+ <script setup lang="ts">
2
+ import { inject, ref, reactive, computed, onMounted, onUnmounted, nextTick, watch, StyleValue } from 'vue';
3
+ import { VDataTableVirtual } from 'vuetify/labs/VDataTable';
4
+ import type { ComponentPublicInstance } from 'vue';
5
+ import { useRouter } from 'vue-router';
6
+ import { IAppState, IAppConfig, EDevice, EBrowser, appStateSymbol, appConfigSymbol, Helper } from '@christianriedl/utils';
7
+ import { EItemType, EMediaType, IMediaFolder, IMediaItem, IPhotoSelection, MediaService, getMediaSymbol, IMediaAppConfig } from '@christianriedl/media';
8
+ import FileUpload from '../components/FileUpload.vue';
9
+ import PhotoDownload from '../components/PhotoDownload.vue';
10
+
11
+ const appState = inject(appStateSymbol)!;
12
+ const appConfig = inject(appConfigSymbol)!;
13
+ const mediaAppConfig = appConfig as unknown as IMediaAppConfig;
14
+ const getMediaService = inject(getMediaSymbol)!;
15
+ const mediaService = getMediaService();
16
+ const heightStyle = computed<StyleValue>(() => { return { height: appState.bodyHeight.value + 'px', overflowY: 'auto' } });
17
+ const isMobile = appState.isMobile && (appState.device != EDevice.iPad);
18
+ const router = useRouter();
19
+ const open = ref<string[]>([]);
20
+
21
+ const items = ref<IMediaFolder[]>([]);
22
+ const selected = reactive<IPhotoSelection>(mediaService.photoSelection);
23
+ const listHeight = ref(appState.bodyHeight.value);
24
+ const listhead = ref<ComponentPublicInstance|null>(null);
25
+ const table = ref<InstanceType<typeof VDataTableVirtual> | null>(null);
26
+ const uploadVisible = ref(false);
27
+
28
+ const roots: string[] = ['Jahr', 'Orte', 'Ereignisse', 'Personen'];
29
+ const criterias: string[] = ['ye', 'lo', 'ev', 'pe'];
30
+ const groupBy = ref<any>([{key: 'info', order: 'asc'}]);
31
+ const sortBy = ref<any>([{key: 'title', order: 'asc'}]);
32
+ const headers= [
33
+ { title: 'Title', key: 'title', align: 'start', sortable: false },
34
+ { title: 'Actions', key: 'actions', sortable: false }
35
+ ];
36
+ const grouped = ref(false);
37
+ const backVisible = ref(false);
38
+ const showDownload = ref(false);
39
+ const downloadFolder = ref<IMediaFolder | null>(null);
40
+ const selectedYear = ref<number | undefined>(undefined);
41
+ const selectedName = ref<string | undefined>(undefined);
42
+
43
+ window.addEventListener('popstate', onPopState);
44
+ function onPopState(event: any) {
45
+ if (event.state && event.state.noBackExitsApp && backVisible.value) {
46
+ event.preventDefault();
47
+ listBack();
48
+ }
49
+ }
50
+ onUnmounted(() => {
51
+ window.removeEventListener('popstate', onPopState);
52
+ while (window.history.state && window.history.state.noBackExitsApp)
53
+ window.history.back();
54
+ });
55
+ onMounted(async () => {
56
+ mediaService.log.trace('Photos created');
57
+ const rc = await mediaService.getLists(EMediaType.Picture);
58
+ let root;
59
+ if (!selected.selected.DLNAID) {
60
+ root = await mediaService.initializePhotos(stars(), selected.criteria);
61
+ }
62
+ else {
63
+ root = mediaService.folders[selected.selected.DLNAID];
64
+ }
65
+ const folders = setSelected(root);
66
+ if (selected.selectedAlbum)
67
+ selectedYear.value = selected.selectedAlbum.Year;
68
+ const expanded = selected.criteria == 'ye' || root.ItemType == EItemType.PictureCategory;
69
+ setGrouped(folders, expanded, selectedYear.value);
70
+ const scrollTop = appState.scrollPosition.value;
71
+ if (scrollTop > 0 && table.value) {
72
+ nextTick(() => {
73
+ // Scoll to this position
74
+ const el = table.value?._?.vnode?.el;
75
+ el?.scrollTo(0, scrollTop);
76
+ });
77
+ }
78
+ mediaService.log.trace(`Photos start with ${selected.selected.DLNAID} scrollTop: ${scrollTop}`);
79
+ })
80
+ watch(appState.bodyHeight, () => computeListHeight());
81
+ function computeListHeight() {
82
+ nextTick(() => {
83
+ let height = appState.bodyHeight.value;
84
+ if (listhead.value) {
85
+ height = height - (listhead.value.$el as HTMLElement).clientHeight;
86
+ }
87
+ listHeight.value = height;
88
+ })
89
+ }
90
+ function stars() {
91
+ return selected.rating ? (selected.rating == 1 ? '*' : '**') : 'all';
92
+ }
93
+ function showFolder(folder: IMediaFolder) {
94
+ const folders = setSelected(folder);
95
+ setGrouped(folders, true);
96
+ computeListHeight();
97
+ }
98
+ function listExpand(folder: IMediaFolder) {
99
+ if (folder.Year && folder.Year > 0)
100
+ selectedYear.value = folder.Year;
101
+ }
102
+ function showPictures(folder: IMediaFolder) {
103
+ selected.selectedAlbum = folder;
104
+ router.push({ path: 'photoalbum', query: { id: folder.DLNAID } });
105
+ }
106
+ function onClick(ev: Event, row: any) {
107
+ const folder = row.item.raw as IMediaFolder;
108
+ if (folder.info)
109
+ showPictures(folder);
110
+ else
111
+ showFolder(folder);
112
+ }
113
+ function listBack() {
114
+ const parent = mediaService.folders[selected.selected.DLNAParentID];
115
+ const index = parent.Folders.indexOf(selected.selected);
116
+ const folders = setSelected(parent);
117
+ setGrouped(folders, false);
118
+ }
119
+ function onLightbox(folder: IMediaFolder) {
120
+ selected.selectedAlbum = folder;
121
+ router.push({ path: 'photothumbnails', query: { id: folder.DLNAID } });
122
+
123
+ }
124
+ function onDownload(folder: IMediaFolder) {
125
+ downloadFolder.value = folder;
126
+ showDownload.value = true;
127
+ }
128
+ async function onShare(folder: IMediaFolder) {
129
+ const guid = Helper.generateUUID();
130
+ const url = `https://www.christian-riedl.com/photos?id=${guid}`;
131
+ try {
132
+ await window.navigator.clipboard.writeText(url);
133
+ const id = await mediaService.addPhotoService(guid, appConfig.user, folder.DLNAID, mediaAppConfig.linkDaysValid);
134
+ if (id)
135
+ window.alert(`Created and Pasted - id : ${id} !`);
136
+ else
137
+ window.alert(`Creation failed, please repeat !`);
138
+ }
139
+ catch (err) {
140
+ window.alert(`Not Pasted : ${err} !`);
141
+ }
142
+ }
143
+ function onCloseDownload() {
144
+ downloadFolder.value = null;
145
+ showDownload.value = false;
146
+ }
147
+ function onRootChange(root: string) {
148
+ const idx = roots.indexOf(selected.root);
149
+ selected.criteria = criterias[idx];
150
+ selectedName.value = undefined;
151
+ initialize().then((v) => { });;
152
+ }
153
+ function onRating(value: number | string) {
154
+ selected.rating = Number(value);
155
+ initialize().then((v) => { });;
156
+ }
157
+ async function initialize(): Promise<boolean> {
158
+ let root = await mediaService.initializePhotos(stars(), selected.criteria);
159
+ let expanded = selected.criteria == 'ye';
160
+ if (selected.criteria != 'ye' && selectedName.value) {
161
+ for (let i = 0; i < root.Folders.length; i++) {
162
+ if (root.Folders[i].Name == selectedName.value) {
163
+ root = root.Folders[i];
164
+ expanded = true;
165
+ break;
166
+ }
167
+ }
168
+ }
169
+ const folders = setSelected(root);
170
+ setGrouped(folders, expanded, selectedYear.value);
171
+ return true;
172
+ }
173
+ function setSelected(folder: IMediaFolder) : IMediaFolder[] {
174
+ selected.selected = folder;
175
+ if (folder.Year && folder.Year > 0)
176
+ selectedYear.value = folder.Year;
177
+ selectedName.value = folder.Name;
178
+ backVisible.value = selected.selected.ItemType != EItemType.PictureGenreType;
179
+
180
+ // calculate list height
181
+ computeListHeight();
182
+ return folder.Folders;
183
+ }
184
+ function setGrouped(folders: IMediaFolder[], expand: boolean, year?: number) {
185
+ grouped.value = expand;
186
+ groupBy.value = expand ? [{key: 'info', order: 'asc'}] : [];
187
+ const expanded = (expand && folders.length <= 6) ? true : false;
188
+ const list: IMediaFolder[] = [];
189
+ for (let i = 0; i < folders.length; i++) {
190
+ let folder = folders[i];
191
+ if (expand) {
192
+ if (year && year == folder.Year) {
193
+ open.value.push(year.toString());
194
+ }
195
+ for (let j = 0; j < folder.Folders.length; j++) {
196
+ const item = folder.Folders[j];
197
+ item.info = folder.title;
198
+ list.push(item);
199
+ }
200
+ }
201
+ else {
202
+ folder.info = undefined;
203
+ list.push(folder);
204
+ }
205
+ }
206
+ items.value = list;
207
+ }
208
+ function onScroll(ev: Event) {
209
+ const scrollTop = (ev.target as Element).scrollTop;
210
+ appState.scrollPosition.value = scrollTop;
211
+ }
212
+ function setGroupFunc(toggle:any) : string {
213
+ return "";
214
+ }
215
+ </script>
216
+ <template>
217
+ <v-card ref="listhead" class="bg-media_head">
218
+ <v-card-title>{{selected.selected.Name}}</v-card-title>
219
+ <v-card-actions>
220
+ <v-btn v-if="backVisible" @click="listBack">
221
+ <v-icon size="large" icon="$back" />
222
+ </v-btn>
223
+ <v-rating clearable length="2" v-model="selected.rating" @update:modelValue="onRating" />
224
+ <v-select v-model="selected.root"
225
+ :items="roots"
226
+ persistent-hint
227
+ @update:modelValue="onRootChange"
228
+ hide-details single-line>
229
+ </v-select>
230
+ <v-btn v-if="!isMobile" @click="uploadVisible = !uploadVisible">
231
+ UPLOAD
232
+ <v-icon icon="$upload" />
233
+ </v-btn>
234
+ </v-card-actions>
235
+ </v-card>
236
+ <file-upload v-if="uploadVisible" accept="image/jpeg"></file-upload>
237
+ <v-data-table-virtual ref="table" :group-by="groupBy" :sort-by="sortBy" :items="items" :headers="headers" class="elevation-1" item-value="title" :height="listHeight"
238
+ @click:row="onClick" v-scroll.self="onScroll">
239
+ <template v-slot:top="{ toggleGroup, isGroupOpen }">
240
+ <p style="display:none">{{setGroupFunc(toggleGroup)}}</p>
241
+ </template>
242
+ <template v-slot:item.actions="{ item }">
243
+ <div v-if="item.raw.info">
244
+ <v-icon size="small" icon="$share" @click="onShare(item.raw)"></v-icon>
245
+ <v-icon size="small" icon="$download" @click="onDownload(item.raw)"></v-icon>
246
+ <v-icon size="small" icon="$grid" @click="onLightbox(item.raw)"></v-icon>
247
+ </div>
248
+ </template>
249
+ </v-data-table-virtual>
250
+ <!--
251
+ <v-card :max-height="listHeight" class="overflow-y-auto bg-media" ref="scrollElement" v-scroll.self="onScroll">
252
+ <v-data-table-virtual ref="table" :group-by="groupBy" :items="items" :headers="headers" class="elevation-1" item-value="title" :height="listHeight" @click:row="onClick">
253
+ <template v-slot:item.actions="{ item }">
254
+ <v-icon size="small" icon="$share" @click="onShare(item.raw)"></v-icon>
255
+ <v-icon size="small" icon="$download" @click="onDownload(item.raw)"></v-icon>
256
+ <v-icon size="small" icon="$grid" @click="onLightbox(item.raw)"></v-icon>
257
+ </template>
258
+ </v-data-table-virtual>
259
+ <!-
260
+ <v-list v-if="!grouped">
261
+ <v-list-item v-for="item in items" :key="item.DLNAID" :title="item.info" density="compact" @click.stop="showFolder(item)">
262
+ </v-list-item>
263
+ </v-list>
264
+ <v-list v-else v-model:opened="open">
265
+ <v-list-group v-for="item in items" :key="item.DLNAID" :value="item.Name" @click.stop="listExpand(item)">
266
+ <template v-slot:activator="{ props }">
267
+ <v-list-item v-bind="props" :title="item.info" :value="item.info" density="compact"></v-list-item>
268
+ </template>
269
+ <v-list-item v-for="subItem in item.Folders" :key="subItem.DLNAID" :title="subItem.info" density="compact" @click.stop="showPictures(subItem)">
270
+ <template v-slot:append>
271
+ <v-btn color="grey" variant="tonal" icon="$share" @click.stop.prevent="onShare(subItem)"></v-btn>
272
+ <v-btn color="grey" variant="tonal" icon="$download" @click.stop.prevent="onDownload(subItem)"></v-btn>
273
+ <v-btn color="grey" variant="tonal" icon="$grid" @click.stop.prevent="onLightbox(subItem)"></v-btn>
274
+ </template>
275
+ </v-list-item>
276
+ </v-list-group>
277
+ </v-list>
278
+ ->
279
+ </v-card>
280
+ -->
281
+
282
+ <v-dialog v-model="showDownload">
283
+ <photo-download :folder="downloadFolder!" v-click-outside="onCloseDownload" @close="onCloseDownload"></photo-download>
284
+ </v-dialog>
285
+ </template>
286
+