@christianriedl/media 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -0
- package/dist/iMedia.d.ts +156 -0
- package/dist/iMedia.js +87 -0
- package/dist/iMedia.js.map +1 -0
- package/dist/iMediaInstance.d.ts +49 -0
- package/dist/iMediaInstance.js +2 -0
- package/dist/iMediaInstance.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -0
- package/dist/mediaInstanceService.d.ts +18 -0
- package/dist/mediaInstanceService.js +100 -0
- package/dist/mediaInstanceService.js.map +1 -0
- package/dist/mediaService.d.ts +46 -0
- package/dist/mediaService.js +355 -0
- package/dist/mediaService.js.map +1 -0
- package/dist/playerService.d.ts +31 -0
- package/dist/playerService.js +35 -0
- package/dist/playerService.js.map +1 -0
- package/package.json +29 -0
- package/src/components/RootInformationLine.vue +34 -0
- package/src/components/RootStatisticsLine.vue +21 -0
- package/src/views/MediaInstancePage.vue +149 -0
- package/src/views/MusicPage.vue +234 -0
- package/src/views/PhotoAlbumPage.vue +255 -0
- package/src/views/PhotosPage.vue +199 -0
- package/src/views/RecordedVideosPage.vue +98 -0
- package/src/views/ThumbnailsPage.vue +82 -0
- package/src/views/VideoPage.vue +46 -0
- package/src/views/VideosPage.vue +230 -0
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { inject, ref, reactive, computed, onMounted, onUnmounted, nextTick, watch } from 'vue';
|
|
3
|
+
import { VNestedSymbol, emptyNested } from 'vuetify';
|
|
4
|
+
import { useRouter } from 'vue-router';
|
|
5
|
+
import { IAppState } from '@christianriedl/utils';
|
|
6
|
+
import { EItemType, EMediaType, IMediaFolder, IMediaItem, IPhotoSelection, MediaService } from '@christianriedl/media';
|
|
7
|
+
|
|
8
|
+
const appState = inject<IAppState>('appstate')!;
|
|
9
|
+
const getMediaService = inject<() => MediaService>('get-media')!;
|
|
10
|
+
const mediaService = getMediaService();
|
|
11
|
+
const heightStyle = computed(() => { return { height: appState.bodyHeight.value + 'px', overflowY: 'auto' } });
|
|
12
|
+
const isMobile = appState.isMobile;
|
|
13
|
+
const router = useRouter();
|
|
14
|
+
const open = ref<string[]>([]);
|
|
15
|
+
|
|
16
|
+
const items: IMediaItem[] = reactive([]);
|
|
17
|
+
const selected = reactive<IPhotoSelection>(mediaService.photoSelection);
|
|
18
|
+
const listHeight = ref(0);
|
|
19
|
+
const listhead = ref<any>(null);
|
|
20
|
+
|
|
21
|
+
const roots: string[] = ['Jahr', 'Orte', 'Ereignisse', 'Personen'];
|
|
22
|
+
const criterias: string[] = ['ye', 'lo', 'ev', 'pe'];
|
|
23
|
+
const grouped = ref(false);
|
|
24
|
+
const backVisible = ref(false);
|
|
25
|
+
const itemIndex = ref(0);
|
|
26
|
+
const selectedYear = ref<number | undefined>(undefined);
|
|
27
|
+
const selectedName = ref<string | undefined>(undefined);
|
|
28
|
+
|
|
29
|
+
window.addEventListener('popstate', onPopState);
|
|
30
|
+
function onPopState(event: any) {
|
|
31
|
+
if (event.state && event.state.noBackExitsApp && backVisible.value) {
|
|
32
|
+
event.preventDefault();
|
|
33
|
+
listBack();
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
onUnmounted(() => {
|
|
37
|
+
window.removeEventListener('popstate', onPopState);
|
|
38
|
+
while (window.history.state && window.history.state.noBackExitsApp)
|
|
39
|
+
window.history.back();
|
|
40
|
+
});
|
|
41
|
+
onMounted(async () => {
|
|
42
|
+
mediaService.log.trace('Photos created');
|
|
43
|
+
const rc = await mediaService.getLists(EMediaType.Picture);
|
|
44
|
+
let root;
|
|
45
|
+
if (!selected.selected.DLNAID) {
|
|
46
|
+
root = await mediaService.initializePhotos(stars(), selected.criteria);
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
root = mediaService.folders[selected.selected.DLNAID];
|
|
50
|
+
}
|
|
51
|
+
const folders = setSelected(root);
|
|
52
|
+
if (selected.selectedAlbum)
|
|
53
|
+
selectedYear.value = selected.selectedAlbum.Year;
|
|
54
|
+
const expanded = selected.criteria == 'ye' || root.ItemType == EItemType.PictureCategory;
|
|
55
|
+
setGrouped(folders, expanded, selectedYear.value);
|
|
56
|
+
mediaService.log.trace('Photos start with ' + selected.selected.DLNAID);
|
|
57
|
+
})
|
|
58
|
+
watch(appState.bodyHeight, () => computeListHeight());
|
|
59
|
+
function computeListHeight() {
|
|
60
|
+
nextTick(() => {
|
|
61
|
+
let height = appState.bodyHeight.value;
|
|
62
|
+
if (listhead.value && listhead.value._.vnode && listhead.value._.vnode.el) {
|
|
63
|
+
height = height - listhead.value._.vnode.el.clientHeight;
|
|
64
|
+
}
|
|
65
|
+
listHeight.value = height;
|
|
66
|
+
})
|
|
67
|
+
}
|
|
68
|
+
function stars() {
|
|
69
|
+
return selected.rating ? (selected.rating == 1 ? '*' : '**') : 'all';
|
|
70
|
+
}
|
|
71
|
+
function listItem(item: IMediaFolder) {
|
|
72
|
+
const folders = setSelected(item);
|
|
73
|
+
setGrouped(folders, true);
|
|
74
|
+
computeListHeight();
|
|
75
|
+
}
|
|
76
|
+
function listExpand(item: IMediaFolder) {
|
|
77
|
+
if (item.Year && item.Year > 0)
|
|
78
|
+
selectedYear.value = item.Year;
|
|
79
|
+
}
|
|
80
|
+
function onOpen(arg: unknown) {
|
|
81
|
+
console.log('onOpen');
|
|
82
|
+
}
|
|
83
|
+
function listSubItem(item: IMediaFolder) {
|
|
84
|
+
selected.selectedAlbum = item;
|
|
85
|
+
router.push({ path: 'photoalbum', query: { id: item.DLNAID } });
|
|
86
|
+
}
|
|
87
|
+
function listBack() {
|
|
88
|
+
const parent = mediaService.folders[selected.selected.DLNAParentID];
|
|
89
|
+
const index = parent.Folders.indexOf(selected.selected);
|
|
90
|
+
const folders = setSelected(parent);
|
|
91
|
+
if (index)
|
|
92
|
+
itemIndex.value = index;
|
|
93
|
+
setGrouped(folders, false);
|
|
94
|
+
}
|
|
95
|
+
function onLightbox(item: IMediaFolder) {
|
|
96
|
+
selected.selectedAlbum = item;
|
|
97
|
+
router.push({ path: 'photothumbnails', query: { id: item.DLNAID } });
|
|
98
|
+
|
|
99
|
+
}
|
|
100
|
+
async function onRootChange(root: string): Promise<boolean> {
|
|
101
|
+
const idx = roots.indexOf(selected.root);
|
|
102
|
+
selected.criteria = criterias[idx];
|
|
103
|
+
selectedName.value = undefined;
|
|
104
|
+
return await initialize();
|
|
105
|
+
}
|
|
106
|
+
async function onRating(value: number): Promise<boolean> {
|
|
107
|
+
selected.rating = value;
|
|
108
|
+
return await initialize();
|
|
109
|
+
}
|
|
110
|
+
async function initialize(): Promise<boolean> {
|
|
111
|
+
let root = await mediaService.initializePhotos(stars(), selected.criteria);
|
|
112
|
+
let expanded = selected.criteria == 'ye';
|
|
113
|
+
if (selected.criteria != 'ye' && selectedName.value) {
|
|
114
|
+
for (let i = 0; i < root.Folders.length; i++) {
|
|
115
|
+
if (root.Folders[i].Name == selectedName.value) {
|
|
116
|
+
root = root.Folders[i];
|
|
117
|
+
expanded = true;
|
|
118
|
+
break;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
const folders = setSelected(root);
|
|
123
|
+
setGrouped(folders, expanded, selectedYear.value);
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
126
|
+
function setSelected(folder: IMediaFolder) : IMediaFolder[] {
|
|
127
|
+
selected.selected = folder;
|
|
128
|
+
if (folder.Year && folder.Year > 0)
|
|
129
|
+
selectedYear.value = folder.Year;
|
|
130
|
+
selectedName.value = folder.Name;
|
|
131
|
+
//items.splice(0, items.length, ...folder.Folders);
|
|
132
|
+
backVisible.value = selected.selected.ItemType != EItemType.PictureGenreType;
|
|
133
|
+
|
|
134
|
+
// calculate list height
|
|
135
|
+
computeListHeight();
|
|
136
|
+
return folder.Folders;
|
|
137
|
+
}
|
|
138
|
+
function setGrouped(folders: IMediaFolder[], expand: boolean, year?: number) {
|
|
139
|
+
grouped.value = expand;
|
|
140
|
+
const expanded = (expand && folders.length <= 6) ? true : false;
|
|
141
|
+
for (let i = 0; i < folders.length; i++) {
|
|
142
|
+
if (year && year == folders[i].Year) {
|
|
143
|
+
if (expand)
|
|
144
|
+
open.value.push(year.toString());
|
|
145
|
+
else
|
|
146
|
+
itemIndex.value = i;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
items.splice(0, items.length, ...folders);
|
|
150
|
+
}
|
|
151
|
+
function itemText(item: IMediaFolder) : string {
|
|
152
|
+
return `${item.Name} (${item.ChildCount})`
|
|
153
|
+
}
|
|
154
|
+
</script>
|
|
155
|
+
<template>
|
|
156
|
+
<v-card ref="listhead" class="bg-media">
|
|
157
|
+
<v-card-title>{{selected.selected.Name}}</v-card-title>
|
|
158
|
+
<v-card-actions>
|
|
159
|
+
<v-btn v-if="backVisible" @click="listBack">
|
|
160
|
+
<v-icon large>{{$vuetify.icons.values.back}}</v-icon>Back
|
|
161
|
+
</v-btn>
|
|
162
|
+
<v-rating clearable length="2" v-model="selected.rating" @update:modelValue="onRating" />
|
|
163
|
+
<v-select v-model="selected.root" c
|
|
164
|
+
:items="roots"
|
|
165
|
+
persistent-hint
|
|
166
|
+
@update:modelValue="onRootChange"
|
|
167
|
+
dense solo hide-details single-line>
|
|
168
|
+
</v-select>
|
|
169
|
+
</v-card-actions>
|
|
170
|
+
</v-card>
|
|
171
|
+
<v-card :max-height="listHeight" class="overflow-y-auto">
|
|
172
|
+
<v-list v-if="!grouped" class="bg-media" >
|
|
173
|
+
<v-list-item-group v-model="itemIndex" color="primary">
|
|
174
|
+
<v-list-item v-for="item in items" :key="item.DLNAID" :title="itemText(item)" :value="itemText(item)" density="compact" @click.stop="listItem(item)">
|
|
175
|
+
<template v-slot:append>
|
|
176
|
+
<v-list-item-avatar right>
|
|
177
|
+
<v-btn variant="text" color="grey" icon="mdi-grid" @click.stop.prevent="onLightbox(item)"></v-btn>
|
|
178
|
+
</v-list-item-avatar>
|
|
179
|
+
</template>
|
|
180
|
+
</v-list-item>
|
|
181
|
+
</v-list-item-group>
|
|
182
|
+
</v-list>
|
|
183
|
+
<v-list v-else class="bg-media" v-model:opened="open" ref="listRef" @click:open="onOpen">
|
|
184
|
+
<v-list-group v-for="item in items" :key="item.DLNAID" :value="item.Name" @click.stop="listExpand(item)">
|
|
185
|
+
<template v-slot:activator="{ props }">
|
|
186
|
+
<v-list-item v-bind="props" :title="itemText(item)" :value="itemText(item)" density="compact"></v-list-item>
|
|
187
|
+
</template>
|
|
188
|
+
<v-list-item v-for="subItem in item.Folders" :key="subItem.DLNAID" :title="itemText(subItem)" :value="itemText(subItem)" density="compact" @click.stop="listSubItem(subItem)">
|
|
189
|
+
<template v-slot:append>
|
|
190
|
+
<v-list-item-avatar right>
|
|
191
|
+
<v-btn variant="text" color="grey" icon="mdi-grid" @click.stop.prevent="onLightbox(subItem)"></v-btn>
|
|
192
|
+
</v-list-item-avatar>
|
|
193
|
+
</template>
|
|
194
|
+
</v-list-item>
|
|
195
|
+
</v-list-group>
|
|
196
|
+
</v-list>
|
|
197
|
+
</v-card>
|
|
198
|
+
</template>
|
|
199
|
+
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { inject, ref, reactive, computed, onMounted, onUnmounted, nextTick, watch } from 'vue';
|
|
3
|
+
import { useRouter } from 'vue-router';
|
|
4
|
+
import { IAppState } from '@christianriedl/utils';
|
|
5
|
+
import { EItemType, IMediaFolder, IMediaItem, MediaService } from '@christianriedl/media';
|
|
6
|
+
|
|
7
|
+
const appState = inject<IAppState>('appstate')!;
|
|
8
|
+
const getMediaService = inject<() => MediaService>('get-media')!;
|
|
9
|
+
const mediaService = getMediaService();
|
|
10
|
+
const heightStyle = computed(() => { return { height: appState.bodyHeight.value + 'px', overflowY: 'auto' } });
|
|
11
|
+
const isMobile = appState.isMobile;
|
|
12
|
+
const router = useRouter();
|
|
13
|
+
|
|
14
|
+
const items: IMediaItem[] = reactive([]);
|
|
15
|
+
const selected = ref<IMediaItem>({ Name: 'Root', ItemType: EItemType.Root } as IMediaFolder);
|
|
16
|
+
const listHeight = ref(0);
|
|
17
|
+
const listhead = ref<any>(null);
|
|
18
|
+
|
|
19
|
+
const backVisible = computed(() => selected.value.ItemType != EItemType.VideoRoot);
|
|
20
|
+
const playVisible = computed(() => selected.value.ItemType == EItemType.VideoItem || selected.value.ItemType == EItemType.VideoBroadcast);
|
|
21
|
+
|
|
22
|
+
onMounted(async () => {
|
|
23
|
+
mediaService.log.info('RecordedTVs created');
|
|
24
|
+
const root = await mediaService.initializeRecordedTVs();
|
|
25
|
+
selected.value = root;
|
|
26
|
+
items.splice(0, items.length, ...root.Files);
|
|
27
|
+
computeListHeight();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
watch(appState.bodyHeight, () => computeListHeight());
|
|
31
|
+
|
|
32
|
+
function computeListHeight() {
|
|
33
|
+
nextTick(() => {
|
|
34
|
+
let height = appState.bodyHeight.value;
|
|
35
|
+
if (listhead.value && listhead.value._.vnode && listhead.value._.vnode.el) {
|
|
36
|
+
height = height - listhead.value._.vnode.el.clientHeight;
|
|
37
|
+
}
|
|
38
|
+
listHeight.value = height;
|
|
39
|
+
})
|
|
40
|
+
}
|
|
41
|
+
function play() {
|
|
42
|
+
router.push({ path: 'video', query: { url: selected.value.Url } });
|
|
43
|
+
}
|
|
44
|
+
function listItem(item: IMediaItem) {
|
|
45
|
+
if (item.ItemType == EItemType.VideoItem || item.ItemType == EItemType.VideoBroadcast) {
|
|
46
|
+
selected.value = item;
|
|
47
|
+
computeListHeight();
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
function listBack() {
|
|
52
|
+
router.back();
|
|
53
|
+
}
|
|
54
|
+
</script>
|
|
55
|
+
|
|
56
|
+
<template>
|
|
57
|
+
<v-container>
|
|
58
|
+
<v-col>
|
|
59
|
+
<v-card ref="listhead" class="bg-media">
|
|
60
|
+
<v-list-item v-if="selected" three-line>
|
|
61
|
+
<v-list-item-content>
|
|
62
|
+
<v-list-item-title>{{selected.title}}</v-list-item-title>
|
|
63
|
+
<v-list-item-subtitle>{{selected.subTitle}}</v-list-item-subtitle>
|
|
64
|
+
<v-list-item-subtitle>{{selected.info}}</v-list-item-subtitle>
|
|
65
|
+
</v-list-item-content>
|
|
66
|
+
</v-list-item>
|
|
67
|
+
<v-card-actions>
|
|
68
|
+
<v-btn v-if="backVisible" @click="listBack">
|
|
69
|
+
<v-icon>{{$vuetify.icons.values.back}}</v-icon>
|
|
70
|
+
</v-btn>
|
|
71
|
+
<v-btn v-if="playVisible" @click.stop="play">
|
|
72
|
+
<v-icon>{{$vuetify.icons.values.play }}</v-icon>
|
|
73
|
+
</v-btn>
|
|
74
|
+
</v-card-actions>
|
|
75
|
+
</v-card>
|
|
76
|
+
<v-card :max-height="listHeight" class="overflow-y-auto bg-media">
|
|
77
|
+
<v-list two-line class="bg-media">
|
|
78
|
+
<v-list-item-group v-model="itemIndex" color="primary">
|
|
79
|
+
<v-list-item v-for="item in items" :key="item.DLNAID" :title="item.title" :subtitle="item.subTitle" @click="listItem(item)">
|
|
80
|
+
<!--
|
|
81
|
+
<v-list-item-content>
|
|
82
|
+
<v-list-item-title v-html="item.title"></v-list-item-title>
|
|
83
|
+
<v-list-item-subtitle v-html="item.subTitle"></v-list-item-subtitle>
|
|
84
|
+
</v-list-item-content>
|
|
85
|
+
-->
|
|
86
|
+
<template v-slot:append>
|
|
87
|
+
<v-list-item-avatar right>
|
|
88
|
+
<v-btn variant="text" color="grey lighten-1" icon="mdi-information"></v-btn>
|
|
89
|
+
</v-list-item-avatar>
|
|
90
|
+
</template>
|
|
91
|
+
</v-list-item>
|
|
92
|
+
</v-list-item-group>
|
|
93
|
+
</v-list>
|
|
94
|
+
</v-card>
|
|
95
|
+
</v-col>
|
|
96
|
+
</v-container>
|
|
97
|
+
</template>
|
|
98
|
+
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { inject, ref, computed, onMounted } from 'vue';
|
|
3
|
+
import { useRouter, useRoute } from 'vue-router';
|
|
4
|
+
import { IAppState } from '@christianriedl/utils';
|
|
5
|
+
import { EOrientation, EMediaType, IMediaFolder, IPictureFile, MediaService } from '@christianriedl/media';
|
|
6
|
+
|
|
7
|
+
const appState = inject<IAppState>('appstate')!;
|
|
8
|
+
const getMediaService = inject<() => MediaService>('get-media')!;
|
|
9
|
+
const mediaService = getMediaService();
|
|
10
|
+
const heightStyle = computed(() => { return { height: appState.bodyHeight.value + 'px', overflowY: 'auto' } });
|
|
11
|
+
const isMobile = appState.isMobile;
|
|
12
|
+
const router = useRouter();
|
|
13
|
+
const route = useRoute();
|
|
14
|
+
const folderId = decodeURIComponent(route.query!.id!.toString());
|
|
15
|
+
const photos = ref<IPictureFile[]>([]);
|
|
16
|
+
const minImageWidth = 100;
|
|
17
|
+
const numImagesPerRow = Math.min(window.innerWidth / minImageWidth, 12);
|
|
18
|
+
const numColsPerImage = ref(Math.floor(12 / numImagesPerRow));
|
|
19
|
+
const width = ref(Math.floor(numColsPerImage.value * window.innerWidth / 12));
|
|
20
|
+
const height = ref(Math.floor(width.value / 3 * 2));
|
|
21
|
+
let folder: IMediaFolder;
|
|
22
|
+
//const matrix = ref<IPictureFile[][]>([]);
|
|
23
|
+
//let numRows = 0;
|
|
24
|
+
|
|
25
|
+
onMounted(async () => {
|
|
26
|
+
folder = mediaService.folders[folderId];
|
|
27
|
+
if (!folder) {
|
|
28
|
+
const split = folderId.split(/[.|]/);
|
|
29
|
+
await mediaService.initializePhotos(split[1], split[2]);
|
|
30
|
+
folder = mediaService.folders[folderId];
|
|
31
|
+
if (folder) {
|
|
32
|
+
if (!mediaService.medialists["Picture.Event"])
|
|
33
|
+
await mediaService.getLists(EMediaType.Picture);
|
|
34
|
+
}
|
|
35
|
+
else
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
folder = await mediaService.getPhotos(folder);
|
|
39
|
+
photos.value.splice(0, photos.value.length, ...folder.Files as IPictureFile[]);
|
|
40
|
+
|
|
41
|
+
/*
|
|
42
|
+
numRows = Math.floor((photos.length + 11) / 12);
|
|
43
|
+
let count = 0;
|
|
44
|
+
for (let i = 0; i < numRows; i++) {
|
|
45
|
+
matrix.value.push([]);
|
|
46
|
+
for (let j = 0; j < 12; j++) {
|
|
47
|
+
if (count >= photos.length)
|
|
48
|
+
break;
|
|
49
|
+
matrix.value[i].push(photos[count++] as IPictureFile);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
*/
|
|
53
|
+
console.log(photos.value.length);
|
|
54
|
+
})
|
|
55
|
+
function onClick(item: IPictureFile) {
|
|
56
|
+
router.push({ path: 'photoalbum', query: { id: folder.DLNAID, start: item.DLNAID } });
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function getThumbnailUrl(image: IPictureFile) : string {
|
|
60
|
+
let url = `https://www.christian-riedl.com/media/apiimage/Thumbnail?image=${folder.Url}${image.Url}&tresX=${image.ThumbWidth}&tresY=${image.ThumbHeight}`;
|
|
61
|
+
if (image.Orientation && (image.Orientation === EOrientation.BottomLeft || image.Orientation === EOrientation.TopRight))
|
|
62
|
+
url += '&orientation=' + Number(image.Orientation);
|
|
63
|
+
return encodeURI(url);
|
|
64
|
+
}
|
|
65
|
+
</script>
|
|
66
|
+
|
|
67
|
+
<template>
|
|
68
|
+
<v-container fluid :style="heightStyle">
|
|
69
|
+
<!--
|
|
70
|
+
<v-row dense align="center" v-for="(row,irow) in matrix" :key="irow" >
|
|
71
|
+
<v-col cols="1" align="center" v-for="(image, icol) in row" :key="irow * 12 + icol">
|
|
72
|
+
<img :src="getThumbnailUrl(image)" :height="height"/>
|
|
73
|
+
</v-col>
|
|
74
|
+
</v-row>
|
|
75
|
+
-->
|
|
76
|
+
<v-row dense align="center" >
|
|
77
|
+
<v-col :cols="numColsPerImage" align="center" v-for="(image, index) in photos" :key="index">
|
|
78
|
+
<img :src="getThumbnailUrl(image)" :height="height" @click="onClick(image)"/>
|
|
79
|
+
</v-col>
|
|
80
|
+
</v-row>
|
|
81
|
+
</v-container>
|
|
82
|
+
</template>
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { inject, ref, onMounted, onUnmounted } from 'vue';
|
|
3
|
+
import { useRoute, useRouter } from 'vue-router';
|
|
4
|
+
import { MediaService } from '@christianriedl/media';
|
|
5
|
+
|
|
6
|
+
const getMediaService = inject<() => MediaService>('get-media')!;
|
|
7
|
+
const mediaService = getMediaService();
|
|
8
|
+
const route = useRoute();
|
|
9
|
+
const router = useRouter();
|
|
10
|
+
|
|
11
|
+
const url = ref('');
|
|
12
|
+
const height = ref(0);
|
|
13
|
+
const width = ref(0);
|
|
14
|
+
let landscape = true;
|
|
15
|
+
|
|
16
|
+
mediaService.log.info('Video created');
|
|
17
|
+
|
|
18
|
+
onMounted(() => {
|
|
19
|
+
window.document.addEventListener('keydown', onKey);
|
|
20
|
+
url.value = route.query.url!.toString();
|
|
21
|
+
onResize();
|
|
22
|
+
});
|
|
23
|
+
onUnmounted(() => {
|
|
24
|
+
window.document.removeEventListener('keydown', onKey);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
function onResize() {
|
|
28
|
+
height.value = window.innerHeight;
|
|
29
|
+
width.value = window.innerWidth;
|
|
30
|
+
landscape = width.value > height.value;
|
|
31
|
+
}
|
|
32
|
+
function onKey(ev: KeyboardEvent) {
|
|
33
|
+
switch (ev.code) {
|
|
34
|
+
case "Escape":
|
|
35
|
+
router.back();
|
|
36
|
+
break;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
</script>
|
|
40
|
+
|
|
41
|
+
<template>
|
|
42
|
+
<div v-resize="onResize">
|
|
43
|
+
<video controls autoplay :src="url" :width="width" :height="height">
|
|
44
|
+
</video>
|
|
45
|
+
</div>
|
|
46
|
+
</template>
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { inject, ref, reactive, computed, onMounted, onUnmounted, nextTick, watch } from 'vue';
|
|
3
|
+
import { useRouter } from 'vue-router';
|
|
4
|
+
import { IAppState } from '@christianriedl/utils';
|
|
5
|
+
import { EItemType, EMediaType, IMediaFolder, IMediaItem, IVideoFile, MediaService, PlayerService } from '@christianriedl/media';
|
|
6
|
+
import { ISensorsState } from '@christianriedl/smarthome';
|
|
7
|
+
|
|
8
|
+
const appState = inject<IAppState>('appstate')!;
|
|
9
|
+
const sensors = inject<ISensorsState>('smarthome')!;
|
|
10
|
+
const getMediaService = inject<() => MediaService>('get-media')!;
|
|
11
|
+
const mediaService = getMediaService();
|
|
12
|
+
const getPlayerService = inject<() => PlayerService>('get-player')!;
|
|
13
|
+
const playerService = getPlayerService();
|
|
14
|
+
const rendererName = "[TV] Samsung Q9 Series (75)";
|
|
15
|
+
const showRenderer = ref(false);
|
|
16
|
+
|
|
17
|
+
const heightStyle = computed(() => { return { height: appState.bodyHeight.value + 'px', overflowY: 'auto' } });
|
|
18
|
+
const isMobile = appState.isMobile;
|
|
19
|
+
const router = useRouter();
|
|
20
|
+
|
|
21
|
+
const items: IMediaItem[] = reactive([]);
|
|
22
|
+
const selected = ref<IMediaItem>({ Name: 'Root', ItemType: EItemType.Root } as IMediaFolder);
|
|
23
|
+
const listHeight = ref(0);
|
|
24
|
+
const listhead = ref<any>(null);
|
|
25
|
+
const dialog = ref(false);
|
|
26
|
+
const showItem = ref<IVideoFile|null>(null);
|
|
27
|
+
|
|
28
|
+
const backVisible = computed (() => selected.value.ItemType != EItemType.VideoRoot);
|
|
29
|
+
const recordedVisible = computed(() => selected.value.ItemType == EItemType.VideoRoot);
|
|
30
|
+
const playVisible = computed(() => selected.value.ItemType == EItemType.VideoItem);
|
|
31
|
+
const dialogWidth = computed(() => isMobile ? appState.pageWidth : 600);
|
|
32
|
+
|
|
33
|
+
getPlayer();
|
|
34
|
+
|
|
35
|
+
window.addEventListener('popstate', onPopState);
|
|
36
|
+
function onPopState(event: any) {
|
|
37
|
+
if (event.state && event.state.noBackExitsApp && backVisible.value) {
|
|
38
|
+
event.preventDefault();
|
|
39
|
+
listBack();
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
onUnmounted(() => {
|
|
43
|
+
window.removeEventListener('popstate', onPopState);
|
|
44
|
+
while (window.history.state && window.history.state.noBackExitsApp)
|
|
45
|
+
window.history.back();
|
|
46
|
+
});
|
|
47
|
+
onMounted(async () => {
|
|
48
|
+
mediaService.log.trace('Videos created');
|
|
49
|
+
const rc = await mediaService.getLists(EMediaType.Video);
|
|
50
|
+
let root;
|
|
51
|
+
if (selected.value.ItemType == EItemType.VideoCategory)
|
|
52
|
+
root = mediaService.folders[selected.value.DLNAParentID];
|
|
53
|
+
else
|
|
54
|
+
root = await mediaService.initializeVideos();
|
|
55
|
+
mediaService.log.trace('Videos start with ' + root.DLNAID);
|
|
56
|
+
selected.value = root;
|
|
57
|
+
items.splice(0, items.length, ...root.Folders);
|
|
58
|
+
computeListHeight();
|
|
59
|
+
})
|
|
60
|
+
watch(appState.bodyHeight, () => computeListHeight());
|
|
61
|
+
function computeListHeight() {
|
|
62
|
+
nextTick(() => {
|
|
63
|
+
let height = appState.bodyHeight.value;
|
|
64
|
+
if (listhead.value && listhead.value._.vnode && listhead.value._.vnode.el) {
|
|
65
|
+
height = height - listhead.value._.vnode.el.clientHeight;
|
|
66
|
+
}
|
|
67
|
+
listHeight.value = height;
|
|
68
|
+
})
|
|
69
|
+
}
|
|
70
|
+
function play() {
|
|
71
|
+
router.push({ path: 'video', query: { url: selected.value.Url } });
|
|
72
|
+
}
|
|
73
|
+
async function playTV() {
|
|
74
|
+
const request = {
|
|
75
|
+
playerName: rendererName,
|
|
76
|
+
folderId: selected.value.DLNAParentID,
|
|
77
|
+
mediaId: selected.value.DLNAID,
|
|
78
|
+
trackNo: 0,
|
|
79
|
+
withStreamTitle: false
|
|
80
|
+
};
|
|
81
|
+
const rc = await playerService!.play(request);
|
|
82
|
+
}
|
|
83
|
+
function recorded() {
|
|
84
|
+
router.push({ path: 'recordedvideos' });
|
|
85
|
+
}
|
|
86
|
+
function listItem(item: IMediaItem) {
|
|
87
|
+
if (item.ItemType == EItemType.VideoItem) {
|
|
88
|
+
selected.value = item;
|
|
89
|
+
getPlayer();
|
|
90
|
+
computeListHeight();
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
const folder = item as IMediaFolder;
|
|
94
|
+
if (item.ItemType == EItemType.VideoCategory) {
|
|
95
|
+
mediaService.getVideos(folder)
|
|
96
|
+
.then((videos) => {
|
|
97
|
+
selected.value = videos;
|
|
98
|
+
items.splice(0, items.length, ...videos.Files);
|
|
99
|
+
window.history.pushState({ noBackExitsApp: true }, '')
|
|
100
|
+
computeListHeight();
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
function listBack() {
|
|
105
|
+
const parent = mediaService.folders[selected.value.DLNAParentID];
|
|
106
|
+
selected.value = parent;
|
|
107
|
+
items.splice(0, items.length, ...parent.Folders);
|
|
108
|
+
computeListHeight();
|
|
109
|
+
}
|
|
110
|
+
async function onInfo(item: IVideoFile) {
|
|
111
|
+
showItem.value = item;
|
|
112
|
+
dialog.value = true;
|
|
113
|
+
}
|
|
114
|
+
function getActors(item: IVideoFile) : string {
|
|
115
|
+
if (item.ActorIdxs && item.ActorIdxs.length > 0) {
|
|
116
|
+
let persons = "";
|
|
117
|
+
for (let i = 0; i < item.ActorIdxs.length; i++) {
|
|
118
|
+
if (persons.length > 0)
|
|
119
|
+
persons = persons + ', ';
|
|
120
|
+
persons = persons + mediaService.getListEntry("Video.Actors", item.ActorIdxs[i]);
|
|
121
|
+
}
|
|
122
|
+
return persons;
|
|
123
|
+
}
|
|
124
|
+
return "";
|
|
125
|
+
}
|
|
126
|
+
function getCreator(item: IVideoFile): string {
|
|
127
|
+
if (item.CreatorIdx >= 0) {
|
|
128
|
+
return mediaService.getListEntry("Video.Director", item.CreatorIdx);
|
|
129
|
+
}
|
|
130
|
+
return "";
|
|
131
|
+
}
|
|
132
|
+
function getGenre(item: IVideoFile): string {
|
|
133
|
+
if (item.GenreIdx >= 0) {
|
|
134
|
+
return mediaService.getListEntry("Video.Genre", item.GenreIdx);
|
|
135
|
+
}
|
|
136
|
+
return "";
|
|
137
|
+
}
|
|
138
|
+
function getCountry(item: IVideoFile): string {
|
|
139
|
+
if (item.CountryIdx >= 0) {
|
|
140
|
+
return mediaService.getListEntry("Video.Countries", item.CountryIdx);
|
|
141
|
+
}
|
|
142
|
+
return "";
|
|
143
|
+
}
|
|
144
|
+
function getPlayer() {
|
|
145
|
+
const samsungTV = sensors.sensors['HomeNetwork.SamsungTV'];
|
|
146
|
+
if (samsungTV && samsungTV.values["Online"].value) {
|
|
147
|
+
if (!showRenderer.value) {
|
|
148
|
+
playerService.getPlayerState(rendererName)
|
|
149
|
+
.then((playerState) => {
|
|
150
|
+
if (playerState)
|
|
151
|
+
showRenderer.value = true;
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
showRenderer.value = false;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
</script>
|
|
160
|
+
|
|
161
|
+
<template>
|
|
162
|
+
<v-card ref="listhead" class="bg-media">
|
|
163
|
+
<v-list-item v-if="selected" three-line>
|
|
164
|
+
<v-list-item-avatar tile rounded="0" size="x-large" v-if="selected.ThumbnailUrl">
|
|
165
|
+
<img width="32" height="48" :src="selected.ThumbnailUrl">
|
|
166
|
+
</v-list-item-avatar>
|
|
167
|
+
<v-list-item-content>
|
|
168
|
+
<v-list-item-title>{{selected.title}}</v-list-item-title>
|
|
169
|
+
<v-list-item-subtitle>{{selected.subTitle}}</v-list-item-subtitle>
|
|
170
|
+
<v-list-item-subtitle>{{selected.info}}</v-list-item-subtitle>
|
|
171
|
+
</v-list-item-content>
|
|
172
|
+
</v-list-item>
|
|
173
|
+
<v-card-actions>
|
|
174
|
+
<v-btn v-if="backVisible" @click="listBack">
|
|
175
|
+
<v-icon size="x-large">{{$vuetify.icons.values.back}}</v-icon>
|
|
176
|
+
</v-btn>
|
|
177
|
+
<v-btn v-if="playVisible" @click.stop="play">
|
|
178
|
+
<v-icon size="x-large">{{$vuetify.icons.values.play }}</v-icon>
|
|
179
|
+
</v-btn>
|
|
180
|
+
<v-btn v-if="showRenderer && playVisible" @click.stop="playTV">
|
|
181
|
+
<v-icon size="x-large">{{$vuetify.icons.values.tv }}</v-icon>
|
|
182
|
+
</v-btn>
|
|
183
|
+
<v-btn v-if="recordedVisible" @click.stop="recorded">
|
|
184
|
+
<v-icon size="x-large">{{$vuetify.icons.values.recorded }}</v-icon>
|
|
185
|
+
</v-btn>
|
|
186
|
+
</v-card-actions>
|
|
187
|
+
</v-card>
|
|
188
|
+
<v-card :max-height="listHeight" class="overflow-y-auto bg-media">
|
|
189
|
+
<v-list two-line class="bg-media">
|
|
190
|
+
<v-list-item-group v-model="itemIndex">
|
|
191
|
+
<v-list-item v-for="item in items" :key="item.DLNAID" :title="item.title" :subtitle="item.subTitle" @click="listItem(item)">
|
|
192
|
+
<template v-slot:append>
|
|
193
|
+
<v-list-item-avatar left rounded="0" v-if="item.ThumbnailUrl">
|
|
194
|
+
<v-btn variant="text" size="large" color="grey" icon="mdi-information" @click.stop.prevent="onInfo(item)"></v-btn>
|
|
195
|
+
</v-list-item-avatar>
|
|
196
|
+
</template>
|
|
197
|
+
<v-list-item-avatar right rounded="0" v-if="item.ThumbnailUrl">
|
|
198
|
+
<v-img width="64" height="96" :src="item.ThumbnailUrl"></v-img>
|
|
199
|
+
</v-list-item-avatar>
|
|
200
|
+
</v-list-item>
|
|
201
|
+
</v-list-item-group>
|
|
202
|
+
</v-list>
|
|
203
|
+
</v-card>
|
|
204
|
+
<v-dialog v-model="dialog" :fullscreen="isMobile">
|
|
205
|
+
<v-card :width="dialogWidth">
|
|
206
|
+
<v-card-title class="headline">{{showItem.Name}}</v-card-title>
|
|
207
|
+
<v-card-subtitle>{{showItem.subTitle}}</v-card-subtitle>
|
|
208
|
+
<v-card-actions>
|
|
209
|
+
<p>{{showItem.info}}</p>
|
|
210
|
+
<v-spacer></v-spacer>
|
|
211
|
+
<v-btn @click="dialog = false">
|
|
212
|
+
<v-icon large>{{$vuetify.icons.values.stop}}</v-icon>Exit
|
|
213
|
+
</v-btn>
|
|
214
|
+
</v-card-actions>
|
|
215
|
+
<v-card-text>
|
|
216
|
+
<v-container>
|
|
217
|
+
<v-row>
|
|
218
|
+
<p>{{showItem.Description}}</p>
|
|
219
|
+
</v-row>
|
|
220
|
+
<v-row>
|
|
221
|
+
<h3 style="height:32px"></h3>
|
|
222
|
+
</v-row>
|
|
223
|
+
<v-row>
|
|
224
|
+
<p>{{getActors(showItem)}}</p>
|
|
225
|
+
</v-row>
|
|
226
|
+
</v-container>
|
|
227
|
+
</v-card-text>
|
|
228
|
+
</v-card>
|
|
229
|
+
</v-dialog>
|
|
230
|
+
</template>
|