From d3c9ca4f034dafb5ef2ea47ff6d75c6735f63b6b Mon Sep 17 00:00:00 2001 From: Charles Olivier Savignac <1275666+sircharlo@users.noreply.github.com> Date: Mon, 9 Dec 2024 09:17:03 -0800 Subject: [PATCH] feat: allow time entry for custom durations; show time elapsed and remaining during video playback (#3661) * fix: groundwork to allow time entry for custom durations * fix: add elapsed time * fix: timeToSeconds * fix: logic fixes * fix: more logic fixes and linting * fix: media tag editing was broken * fix: properly set initial tag value on edit * fix: tag typing * fix: repeat and custom duration was not working as intended --------- Co-authored-by: Manoah Tervoort <46671786+mtdvlpr@users.noreply.github.com> --- src/components/dialog/DialogAudioBible.vue | 34 +-- src/components/media/MediaItem.vue | 324 +++++++++++++-------- src/css/app.scss | 4 +- src/helpers/cleanup.ts | 2 +- src/helpers/jw-media.ts | 44 +-- src/layouts/MainLayout.vue | 5 +- src/pages/MediaCalendarPage.vue | 90 +++--- src/pages/MediaPlayerPage.vue | 38 +-- src/stores/app-settings.ts | 7 - src/stores/jw.ts | 7 - src/types/media.d.ts | 8 +- src/utils/time.ts | 22 ++ 12 files changed, 332 insertions(+), 253 deletions(-) diff --git a/src/components/dialog/DialogAudioBible.vue b/src/components/dialog/DialogAudioBible.vue index 2f6fbaf7f8..2c5267462f 100644 --- a/src/components/dialog/DialogAudioBible.vue +++ b/src/components/dialog/DialogAudioBible.vue @@ -202,22 +202,14 @@ import type { } from 'src/types'; import { whenever } from '@vueuse/core'; -import { storeToRefs } from 'pinia'; import { errorCatcher } from 'src/helpers/error-catcher'; import { downloadAdditionalRemoteVideo, getAudioBibleMedia, } from 'src/helpers/jw-media'; -import { useCurrentStateStore } from 'src/stores/current-state'; -import { useJwStore } from 'src/stores/jw'; +import { timeToSeconds } from 'src/utils/time'; import { computed, ref, watch } from 'vue'; -// Stores -const currentStateStore = useCurrentStateStore(); -const { currentCongregation, selectedDate } = storeToRefs(currentStateStore); -const jwStore = useJwStore(); -const { customDurations } = storeToRefs(jwStore); - // Props const props = defineProps<{ section: MediaSection | undefined; @@ -337,11 +329,6 @@ const addSelectedVerses = async () => { const startVerseNumber = chosenVerses.value[0]; const endVerseNumber = chosenVerses.value[1] || startVerseNumber; - const timeToSeconds = (time: string) => { - const [h, m, s] = time.split(':').map(parseFloat); - return (h ?? 0) * 3600 + (m ?? 0) * 60 + (s ?? 0); - }; - const min = timeToSeconds( selectedChapterMedia.value.map((item) => item.markers.markers.find( @@ -359,7 +346,7 @@ const addSelectedVerses = async () => { ? timeToSeconds(endVerse.startTime) + timeToSeconds(endVerse.duration) : 0; - const uniqueId = await downloadAdditionalRemoteVideo( + await downloadAdditionalRemoteVideo( selectedChapterMedia.value, undefined, false, @@ -371,21 +358,12 @@ const addSelectedVerses = async () => { .filter((verse, index, self) => self.indexOf(verse) === index) .join('-'), props.section, + { + max, + min, + }, ); - if ( - uniqueId && - min && - max && - selectedDate.value && - currentCongregation.value - ) { - const congregation = (customDurations.value[currentCongregation.value] ??= - {}); - const dateDurations = (congregation[selectedDate.value] ??= {}); - dateDurations[uniqueId] = { max, min }; - } - resetBibleBook(true, true); loading.value = false; diff --git a/src/components/media/MediaItem.vue b/src/components/media/MediaItem.vue index f417f0354a..a129100244 100644 --- a/src/components/media/MediaItem.vue +++ b/src/components/media/MediaItem.vue @@ -41,10 +41,7 @@ class="q-mr-xs" color="white" :name=" - !!hoveredBadges[media.uniqueId] || - customDurations[currentCongregation]?.[selectedDate]?.[ - media.uniqueId - ] + !!hoveredBadges[media.uniqueId] || customDurationIsSet ? 'mmm-edit' : props.media.isAudio ? 'mmm-music-note' @@ -52,11 +49,13 @@ " /> {{ - customDurationIsSet ? formatTime(customDurationMin) + ' - ' : '' + customDurationIsSet + ? formatTime(mediaCustomDuration.min ?? 0) + ' - ' + : '' }} - {{ formatTime(customDurationMax) }} + {{ formatTime(mediaCustomDuration.max ?? media.duration) }} - +
- {{ formatTime(0) }} +
-
+
- {{ formatTime(media.duration) }} +
@@ -104,13 +138,13 @@ color="negative" flat :label="$t('reset')" - @click="resetMediaDuration(media)" + @click="resetMediaDuration()" /> @@ -173,8 +207,7 @@ (media.isAdditional && !currentSettings?.disableMediaFetching && isFileUrl(media.fileUrl)) || - media.paragraph || - media.song || + media.tag?.type || media.watched " :class="mediaTagClasses" @@ -183,14 +216,16 @@ + @@ -293,31 +327,67 @@ " class="absolute duration-slider" > - +
+
+ {{ + formatTime( + Math.max( + (mediaPlayingCurrentPosition || 0) - + (mediaCustomDuration.min || 0), + 0, + ), + ) + }} +
+
+ +
+
+ {{ + '-' + + formatTime( + Math.max( + (mediaCustomDuration.max || media.duration) - + (mediaPlayingCurrentPosition || 0), + 0, + ), + ) + }} +
+
@@ -572,20 +642,20 @@ @@ -652,7 +722,7 @@ diff --git a/src/pages/MediaPlayerPage.vue b/src/pages/MediaPlayerPage.vue index 8e58cc1311..d2b41dae28 100644 --- a/src/pages/MediaPlayerPage.vue +++ b/src/pages/MediaPlayerPage.vue @@ -90,16 +90,10 @@ import { useI18n } from 'vue-i18n'; const { t } = useI18n(); const currentState = useCurrentStateStore(); -const { - currentCongregation, - currentSettings, - mediaPlayingAction, - selectedDate, -} = storeToRefs(currentState); +const { currentCongregation, currentSettings, mediaPlayingAction } = + storeToRefs(currentState); const jwStore = useJwStore(); -const { customDurations } = storeToRefs(jwStore); - const yeartext = computed(() => jwStore.yeartext); const panzoom = ref(); @@ -160,8 +154,11 @@ whenever( }, ); -const { data: mediaUniqueId } = useBroadcastChannel({ - name: 'unique-id', +const { data: mediaCustomDuration } = useBroadcastChannel< + string | undefined, + string +>({ + name: 'custom-duration', }); const { data: mediaRepeat } = useBroadcastChannel({ @@ -322,22 +319,17 @@ const { post: postMediaState } = useBroadcastChannel<'ended', 'ended'>({ }); const customMin = computed(() => { - return ( - customDurations?.value?.[currentCongregation.value]?.[selectedDate.value]?.[ - mediaUniqueId.value - ]?.min || 0 - ); + return (JSON.parse(mediaCustomDuration.value || '{}') || {})?.min || 0; }); const customMax = computed(() => { - return customDurations?.value?.[currentCongregation.value]?.[ - selectedDate.value - ]?.[mediaUniqueId.value]?.max; + return (JSON.parse(mediaCustomDuration.value || '{}') || {})?.max; }); const endOrLoop = () => { if (!mediaRepeat.value) { postMediaState('ended'); + mediaCustomDuration.value = undefined; } else { if (mediaElement.value) { mediaElement.value.currentTime = customMin.value; @@ -365,13 +357,11 @@ const playMedia = () => { const currentTime = mediaElement.value?.currentTime || 0; postCurrentTime(currentTime); if ( - customDurations?.value?.[currentCongregation.value]?.[ - selectedDate.value - ]?.[mediaUniqueId.value] + mediaCustomDuration.value && + customMax.value && + currentTime >= customMax.value ) { - if (customMax.value && currentTime >= customMax.value) { - endOrLoop(); - } + endOrLoop(); } } catch (e) { errorCatcher(e); diff --git a/src/stores/app-settings.ts b/src/stores/app-settings.ts index 763d8a9e11..678ff3fbd6 100644 --- a/src/stores/app-settings.ts +++ b/src/stores/app-settings.ts @@ -79,12 +79,6 @@ export const useAppSettingsStore = defineStore('app-settings', { additionalMediaMaps: parseJsonSafe< Record> >(QuasarStorage.getItem('additionalMediaMaps'), {}), - customDurations: parseJsonSafe< - Record< - string, - Record> - > - >(QuasarStorage.getItem('customDurations'), {}), jwLanguages: parseJsonSafe<{ list: JwLanguage[]; updated: Date }>( QuasarStorage.getItem('jwLanguages'), { list: [], updated: new Date(0) }, @@ -109,7 +103,6 @@ export const useAppSettingsStore = defineStore('app-settings', { // Remove migrated items from localStorage [ 'additionalMediaMaps', - 'customDurations', 'jwLanguages', 'jwSongs', 'lookupPeriod', diff --git a/src/stores/jw.ts b/src/stores/jw.ts index b5b3125821..226f303ac5 100644 --- a/src/stores/jw.ts +++ b/src/stores/jw.ts @@ -50,12 +50,6 @@ interface Store { additionalMediaMaps: Partial< Record>> >; - customDurations: Partial< - Record< - string, - Partial>> - > - >; jwBibleAudioFiles: Partial< Record>> >; @@ -420,7 +414,6 @@ export const useJwStore = defineStore('jw-store', { state: (): Store => { return { additionalMediaMaps: {}, - customDurations: {}, jwBibleAudioFiles: {}, jwLanguages: { list: [], updated: oldDate }, jwSongs: {}, diff --git a/src/types/media.d.ts b/src/types/media.d.ts index 217cd34348..bfe66da9b8 100644 --- a/src/types/media.d.ts +++ b/src/types/media.d.ts @@ -34,13 +34,12 @@ export interface DynamicMediaObject { isImage: boolean; isVideo: boolean; markers?: VideoMarker[]; - paragraph?: number | string; repeat?: boolean; section: MediaSection; sectionOriginal: MediaSection; - song?: boolean | string; streamUrl?: string; subtitlesUrl?: string; + tag?: Tag | undefined; thumbnailUrl: string; title: string; uniqueId: string; @@ -70,6 +69,11 @@ export interface SongItem { title?: string; } +export interface Tag { + type: string | undefined; + value: number | string | undefined; +} + export interface VideoDuration { duration: number; ms: number; diff --git a/src/utils/time.ts b/src/utils/time.ts index 33a0542ebe..f1d6581280 100644 --- a/src/utils/time.ts +++ b/src/utils/time.ts @@ -25,3 +25,25 @@ export const formatTime = (time: number) => { return '..:..'; } }; + +export const timeToSeconds = (time: string) => { + try { + const parts = time.split(':').map(parseFloat); + if (parts.length === 3) { + // Format: hh:mm:ss + const [h, m, s] = parts; + return (h || 0) * 3600 + (m || 0) * 60 + (s || 0); + } else if (parts.length === 2) { + // Format: mm:ss + const [m, s] = parts; + // Convert minutes to hours and calculate seconds + const h = Math.floor((m || 0) / 60); + const remainingMinutes = (m || 0) % 60; + return h * 3600 + remainingMinutes * 60 + (s || 0); + } + return 0; + } catch (error) { + errorCatcher(error); + return 0; + } +};