@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,149 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { reactive, inject, ref, computed, onMounted, onUnmounted } from 'vue';
|
|
3
|
+
import { useRouter } from 'vue-router';
|
|
4
|
+
import { Helper, IValueText, IAppState } from '@christianriedl/utils';
|
|
5
|
+
import { IMediaInstanceService } from '@christianriedl/media';
|
|
6
|
+
import RootInformationLine from '../components/RootInformationLine.vue'
|
|
7
|
+
import RootStatisticsLine from '../components/RootStatisticsLine.vue'
|
|
8
|
+
|
|
9
|
+
const appState = inject<IAppState>('appstate');
|
|
10
|
+
const getMediaInstanceService = inject<() => IMediaInstanceService>('get-mediainstance');
|
|
11
|
+
const instanceService = getMediaInstanceService!();
|
|
12
|
+
const info = instanceService.instanceInformation;
|
|
13
|
+
const stat = instanceService.scanStatistics;
|
|
14
|
+
const heightStyle = computed(() => { return { height: appState!.bodyHeight.value + 'px' } });
|
|
15
|
+
|
|
16
|
+
const listtime = computed(() => unixTimeToString(info.listModificationTime));
|
|
17
|
+
const roottime = computed(() => unixTimeToString(info.rootModificationTime));
|
|
18
|
+
const success = ref(false);
|
|
19
|
+
const text = ref('');
|
|
20
|
+
let timer = -1;
|
|
21
|
+
|
|
22
|
+
instanceService.getInformation()
|
|
23
|
+
.then((info) => { });
|
|
24
|
+
|
|
25
|
+
function unixTimeToString(unixtime: number, withSeconds?: boolean) {
|
|
26
|
+
const dt = Helper.fromUnixTime(unixtime);
|
|
27
|
+
return Helper.formatDate(dt) + ' ' + Helper.formatTime(dt, withSeconds);
|
|
28
|
+
}
|
|
29
|
+
function getStatistics() {
|
|
30
|
+
timer = -1;
|
|
31
|
+
instanceService.getScanState()
|
|
32
|
+
.then((stat) => timer = window.setTimeout(getStatistics, 500));
|
|
33
|
+
}
|
|
34
|
+
async function onCreate() {
|
|
35
|
+
success.value = false;
|
|
36
|
+
text.value = '';
|
|
37
|
+
const resp = await instanceService.createInstance('Default', false, true, true);
|
|
38
|
+
success.value = resp.success;
|
|
39
|
+
text.value = resp.text;
|
|
40
|
+
if (resp.success) {
|
|
41
|
+
timer = window.setTimeout(getStatistics, 500);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
async function onCancel() {
|
|
45
|
+
if (timer >= 0) {
|
|
46
|
+
window.clearTimeout(timer); timer = -1;
|
|
47
|
+
}
|
|
48
|
+
success.value = false;
|
|
49
|
+
text.value = '';
|
|
50
|
+
const resp = await instanceService.cancelScan();
|
|
51
|
+
success.value = resp.success;
|
|
52
|
+
text.value = resp.text;
|
|
53
|
+
}
|
|
54
|
+
async function onClear() {
|
|
55
|
+
if (timer >= 0) {
|
|
56
|
+
window.clearTimeout(timer); timer = -1;
|
|
57
|
+
}
|
|
58
|
+
success.value = false;
|
|
59
|
+
text.value = '';
|
|
60
|
+
const resp = await instanceService.clearInstance();
|
|
61
|
+
success.value = resp.success;
|
|
62
|
+
text.value = resp.text;
|
|
63
|
+
}
|
|
64
|
+
async function onMakeCurrent() {
|
|
65
|
+
if (timer >= 0) {
|
|
66
|
+
window.clearTimeout(timer); timer = -1;
|
|
67
|
+
}
|
|
68
|
+
success.value = false;
|
|
69
|
+
text.value = '';
|
|
70
|
+
const resp = await instanceService.makeCurrent();
|
|
71
|
+
success.value = resp.success;
|
|
72
|
+
text.value = resp.text;
|
|
73
|
+
if (resp.success) {
|
|
74
|
+
await instanceService.getInformation();
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
</script>
|
|
78
|
+
|
|
79
|
+
<template>
|
|
80
|
+
<v-container fluid class="app-container app-scroll bg-light text-on-light" :style="heightStyle">
|
|
81
|
+
<h2>Media Instance Administration</h2>
|
|
82
|
+
<v-row dense class="font-weight-bold">
|
|
83
|
+
<v-col cols="4">Name</v-col>
|
|
84
|
+
<v-col cols="8">{{info.instanceName}}</v-col>
|
|
85
|
+
</v-row>
|
|
86
|
+
<v-row dense class="font-weight-bold">
|
|
87
|
+
<v-col cols="4">IP Address</v-col>
|
|
88
|
+
<v-col cols="8">{{info.ipAddress}}</v-col>
|
|
89
|
+
</v-row>
|
|
90
|
+
<v-row dense class="font-weight-bold">
|
|
91
|
+
<v-col cols="4">Number of list items</v-col>
|
|
92
|
+
<v-col cols="8">{{info.listItemCount}}</v-col>
|
|
93
|
+
</v-row>
|
|
94
|
+
<v-row dense class="font-weight-bold">
|
|
95
|
+
<v-col cols="4">List item date</v-col>
|
|
96
|
+
<v-col cols="8">{{listtime}}</v-col>
|
|
97
|
+
</v-row>
|
|
98
|
+
<v-row dense class="font-weight-bold">
|
|
99
|
+
<v-col cols="4">Root date</v-col>
|
|
100
|
+
<v-col cols="8">{{roottime}}</v-col>
|
|
101
|
+
</v-row>
|
|
102
|
+
<h4>Roots</h4>
|
|
103
|
+
<v-row dense class="font-weight-bold">
|
|
104
|
+
<v-col cols="1">Name</v-col>
|
|
105
|
+
<v-col cols="1">Id</v-col>
|
|
106
|
+
<v-col cols="1">Folders</v-col>
|
|
107
|
+
<v-col cols="1">V-Folders</v-col>
|
|
108
|
+
<v-col cols="2">Files</v-col>
|
|
109
|
+
<v-col cols="2">Modified at</v-col>
|
|
110
|
+
<v-col cols="4">Item types</v-col>
|
|
111
|
+
</v-row>
|
|
112
|
+
<root-information-line v-for="root in info.rootInformations" :key="root.id" :info="root"></root-information-line>
|
|
113
|
+
|
|
114
|
+
<v-row dense class="font-weight-bold">
|
|
115
|
+
<v-col cols="3">
|
|
116
|
+
<v-btn @click="onCreate">CREATE</v-btn>
|
|
117
|
+
</v-col>
|
|
118
|
+
<v-col cols="3">
|
|
119
|
+
<v-btn @click="onCancel">CANCEL</v-btn>
|
|
120
|
+
</v-col>
|
|
121
|
+
<v-col cols="3">
|
|
122
|
+
<v-btn @click="onClear">CLEAR</v-btn>
|
|
123
|
+
</v-col>
|
|
124
|
+
<v-col cols="3">
|
|
125
|
+
<v-btn @click="onMakeCurrent">MAKE CURRENT</v-btn>
|
|
126
|
+
</v-col>
|
|
127
|
+
</v-row>
|
|
128
|
+
<v-row dense class="font-weight-bold">
|
|
129
|
+
<v-col cols="2" v-show="stat.isScanning">SCANNING</v-col>
|
|
130
|
+
<v-col cols="2" v-show="stat.isCanceled">CANCELED</v-col>
|
|
131
|
+
<v-col cols="4">{{stat.listItemCount + ' list items'}}</v-col>
|
|
132
|
+
<v-col cols="1" v-show="success">OK</v-col>
|
|
133
|
+
<v-col cols="1" v-show="!success">NOT OK</v-col>
|
|
134
|
+
<v-col cols="3">{{text}}</v-col>
|
|
135
|
+
</v-row>
|
|
136
|
+
<h4>Statistics</h4>
|
|
137
|
+
<v-row dense class="font-weight-bold">
|
|
138
|
+
<v-col cols="1">Name</v-col>
|
|
139
|
+
<v-col cols="5">Directory</v-col>
|
|
140
|
+
<v-col cols="1"># Dirs</v-col>
|
|
141
|
+
<v-col cols="1">Folders</v-col>
|
|
142
|
+
<v-col cols="1">V-Folders</v-col>
|
|
143
|
+
<v-col cols="1">Files</v-col>
|
|
144
|
+
<v-col cols="1">Medias</v-col>
|
|
145
|
+
<v-col cols="1">Errors</v-col>
|
|
146
|
+
</v-row>
|
|
147
|
+
<root-statistics-line v-for="root in stat.rootStatistics" :key="root.rootName" :stat="root"></root-statistics-line>
|
|
148
|
+
</v-container>
|
|
149
|
+
</template>
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { inject, ref, reactive, computed, onMounted, onUnmounted, nextTick, watch } from 'vue';
|
|
3
|
+
import { IAppState } from '@christianriedl/utils';
|
|
4
|
+
import { EItemType, EMediaType, IMediaFolder, IMediaItem, IAudioFile, MediaService } from '@christianriedl/media';
|
|
5
|
+
|
|
6
|
+
const appState = inject<IAppState>('appstate')!;
|
|
7
|
+
const getMediaService = inject<() => MediaService>('get-media')!;
|
|
8
|
+
const mediaService = getMediaService();
|
|
9
|
+
const heightStyle = computed(() => { return { height: appState.bodyHeight.value + 'px', overflowY: 'auto' } });
|
|
10
|
+
const isMobile = appState.isMobile;
|
|
11
|
+
|
|
12
|
+
const items: IMediaItem[] = reactive([]);
|
|
13
|
+
const selected = ref<IMediaItem>({ Name: 'Root', ItemType: EItemType.Root } as IMediaFolder);
|
|
14
|
+
const playingTrackUrl = ref("");
|
|
15
|
+
const playingTrack = ref<IAudioFile | null>(null);
|
|
16
|
+
const playItems: IAudioFile[] = [];
|
|
17
|
+
const itemIndex = ref(0);
|
|
18
|
+
const position = ref(0);
|
|
19
|
+
const positionLength = ref(0);
|
|
20
|
+
const listHeight = ref(0);
|
|
21
|
+
const playIndex = ref(-1);
|
|
22
|
+
|
|
23
|
+
const backVisible = computed(() => selected.value.ItemType != EItemType.AudioRoot);
|
|
24
|
+
const downloadAlbumVisible = computed(() => selected.value.ItemType == EItemType.AudioAlbum && !playingTrackUrl.value && !isMobile);
|
|
25
|
+
const downloadVisible = computed(() => playingTrackUrl.value && !isMobile);
|
|
26
|
+
const audio = ref<HTMLAudioElement | null>(null);
|
|
27
|
+
const paused = ref(false);
|
|
28
|
+
const listhead = ref<any>(null);
|
|
29
|
+
const positionText = computed(() => {
|
|
30
|
+
if (playingTrack.value) {
|
|
31
|
+
if (playingTrack.value.Duration > 0)
|
|
32
|
+
return to_time(position.value) + '/' + to_time(playingTrack.value.Duration);
|
|
33
|
+
else
|
|
34
|
+
return to_time(position.value);
|
|
35
|
+
}
|
|
36
|
+
return '';
|
|
37
|
+
});
|
|
38
|
+
window.addEventListener('popstate', onPopState);
|
|
39
|
+
function onPopState (event: any) {
|
|
40
|
+
if (event.state && event.state.noBackExitsApp && backVisible.value) {
|
|
41
|
+
event.preventDefault();
|
|
42
|
+
listBack();
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
onUnmounted(() => {
|
|
46
|
+
window.removeEventListener('popstate', onPopState);
|
|
47
|
+
while (window.history.state && window.history.state.noBackExitsApp)
|
|
48
|
+
window.history.back();
|
|
49
|
+
});
|
|
50
|
+
onMounted(async () => {
|
|
51
|
+
mediaService.log.trace('Music created');
|
|
52
|
+
const rc = await mediaService.getLists(EMediaType.Audio);
|
|
53
|
+
let root;
|
|
54
|
+
if (selected.value.ItemType == EItemType.AudioAlbum)
|
|
55
|
+
root = mediaService.folders[selected.value.DLNAParentID];
|
|
56
|
+
else
|
|
57
|
+
root = await mediaService.initializeAudios();
|
|
58
|
+
mediaService.log.trace('Music start with ' + root.DLNAID);
|
|
59
|
+
selected.value = root;
|
|
60
|
+
items.splice(0, items.length, ...root.Folders);
|
|
61
|
+
computeListHeight();
|
|
62
|
+
})
|
|
63
|
+
watch(appState.bodyHeight, () => computeListHeight());
|
|
64
|
+
function computeListHeight() {
|
|
65
|
+
nextTick(() => {
|
|
66
|
+
let height = appState.bodyHeight.value;
|
|
67
|
+
if (listhead.value && listhead.value._.vnode && listhead.value._.vnode.el) {
|
|
68
|
+
height = height - listhead.value._.vnode.el.clientHeight;
|
|
69
|
+
}
|
|
70
|
+
listHeight.value = height;
|
|
71
|
+
})
|
|
72
|
+
}
|
|
73
|
+
function listItem(item: IMediaItem) {
|
|
74
|
+
if (item.ItemType == EItemType.AudioTrack) {
|
|
75
|
+
//playItems = [];
|
|
76
|
+
playItems.splice(0, 0, ...items as IAudioFile[]);
|
|
77
|
+
play(items.indexOf(item));
|
|
78
|
+
computeListHeight();
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
const folder = item as IMediaFolder;
|
|
82
|
+
if (folder.Folders && item.ItemType != EItemType.AudioAlbum) {
|
|
83
|
+
items.splice(0, items.length, ...folder.Folders);
|
|
84
|
+
selected.value = item;
|
|
85
|
+
window.history.pushState({ noBackExitsApp: true }, '')
|
|
86
|
+
computeListHeight();
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
mediaService.getAudios(folder)
|
|
90
|
+
.then((audios) => {
|
|
91
|
+
window.history.pushState({ noBackExitsApp: true }, '')
|
|
92
|
+
selected.value = audios;
|
|
93
|
+
items.splice(0, items.length, ...audios.Files);
|
|
94
|
+
computeListHeight();
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
function listBack() {
|
|
100
|
+
const parent = mediaService.folders[selected.value.DLNAParentID];
|
|
101
|
+
selected.value = parent;
|
|
102
|
+
items.splice(0, items.length, ...parent.Folders);
|
|
103
|
+
computeListHeight();
|
|
104
|
+
}
|
|
105
|
+
function onTimeUpdate() {
|
|
106
|
+
position.value = audio.value!.currentTime;
|
|
107
|
+
if (playingTrack.value && playingTrack.value.Duration > 0)
|
|
108
|
+
positionLength.value = position.value * 100 / playingTrack.value.Duration;
|
|
109
|
+
else
|
|
110
|
+
positionLength.value = 0;
|
|
111
|
+
}
|
|
112
|
+
function onEnded() {
|
|
113
|
+
mediaService.log.trace(`onEnded ${playIndex} ended`);
|
|
114
|
+
if (playIndex.value < playItems.length - 1)
|
|
115
|
+
play(playIndex.value + 1);
|
|
116
|
+
}
|
|
117
|
+
function play(index: number) {
|
|
118
|
+
if (items.length == playItems.length && items[0].DLNAParentID == playItems[0].DLNAParentID)
|
|
119
|
+
itemIndex.value = index;
|
|
120
|
+
playIndex.value = index;
|
|
121
|
+
const item = playItems[playIndex.value];
|
|
122
|
+
paused.value = false;
|
|
123
|
+
playingTrack.value = item;
|
|
124
|
+
playingTrackUrl.value = mediaService.getAudioUrl(item);
|
|
125
|
+
audio.value!.src = playingTrackUrl.value;
|
|
126
|
+
audio.value!.load();
|
|
127
|
+
audio.value!.play()
|
|
128
|
+
.then(() => { mediaService.log.trace(`play ${index}ended`) });
|
|
129
|
+
}
|
|
130
|
+
function playpause() {
|
|
131
|
+
if (audio.value!.paused) {
|
|
132
|
+
paused.value = false;
|
|
133
|
+
audio.value!.play()
|
|
134
|
+
.then(() => { console.log("play ended") });
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
paused.value = true;
|
|
138
|
+
audio.value!.pause();
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
function previous() {
|
|
142
|
+
if (playIndex.value > 0)
|
|
143
|
+
play(playIndex.value - 1);
|
|
144
|
+
}
|
|
145
|
+
function next() {
|
|
146
|
+
if (playIndex.value < playItems.length - 1)
|
|
147
|
+
play(playIndex.value + 1);
|
|
148
|
+
}
|
|
149
|
+
function download() {
|
|
150
|
+
window.open(playingTrackUrl.value);
|
|
151
|
+
}
|
|
152
|
+
function downloadAlbum() {
|
|
153
|
+
const downloadUrl = mediaService.getAlbumDownloadUrl(selected.value.Url);
|
|
154
|
+
window.open(downloadUrl);
|
|
155
|
+
}
|
|
156
|
+
function to_time(s: number): string {
|
|
157
|
+
var r = "";
|
|
158
|
+
s = Math.floor(s);
|
|
159
|
+
if (s >= 3600) {
|
|
160
|
+
r += Math.floor(s / 3600).toString() + ":"; s = s % 3600;
|
|
161
|
+
}
|
|
162
|
+
if (!r || s >= 600) {
|
|
163
|
+
r += Math.floor(s / 60).toString() + ":"; s = s % 60;
|
|
164
|
+
}
|
|
165
|
+
else {
|
|
166
|
+
r += "0"; r += Math.floor(s / 60).toString() + ":"; s = s % 60;
|
|
167
|
+
}
|
|
168
|
+
if (s >= 10) {
|
|
169
|
+
r += s.toString(); s = -1;
|
|
170
|
+
}
|
|
171
|
+
else if (s >= 0) {
|
|
172
|
+
r += "0"; r += s.toString();
|
|
173
|
+
s = -1;
|
|
174
|
+
}
|
|
175
|
+
return r;
|
|
176
|
+
}
|
|
177
|
+
</script>
|
|
178
|
+
|
|
179
|
+
<template>
|
|
180
|
+
<v-card ref="listhead" class="bg-media">
|
|
181
|
+
<audio ref="audio" preload="none" @ended="onEnded" @timeupdate="onTimeUpdate">
|
|
182
|
+
</audio>
|
|
183
|
+
<v-list-item three-line>
|
|
184
|
+
<v-list-item-avatar tile rounded="0" size="x-large" v-if="selected.ThumbnailUrl">
|
|
185
|
+
<img width="40" :src="selected.ThumbnailUrl">
|
|
186
|
+
</v-list-item-avatar>
|
|
187
|
+
<v-list-item-content>
|
|
188
|
+
<v-list-item-title>{{selected.title}}</v-list-item-title>
|
|
189
|
+
<v-list-item-subtitle>{{selected.subTitle}}</v-list-item-subtitle>
|
|
190
|
+
<v-list-item-subtitle v-if="playingTrack">{{playingTrack.Name}}</v-list-item-subtitle>
|
|
191
|
+
</v-list-item-content>
|
|
192
|
+
</v-list-item>
|
|
193
|
+
<v-card-actions>
|
|
194
|
+
<v-btn v-if="backVisible" @click="listBack">
|
|
195
|
+
<v-icon>{{$vuetify.icons.values.back}}</v-icon>
|
|
196
|
+
</v-btn>
|
|
197
|
+
<v-btn v-if="downloadAlbumVisible" @click="downloadAlbum">
|
|
198
|
+
Album
|
|
199
|
+
<v-icon>{{$vuetify.icons.values.download}}</v-icon>
|
|
200
|
+
</v-btn>
|
|
201
|
+
<v-btn small v-if="playingTrackUrl" @click="playpause">
|
|
202
|
+
<v-icon>{{paused ? $vuetify.icons.values.play : $vuetify.icons.values.pause }}</v-icon>
|
|
203
|
+
</v-btn>
|
|
204
|
+
<v-btn small v-if="playingTrackUrl" @click="previous">
|
|
205
|
+
<v-icon>{{$vuetify.icons.values.prev}}</v-icon>
|
|
206
|
+
</v-btn>
|
|
207
|
+
<v-btn small v-if="playingTrackUrl" @click="next">
|
|
208
|
+
<v-icon>{{$vuetify.icons.values.next}}</v-icon>
|
|
209
|
+
</v-btn>
|
|
210
|
+
<v-btn small v-if="downloadVisible" @click="download">
|
|
211
|
+
<v-icon>{{$vuetify.icons.values.download}}</v-icon>
|
|
212
|
+
</v-btn>
|
|
213
|
+
</v-card-actions>
|
|
214
|
+
<v-progress-linear v-if="playingTrackUrl" v-model="positionLength" color="blue" height="25"><strong>{{positionText}}</strong></v-progress-linear>
|
|
215
|
+
</v-card>
|
|
216
|
+
<v-card :height="listHeight" class="overflow-y-auto bg-media">
|
|
217
|
+
<v-list two-line class="bg-media">
|
|
218
|
+
<v-list-item-group v-model="itemIndex" >
|
|
219
|
+
<v-list-item v-for="(item,index) in items" :key="item.DLNAID" :title="item.title" :subtitle="item.subTitle"
|
|
220
|
+
active-color="blue" :active="index == playIndex" @click="listItem(item)">
|
|
221
|
+
<!--
|
|
222
|
+
<v-list-item-content>
|
|
223
|
+
<v-list-item-title v-html="item.title"></v-list-item-title>
|
|
224
|
+
<v-list-item-subtitle v-html="item.subTitle"></v-list-item-subtitle>
|
|
225
|
+
</v-list-item-content>
|
|
226
|
+
-->
|
|
227
|
+
<v-list-item-avatar right rounded="0" v-if="item.ThumbnailUrl">
|
|
228
|
+
<v-img width="64" :src="item.ThumbnailUrl"></v-img>
|
|
229
|
+
</v-list-item-avatar>
|
|
230
|
+
</v-list-item>
|
|
231
|
+
</v-list-item-group>
|
|
232
|
+
</v-list>
|
|
233
|
+
</v-card>
|
|
234
|
+
</template>
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { inject, ref, reactive, computed, onMounted, onBeforeMount, onUnmounted, nextTick, watch } from 'vue';
|
|
3
|
+
import { useRouter, useRoute } from 'vue-router';
|
|
4
|
+
import { IAppState, Dictionary, Helper } from '@christianriedl/utils';
|
|
5
|
+
import { EMediaType, IPictureFile, MediaService } from '@christianriedl/media';
|
|
6
|
+
|
|
7
|
+
const router = useRouter();
|
|
8
|
+
const route = useRoute();
|
|
9
|
+
const appState = inject<IAppState>('appstate')!;
|
|
10
|
+
const getMediaService = inject<() => MediaService>('get-media')!;
|
|
11
|
+
const mediaService = getMediaService();
|
|
12
|
+
const heightStyle = computed(() => { return { height: appState.bodyHeight.value + 'px', overflowY: 'hidden' } });
|
|
13
|
+
const isMobile = appState.isMobile;
|
|
14
|
+
const url = ref('');
|
|
15
|
+
const dialog = ref(false);
|
|
16
|
+
const folderId = decodeURIComponent(route.query!.id!.toString());
|
|
17
|
+
const index = ref(0);
|
|
18
|
+
const items = reactive<IPictureFile[]>([]);
|
|
19
|
+
const width = ref(0);
|
|
20
|
+
const height = ref(0);
|
|
21
|
+
const landscape = ref(true);
|
|
22
|
+
const metadata = reactive<Dictionary<any>>({});
|
|
23
|
+
const cycle = ref(false);
|
|
24
|
+
const interval = ref(8000);
|
|
25
|
+
|
|
26
|
+
watch([appState.bodyHeight, appState.pageWidth], () => {
|
|
27
|
+
width.value = appState.pageWidth.value;
|
|
28
|
+
height.value = appState.bodyHeight.value;
|
|
29
|
+
landscape.value = width.value > height.value;
|
|
30
|
+
}, { immediate: true });
|
|
31
|
+
|
|
32
|
+
async function start(): Promise<boolean> {
|
|
33
|
+
window.document.addEventListener('keydown', onKey);
|
|
34
|
+
let folder = mediaService.folders[folderId];
|
|
35
|
+
if(!folder) {
|
|
36
|
+
const split = folderId.split(/[.|]/);
|
|
37
|
+
await mediaService.initializePhotos(split[1], split[2]);
|
|
38
|
+
folder = mediaService.folders[folderId];
|
|
39
|
+
if (folder) {
|
|
40
|
+
if (!mediaService.medialists["Picture.Event"])
|
|
41
|
+
await mediaService.getLists(EMediaType.Picture);
|
|
42
|
+
}
|
|
43
|
+
else
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
const photos = await mediaService.getPhotos(folder);
|
|
47
|
+
if(photos.Files.length > 0) {
|
|
48
|
+
items.splice(0, items.length, ...photos.Files as IPictureFile[]);
|
|
49
|
+
if (route.query.start) {
|
|
50
|
+
const start = route.query.start.toString();
|
|
51
|
+
const idx = items.findIndex((item) => item.DLNAID == start);
|
|
52
|
+
if (idx >= 0) {
|
|
53
|
+
index.value = idx;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
url.value = photos.Url;
|
|
57
|
+
buildUrls();
|
|
58
|
+
//onInput(0); required für v-carousel ?
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
nextTick(() => router.back());
|
|
62
|
+
}
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
onMounted(async () => {
|
|
66
|
+
await start();
|
|
67
|
+
});
|
|
68
|
+
onUnmounted(() => {
|
|
69
|
+
window.document.removeEventListener('keydown', onKey);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
function buildUrls() {
|
|
73
|
+
for (var i = 0; i < items.length; i++) {
|
|
74
|
+
const item = items[i];
|
|
75
|
+
if (item.realUrl && item.realUrl.startsWith('blob'))
|
|
76
|
+
window.URL.revokeObjectURL(item.realUrl);
|
|
77
|
+
item.realUrl = getUrl(item);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
function getUrl(item: IPictureFile) {
|
|
81
|
+
if (landscape.value)
|
|
82
|
+
return mediaService.getPhotoUrl(url.value, item.Url, `${0}x${height.value}x${item.Orientation}`);
|
|
83
|
+
else
|
|
84
|
+
return mediaService.getPhotoUrl(url.value, item.Url, `${width.value}x${0}x${item.Orientation}`);
|
|
85
|
+
}
|
|
86
|
+
function onInput(idx: number) {
|
|
87
|
+
idx++;
|
|
88
|
+
if (idx < items.length) {
|
|
89
|
+
const item = items[idx];
|
|
90
|
+
mediaService.getImage(item.realUrl!)
|
|
91
|
+
.then(blob => {
|
|
92
|
+
item.realUrl = window.URL.createObjectURL(blob);
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
function onClick(ev: Event) {
|
|
97
|
+
dialog.value = true;
|
|
98
|
+
}
|
|
99
|
+
async function onKey(ev: KeyboardEvent) {
|
|
100
|
+
switch (ev.code) {
|
|
101
|
+
case "Escape":
|
|
102
|
+
router.back();
|
|
103
|
+
break;
|
|
104
|
+
case "ArrowLeft":
|
|
105
|
+
if (index.value > 0)
|
|
106
|
+
index.value--;
|
|
107
|
+
break;
|
|
108
|
+
case "ArrowRight":
|
|
109
|
+
if (index.value < items.length - 1)
|
|
110
|
+
index.value++;
|
|
111
|
+
break;
|
|
112
|
+
case "KeyI":
|
|
113
|
+
const item = items[index.value];
|
|
114
|
+
const exif = await mediaService.getExifInfo(item.DLNAParentID, item.DLNAID);
|
|
115
|
+
setMetaData(exif, item);
|
|
116
|
+
dialog.value = true;
|
|
117
|
+
break;
|
|
118
|
+
case "KeyP":
|
|
119
|
+
cycle.value = true;
|
|
120
|
+
break;
|
|
121
|
+
case "Digit0":
|
|
122
|
+
cycle.value = false;
|
|
123
|
+
break;
|
|
124
|
+
case "Digit1":
|
|
125
|
+
cycle.value = true; interval.value = 1000;
|
|
126
|
+
break;
|
|
127
|
+
case "Digit2":
|
|
128
|
+
cycle.value = true; interval.value = 2000;
|
|
129
|
+
break;
|
|
130
|
+
case "Digit3":
|
|
131
|
+
cycle.value = true; interval.value = 3000;
|
|
132
|
+
break;
|
|
133
|
+
case "Digit4":
|
|
134
|
+
cycle.value = true; interval.value = 4000;
|
|
135
|
+
break;
|
|
136
|
+
case "Digit5":
|
|
137
|
+
cycle.value = true; interval.value = 5000;
|
|
138
|
+
break;
|
|
139
|
+
case "Digit6":
|
|
140
|
+
cycle.value = true; interval.value = 6000;
|
|
141
|
+
break;
|
|
142
|
+
case "Digit7":
|
|
143
|
+
cycle.value = true; interval.value = 7000;
|
|
144
|
+
break;
|
|
145
|
+
case "Digit8":
|
|
146
|
+
cycle.value = true; interval.value = 8000;
|
|
147
|
+
break;
|
|
148
|
+
case "Digit9":
|
|
149
|
+
cycle.value = true; interval.value = 9000;
|
|
150
|
+
break;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
function itemWidth(item: IPictureFile): number {
|
|
154
|
+
const fac = item.Height / height.value;
|
|
155
|
+
return Math.floor(item.Width * fac);
|
|
156
|
+
}
|
|
157
|
+
function aspectRatio(item: IPictureFile): string {
|
|
158
|
+
return `${item.Width}/${item.Height}`;
|
|
159
|
+
}
|
|
160
|
+
function setMetaData(exif: Dictionary<any>, file: IPictureFile) {
|
|
161
|
+
metadata['Name'] = file.Name;
|
|
162
|
+
if (exif) {
|
|
163
|
+
if (exif.Model)
|
|
164
|
+
metadata['Kamera'] = exif.Model;
|
|
165
|
+
else
|
|
166
|
+
delete metadata['Kamera'];
|
|
167
|
+
if (exif.LensModel)
|
|
168
|
+
metadata['Objectiv'] = exif.LensModel;
|
|
169
|
+
else
|
|
170
|
+
delete metadata['Objectiv'];
|
|
171
|
+
if (exif.Artist)
|
|
172
|
+
metadata['Von'] = exif.Artist as string;
|
|
173
|
+
else
|
|
174
|
+
delete metadata['Von'];
|
|
175
|
+
if (exif.ExposureTime)
|
|
176
|
+
metadata['Belichtung'] = '1/' + Helper.round(1.0 / exif.ExposureTime, 0);
|
|
177
|
+
else
|
|
178
|
+
delete metadata['Belichtung'];
|
|
179
|
+
if (exif.FNumber)
|
|
180
|
+
metadata['Blende'] = exif.FNumber.toString();
|
|
181
|
+
else
|
|
182
|
+
delete metadata['Blende'];
|
|
183
|
+
if (exif.DateTimeOriginal)
|
|
184
|
+
metadata['Zeit'] = exif.DateTimeOriginal;
|
|
185
|
+
else
|
|
186
|
+
metadata['Zeit'] = file.Date.toLocaleString();
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
189
|
+
delete metadata['Kamera'];
|
|
190
|
+
delete metadata['Objectiv'];
|
|
191
|
+
delete metadata['Von'];
|
|
192
|
+
delete metadata['Belichtung'];
|
|
193
|
+
delete metadata['Blende'];
|
|
194
|
+
metadata['Zeit'] = file.Date.toLocaleString();
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
metadata['Dimension'] = `${file.Width}x${file.Height} o:${file.Orientation}`
|
|
198
|
+
metadata['Mb'] = (file.Size / 1000000).toString() + " Mb";
|
|
199
|
+
if (file.EventIdx >= 0)
|
|
200
|
+
metadata['Ereignis'] = mediaService.getListEntry("Picture.Event", file.EventIdx);
|
|
201
|
+
else
|
|
202
|
+
delete metadata['Ereignis'];
|
|
203
|
+
if (file.LocationIdx >= 0)
|
|
204
|
+
metadata['Ort'] = mediaService.getListEntry("Picture.Location", file.LocationIdx);
|
|
205
|
+
else
|
|
206
|
+
delete metadata['Ort'];
|
|
207
|
+
if (file.PersonIdxs && file.PersonIdxs.length > 0) {
|
|
208
|
+
let persons = "";
|
|
209
|
+
for (let i = 0; i < file.PersonIdxs.length; i++) {
|
|
210
|
+
persons = persons + mediaService.getListEntry("Picture.Person", file.PersonIdxs[i]) + " ";
|
|
211
|
+
}
|
|
212
|
+
metadata['Personen'] = persons;
|
|
213
|
+
}
|
|
214
|
+
else
|
|
215
|
+
delete metadata['Personen'];
|
|
216
|
+
}
|
|
217
|
+
</script>
|
|
218
|
+
|
|
219
|
+
<template>
|
|
220
|
+
<v-container fluid :style="heightStyle">
|
|
221
|
+
<v-carousel hide-delimiters :show-arrows="false" v-model="index"
|
|
222
|
+
:width="width" :height="height" :cycle="cycle" :interval="interval"
|
|
223
|
+
@change="onInput">
|
|
224
|
+
<v-carousel-item v-for="item in items" :key="item.DLNAID">
|
|
225
|
+
<img :src="item.realUrl" />
|
|
226
|
+
</v-carousel-item>
|
|
227
|
+
</v-carousel>
|
|
228
|
+
<v-dialog v-model="dialog" :fullscreen="isMobile">
|
|
229
|
+
<v-card v-if="dialog" width="400">
|
|
230
|
+
<v-card-title class="headline">Metadata</v-card-title>
|
|
231
|
+
<v-card-text>
|
|
232
|
+
<v-container>
|
|
233
|
+
<v-row v-for="(value, key) in metadata" :key="key">
|
|
234
|
+
<v-col cols="4" class="font-weight-bold">{{key}}</v-col>
|
|
235
|
+
<v-col cols="8">{{value}}</v-col>
|
|
236
|
+
</v-row>
|
|
237
|
+
</v-container>
|
|
238
|
+
</v-card-text>
|
|
239
|
+
<v-card-actions>
|
|
240
|
+
<v-btn @click="dialog = false">
|
|
241
|
+
<v-icon large>{{$vuetify.icons.values.stop}}</v-icon>Exit
|
|
242
|
+
</v-btn>
|
|
243
|
+
</v-card-actions>
|
|
244
|
+
</v-card>
|
|
245
|
+
</v-dialog>
|
|
246
|
+
</v-container>
|
|
247
|
+
</template>
|
|
248
|
+
|
|
249
|
+
<style scoped>
|
|
250
|
+
img {
|
|
251
|
+
display: block;
|
|
252
|
+
margin-left: auto;
|
|
253
|
+
margin-right: auto;
|
|
254
|
+
}
|
|
255
|
+
</style>
|