diff --git a/.vscode/i18n-ally-reviews.yml b/.vscode/i18n-ally-reviews.yml new file mode 100644 index 00000000..b220e963 --- /dev/null +++ b/.vscode/i18n-ally-reviews.yml @@ -0,0 +1,14 @@ +# Review comments generated by i18n-ally. Please commit this file. + +reviews: + autoStartMusic: + locales: + en-US: + comments: + - user: + name: Olivier Savignac + email: sircharlo@gmail.com + id: CzFh4-s2naOGW4eTXNs-f + type: approve + comment: '' + time: '2024-06-15T18:19:27.506Z' diff --git a/.vscode/settings.json b/.vscode/settings.json index 616c5644..9f379ce8 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -90,5 +90,8 @@ "&\n currentSettings.congregationName) ||\n $t('titles.profileSelection')\n }}\n ", "&\n currentSettings ", "Meeting Media Manager" - ] + ], + "[xml]": { + "editor.defaultFormatter": "redhat.vscode-xml" + } } diff --git a/public/obs-icon.svg b/public/obs-icon.svg new file mode 100644 index 00000000..fa106b97 --- /dev/null +++ b/public/obs-icon.svg @@ -0,0 +1,7 @@ + + + + \ No newline at end of file diff --git a/src-electron/electron-main.ts b/src-electron/electron-main.ts index bcac1d1d..f832092c 100644 --- a/src-electron/electron-main.ts +++ b/src-electron/electron-main.ts @@ -149,7 +149,9 @@ function createWindow() { }); enable(mediaWindow.webContents); } - mainWindow.loadURL(process.env.APP_URL + '?page=congregation-selector'); + mainWindow.loadURL( + process.env.APP_URL + '?page=initial-congregation-selector', + ); mediaWindow.loadURL(process.env.APP_URL + '?page=media-player'); } diff --git a/src-electron/electron-preload.ts b/src-electron/electron-preload.ts index eb7e4698..ab9bfeda 100644 --- a/src-electron/electron-preload.ts +++ b/src-electron/electron-preload.ts @@ -196,5 +196,16 @@ contextBridge.exposeInMainWorld('electronApi', { }); }, path, + setMediaWindowPosition: (x: number, y: number) => { + const mediaWindow = getMediaWindow(); + if (mediaWindow) { + mediaWindow.setPosition(x, y); + } + }, + setautoStartAtLogin: (value: boolean) => { + app.setLoginItemSettings({ + openAtLogin: value, + }); + }, toggleMediaWindow, }); diff --git a/src/components/media/MusicButton.vue b/src/components/media/MusicButton.vue index 47ce1975..2ffbb647 100644 --- a/src/components/media/MusicButton.vue +++ b/src/components/media/MusicButton.vue @@ -72,7 +72,7 @@ import { storeToRefs } from 'pinia'; import { date } from 'quasar'; import { getFileUrl, getPublicationDirectoryContents } from 'src/helpers/fs'; import { formatTime } from 'src/helpers/mediaPlayback'; -import { computed, ref } from 'vue'; +import { computed, onMounted, ref } from 'vue'; import { useI18n } from 'vue-i18n'; import { useCurrentStateStore } from '../../stores/current-state'; @@ -129,7 +129,8 @@ const getNextSongUrl = () => { function playMusic() { if (!musicPlayer.value || !musicPlayerSource.value) return; - musicPlayer.value.volume = 1; + musicPlayer.value.volume = + (getSettingValue('musicVolume') as number) / 100 ?? 1; musicPlayerSource.value.src = getNextSongUrl(); musicPlayer.value.load(); musicPlayer.value.play().then(() => { @@ -158,16 +159,24 @@ function playMusic() { }; } +const meetingDay = ref(false); + +onMounted(() => { + meetingDay.value = !!selectedDateObject.value?.meeting; + if (currentSettings.value?.autoStartMusic && meetingDay.value) { + playMusic(); + } +}); + /** * Calculates the remaining time before the meeting starts based on the selected meeting day and start time settings. * * @return {string|null} The remaining time in hours and minutes, optionally formatted, or null if there is no meeting day selected. */ const remainingTimeBeforeMeetingStart = (formatted?: boolean) => { - const meetingDay = selectedDateObject.value.meeting; - if (meetingDay) { + if (meetingDay.value) { const now = new Date(); - const weMeeting = meetingDay === 'we'; + const weMeeting = selectedDateObject.value?.meeting === 'we'; const meetingStartTime = weMeeting ? (getSettingValue('weStartTime') as string) : (getSettingValue('mwStartTime') as string); @@ -192,10 +201,7 @@ const remainingTimeBeforeMeetingStart = (formatted?: boolean) => { const musicRemainingTime = computed(() => { if (!musicPlayer.value) return '..:..'; if (musicStopping.value) return ref(t('music.stopping')).value; - if ( - selectedDateObject.value.meeting && - timeRemainingBeforeMusicStop.value > 0 - ) + if (meetingDay.value && timeRemainingBeforeMusicStop.value > 0) return formatTime(timeRemainingBeforeMusicStop.value); return currentSongRemainingTime.value; }); diff --git a/src/components/media/ObsStatus.vue b/src/components/media/ObsStatus.vue index 9a15f868..678eaca4 100644 --- a/src/components/media/ObsStatus.vue +++ b/src/components/media/ObsStatus.vue @@ -6,36 +6,16 @@ rounded v-if="currentSettings.obsEnable" > - - {{ $t(obsMessage) }} + - - - - - + + {{ $t(obsMessage) }} ', }, - obsImageScene: { + obsPassword: { + actions: ['obsConnect'], depends: 'obsEnable', group: 'integrations', - list: 'obsNonStageScenes', - type: 'list', + type: 'text', }, - - obsMediaScene: { + obsPort: { + actions: ['obsConnect'], + depends: 'obsEnable', + group: 'integrations', + type: 'text', + }, + obsCameraScene: { depends: 'obsEnable', group: 'integrations', list: 'obsAllScenes', type: 'list', }, - obsPassword: { - actions: ['obsConnect'], + obsMediaScene: { depends: 'obsEnable', group: 'integrations', - type: 'text', + list: 'obsAllScenes', + type: 'list', }, - obsPort: { - actions: ['obsConnect'], + obsImageScene: { depends: 'obsEnable', group: 'integrations', - type: 'text', + list: 'obsNonStageScenes', + type: 'list', }, - preferredOutput: { + + // Advanced + + // todo: implement preferredOutput + // preferredOutput: { + // depends: 'enableMediaDisplayButton', + // group: 'advanced', + // list: 'screens', + // type: 'list', + // }, + maxRes: { depends: 'enableMediaDisplayButton', - group: 'mediaRetrievalPlayback', - list: 'screens', + group: 'advanced', + list: 'resolutions', type: 'list', }, - weDay: { - group: 'congregationMeetings', - list: 'days', - rules: ['notEmpty'], + jwlCompanionMode: { + depends: 'enableMediaDisplayButton', + group: 'advanced', + type: 'toggle', + }, + hideMediaLogo: { + depends: 'enableMediaDisplayButton', + group: 'advanced', + type: 'toggle', + }, + includePrinted: { + depends: 'enableMediaDisplayButton', + group: 'advanced', + type: 'toggle', + }, + // todo: test implementation of excludeFootnotes + excludeFootnotes: { + depends: 'enableMediaDisplayButton', + group: 'advanced', + type: 'toggle', + }, + excludeTh: { + depends: 'enableMediaDisplayButton', + group: 'advanced', + type: 'toggle', + }, + enableSubtitles: { + depends: 'enableMediaDisplayButton', + group: 'advanced', + type: 'toggle', + }, + langSubtitles: { + depends: 'enableSubtitles', + group: 'advanced', + list: 'jwLanguages', type: 'list', }, - weStartTime: { - group: 'congregationMeetings', - options: ['meetingTime'], - rules: ['notEmpty'], - type: 'time', + musicVolume: { + depends: 'enableMusicButton', + group: 'advanced', + max: 100, + min: 0, + step: 1, + type: 'slider', + }, + // todo: implement enableMusicFadeOut toggle + enableMusicFadeOut: { + depends: 'enableMusicButton', + group: 'advanced', + type: 'toggle', }, - // Advanced - // enableMp4Conversion: { // type: 'toggle', // depends: 'advanced', @@ -351,7 +356,7 @@ export const defaultSettings: SettingsValues = { // autoPlayFirst: false, // autoPlayFirstTime: 5, // autoQuitWhenDone: false, - autoRunAtBoot: false, + autoStartAtLogin: false, autoStartMusic: true, // autoStartSync: false, coWeek: '', @@ -394,7 +399,7 @@ export const defaultSettings: SettingsValues = { obsMediaScene: '', obsPassword: '', obsPort: '', - preferredOutput: '', + // preferredOutput: '', // specialCong: false, weDay: '', weStartTime: '', diff --git a/src/helpers/electron-api.ts b/src/helpers/electron-api.ts index b9290303..cd99d5a1 100644 --- a/src/helpers/electron-api.ts +++ b/src/helpers/electron-api.ts @@ -16,6 +16,7 @@ export interface ElectronApi { klawSync: typeof import('klaw-sync'); openFolderDialog: () => string[]; path: typeof import('path'); + setautoStartAtLogin: (value: boolean) => void; toggleMediaWindow: (action: string) => void; } diff --git a/src/helpers/jw-media.ts b/src/helpers/jw-media.ts index 78f3755e..01bf80f8 100644 --- a/src/helpers/jw-media.ts +++ b/src/helpers/jw-media.ts @@ -28,6 +28,7 @@ import { TableItem, VideoMarker, } from 'src/types/sqlite'; +import { useRoute } from 'vue-router'; import { useCurrentStateStore } from '../stores/current-state'; import { MAX_SONGS } from '../stores/jw'; @@ -132,9 +133,14 @@ const fetchMedia = async () => { const { currentCongregation, lookupPeriod } = storeToRefs( useCurrentStateStore(), ); + const route = useRoute(); const fetchErrors = {} as Record; for (const day of lookupPeriod.value.filter((day) => day.meeting)) { - if (!currentCongregation.value) break; + if ( + !currentCongregation.value || + !['/media-calendar', '/setup-wizard'].includes(route.fullPath) + ) + break; const dayDate = day.date; day.loading = true; let fetchResult = null; diff --git a/src/helpers/migrations.ts b/src/helpers/migrations.ts index 5ebab765..dd795c12 100644 --- a/src/helpers/migrations.ts +++ b/src/helpers/migrations.ts @@ -29,7 +29,7 @@ const parsePrefsFile = (path: string) => { const buildNewPrefsObject = (oldPrefs: OldAppConfig) => { const newPrefsObject: SettingsValues = { - autoRunAtBoot: oldPrefs.app.autoRunAtBoot || false, + autoStartAtLogin: oldPrefs.app.autoRunAtBoot || false, autoStartMusic: oldPrefs.meeting.autoStartMusic || true, coWeek: oldPrefs.meeting.coWeek || '', congregationName: oldPrefs.app.congregationName || '', @@ -57,7 +57,7 @@ const buildNewPrefsObject = (oldPrefs: OldAppConfig) => { obsMediaScene: oldPrefs.app.obs.mediaScene || '', obsPassword: oldPrefs.app.obs.password || '', obsPort: oldPrefs.app.obs.port?.toString() || '', - preferredOutput: oldPrefs.media.preferredOutput || '', + // preferredOutput: oldPrefs.media.preferredOutput || '', weDay: oldPrefs.meeting.weDay?.toString() || '', weStartTime: oldPrefs.meeting.weStartTime?.toString() || '', }; diff --git a/src/i18n/en-US/index.json b/src/i18n/en-US/index.json index c6c56d13..3829b415 100644 --- a/src/i18n/en-US/index.json +++ b/src/i18n/en-US/index.json @@ -11,8 +11,8 @@ "are-you-using-this-app-at-a-kingdom-hall-if-so-well-configure-a-few-things-for-you-right-off-the-bat": "Are you using this app at a Kingdom Hall? If so, we'll configure a few things for you right off the bat.", "areYouSure": "Are you sure?", "automatic": "Automatic", - "autoRunAtBoot": "Run app at system start-up", - "autoStartMusic": "Play background music automatically before meetings", + "autoStartAtLogin": "Start M³ automatically after logging in", + "autoStartMusic": "Start background music before meetings automatically", "back": "Back", "betaUpdates": "Enable beta updates for testing purposes (Note: Do not enable this setting on the computer used to present media at the Kingdom Hall.)", "cancel": "Cancel", @@ -39,8 +39,8 @@ "does-your-kingdom-hall-use-a-program-called-obs-studio": "Does your Kingdom Hall use a program called OBS Studio?", "doing-so-will-greatly-simplify-and-facilitate-sharing-media-during-hybrid-meetings": "Doing so will greatly simplify and facilitate sharing media during hybrid meetings.", "drop-multimedia-files-here-to-add-them-to-the-list-for-this-day": "Drop multimedia files here to add them to the list for this day.", - "enableMediaDisplayButton": "Present media on an external monitor or in a separate window", - "enableMusicButton": "Enable button to play background music", + "enableMediaDisplayButton": "Enable media playback", + "enableMusicButton": "Enable background music playback", "enter-the-port-and-password-configured-in-obs-studios-websocket-plugin": "Enter the port and password configured in OBS Studio's Websocket plugin.", "entireFile": "Play from start to finish", "error": "Error", @@ -167,5 +167,7 @@ "please-wait-downloading-media-for-this-day": "Please wait, downloading media for this day...", "noDateSelected": "No date is currently selected. Please select a date before continuing.", "welcome-to-mmm": "Welcome to M³!", - "successfully-migrated-from-the-previous-version": "Successfully migrated from the previous version" + "successfully-migrated-from-the-previous-version": "Successfully migrated from the previous version", + "depends-on": "Linked to:", + "setup-wizard": "Setup Wizard" } diff --git a/src/i18n/fr-CA/index.json b/src/i18n/fr-CA/index.json index df778d4f..c0a9fd23 100644 --- a/src/i18n/fr-CA/index.json +++ b/src/i18n/fr-CA/index.json @@ -6,7 +6,7 @@ "areYouSure": "Êtes-vous sûr ?", "automatic": "Automatique", "autoPlayFirst": "Lecture automatique du premier média", - "autoRunAtBoot": "Lancer l'appli automatiquement au démarrage de l'ordinateur", + "autoStartAtLogin": "Démarrez M³ automatiquement après l'ouverture de session", "autoStartMusic": "Jouer automatiquement la musique de fond avant les réunions", "autoStartSync": "Lancer la synchronisation automatiquement", "back": "Retour", @@ -41,8 +41,8 @@ "doing-so-will-greatly-simplify-and-facilitate-sharing-media-during-hybrid-meetings": "Cela simplifiera et facilitera grandement le partage de médias lors de réunions hybrides.", "dontForgetToGetMedia": "N'oubliez pas de rafraîchir les dossiers multimédias sur l'ordinateur utilisé pour présenter les médias lors des réunions de l'assemblée locale !", "downloadShuffleMusic": "Télécharger tous les cantiques pour la musique de fond", - "enableMediaDisplayButton": "Présenter les médias sur un écran externe ou dans une fenêtre séparée", - "enableMusicButton": "Activer le bouton pour jouer de la musique de fond", + "enableMediaDisplayButton": "Activer la fonction de lecture des médias", + "enableMusicButton": "Activer la fonction de musique de fond", "enableObs": "Activer le mode de compatibilité avec OBS Studio (nécessite obs-websocket)", "enablePp": "Activer les raccourcis clavier pendant la lecture des médias (à utiliser avec une télécommande USB, par exemple)", "enableSubtitles": "Activer les sous-titres pour les vidéos", @@ -328,5 +328,6 @@ "please-wait-downloading-media-for-this-day": "Veuillez patienter, téléchargement des médias pour cette journée en cours...", "noDateSelected": "Aucune date n'est actuellement sélectionnée. Veuillez sélectionner une date avant de continuer.", "welcome-to-mmm": "Bienvenue à M³ !", - "successfully-migrated-from-the-previous-version": "Migré avec succès depuis la version précédente" + "successfully-migrated-from-the-previous-version": "Migré avec succès depuis la version précédente", + "depends-on": "Relié à :" } diff --git a/src/layouts/MainLayout.vue b/src/layouts/MainLayout.vue index 6fd31b7b..569a53b8 100644 --- a/src/layouts/MainLayout.vue +++ b/src/layouts/MainLayout.vue @@ -277,6 +277,7 @@ import { useAppSettingsStore } from '../stores/app-settings'; import { useCongregationSettingsStore } from '../stores/congregation-settings'; import { useCurrentStateStore } from '../stores/current-state'; import { useJwStore } from '../stores/jw'; +//electronapi // Store and router initializations @@ -320,7 +321,7 @@ jwStore.$subscribe((_, state) => { // Ref and reactive initializations const chooseSong = ref(false); const mediaSortForDay = ref(true); -const { toggleMediaWindow } = electronApi; +const { setautoStartAtLogin, toggleMediaWindow } = electronApi; const { locale, t } = useI18n({ useScope: 'global' }); const drawer = ref(true); @@ -351,14 +352,15 @@ watch(route, (newVal) => { ); }); -watch(currentSettings, (newSettings) => { - console.log('currentSettings changed', newSettings); - if (!newSettings) { - if (route.fullPath !== '/congregation-selector') { +const navigateToCongregationSelector = () => { + if (route.fullPath !== '/congregation-selector') { router.push({ path: '/congregation-selector' }); - return; } - } +}; + +watch(currentSettings, (newSettings) => { + console.log('currentSettings changed', newSettings); + if (!newSettings) navigateToCongregationSelector(); }); watch( @@ -403,6 +405,13 @@ watch( }, ); +watch( + () => currentSettings.value?.autoStartAtLogin, + (newautoStartAtLogin) => { + setautoStartAtLogin(!!newautoStartAtLogin); + }, +); + const dateOptions = (lookupDate: string) => { const dateArray: Date[] = lookupPeriod.value.map((day) => day.date); // @ts-expect-error "A spread argument must either have a tuple type or be passed to a rest parameter." @@ -455,5 +464,7 @@ if (!migrations.value.includes('firstRun')) { onMounted(() => { document.title = 'Meeting Media Manager'; + console.log(currentSettings.value) + if (!currentSettings.value) navigateToCongregationSelector(); }); diff --git a/src/pages/CongregationSelectorPage.vue b/src/pages/CongregationSelectorPage.vue index f7cee458..be71526f 100644 --- a/src/pages/CongregationSelectorPage.vue +++ b/src/pages/CongregationSelectorPage.vue @@ -98,7 +98,7 @@ function chooseCongregation( } const isHomePage = computed(() => { - return route.path === '/'; + return route.path === '/initial-congregation-selector'; }); function createNewCongregation() { diff --git a/src/pages/SettingsPage.vue b/src/pages/SettingsPage.vue index 4fcf4b54..3302cad3 100644 --- a/src/pages/SettingsPage.vue +++ b/src/pages/SettingsPage.vue @@ -38,16 +38,35 @@ - + {{ $t(settingId) }} - - + + + + {{ $t('depends-on') }} + {{ $t(item.depends) }} + + + + + import('layouts/RouteHelper.vue'), path: '/', }, - { + alias: ['/initial-congregation-selector'], children: [ { component: () => import('pages/CongregationSelectorPage.vue'), @@ -17,7 +17,6 @@ const routes: RouteRecordRaw[] = [ meta: { title: 'titles.profileSelection' }, path: '/congregation-selector', }, - { children: [ { component: () => import('pages/MediaCalendarPage.vue'), path: '' }, @@ -26,7 +25,6 @@ const routes: RouteRecordRaw[] = [ meta: { title: 'titles.mediaCalendar' }, path: '/media-calendar', }, - { children: [ { component: () => import('pages/MediaPlayerPage.vue'), path: '' }, @@ -38,7 +36,7 @@ const routes: RouteRecordRaw[] = [ { children: [{ component: () => import('pages/SetupWizard.vue'), path: '' }], component: () => import('layouts/MainLayout.vue'), - meta: { title: 'Setup Wizard' }, + meta: { title: 'setup-wizard' }, path: '/setup-wizard', }, { diff --git a/src/types/settings.ts b/src/types/settings.ts index d348726a..59d10f5a 100644 --- a/src/types/settings.ts +++ b/src/types/settings.ts @@ -3,7 +3,7 @@ export interface SettingsValues { // autoPlayFirst: boolean; // autoPlayFirstTime: number; // autoQuitWhenDone: boolean; - autoRunAtBoot: boolean; + autoStartAtLogin: boolean; autoStartMusic: boolean; // autoStartSync: boolean; coWeek: string; @@ -47,7 +47,7 @@ export interface SettingsValues { obsMediaScene: string; obsPassword: string; obsPort: string; - preferredOutput: string; + // preferredOutput: string; // [key: string]: unknown; weDay: string; weStartTime: string; @@ -57,6 +57,7 @@ export interface SettingsItem { actions?: string[]; depends?: string; group: string; + icon?: string; list?: string; max?: number; min?: number;