From 071444a9e735d2440005b6dc8b3e6bac761b4a46 Mon Sep 17 00:00:00 2001 From: mfcar Date: Mon, 27 Feb 2023 18:04:26 +0000 Subject: [PATCH] Improve dates, times and schedule backup info --- client/components/cards/LazyBookCard.vue | 4 +- client/components/modals/BookmarksModal.vue | 10 ++- .../modals/ListeningSessionModal.vue | 12 +++- client/components/tables/BackupsTable.vue | 14 ++-- client/components/tables/UsersTable.vue | 14 ++-- .../tables/podcast/EpisodeTableRow.vue | 7 +- .../tables/podcast/EpisodesTable.vue | 12 +++- .../widgets/CronExpressionBuilder.vue | 9 +++ client/pages/config/backups.vue | 48 +++++++++----- client/pages/config/index.vue | 23 ++++++- client/pages/config/sessions.vue | 12 +++- client/pages/config/users/_id/index.vue | 10 ++- client/pages/config/users/_id/sessions.vue | 12 +++- client/pages/item/_id/index.vue | 2 +- .../pages/library/_library/podcast/latest.vue | 7 +- client/plugins/init.client.js | 18 ++++- client/plugins/utils.js | 65 ++++++++++++++++++- client/store/globals.js | 26 ++++++++ client/strings/de.json | 6 +- client/strings/en-us.json | 4 ++ client/strings/es.json | 4 ++ client/strings/fr.json | 4 ++ client/strings/hr.json | 4 ++ client/strings/it.json | 4 ++ client/strings/pl.json | 4 ++ client/strings/ru.json | 4 ++ client/strings/zh-cn.json | 4 ++ server/objects/settings/ServerSettings.js | 5 +- 28 files changed, 295 insertions(+), 53 deletions(-) diff --git a/client/components/cards/LazyBookCard.vue b/client/components/cards/LazyBookCard.vue index fce597cac..435d211f0 100644 --- a/client/components/cards/LazyBookCard.vue +++ b/client/components/cards/LazyBookCard.vue @@ -740,7 +740,7 @@ export default { episodeId: this.recentEpisode.id, title: this.recentEpisode.title, subtitle: this.mediaMetadata.title, - caption: this.recentEpisode.publishedAt ? `Published ${this.$formatDate(this.recentEpisode.publishedAt, 'MMM do, yyyy')}` : 'Unknown publish date', + caption: this.recentEpisode.publishedAt ? `Published ${this.$formatDate(this.recentEpisode.publishedAt, this.dateFormat)}` : 'Unknown publish date', duration: this.recentEpisode.audioFile.duration || null, coverPath: this.media.coverPath || null } @@ -864,7 +864,7 @@ export default { episodeId: episode.id, title: episode.title, subtitle: this.mediaMetadata.title, - caption: episode.publishedAt ? `Published ${this.$formatDate(episode.publishedAt, 'MMM do, yyyy')}` : 'Unknown publish date', + caption: episode.publishedAt ? `Published ${this.$formatDate(episode.publishedAt, this.dateFormat)}` : 'Unknown publish date', duration: episode.audioFile.duration || null, coverPath: this.media.coverPath || null }) diff --git a/client/components/modals/BookmarksModal.vue b/client/components/modals/BookmarksModal.vue index de76c4c1f..1bf3748da 100644 --- a/client/components/modals/BookmarksModal.vue +++ b/client/components/modals/BookmarksModal.vue @@ -73,6 +73,12 @@ export default { }, canCreateBookmark() { return !this.bookmarks.find((bm) => bm.time === this.currentTime) + }, + dateFormat() { + return this.$store.state.serverSettings.dateFormat + }, + timeFormat() { + return this.$store.state.serverSettings.timeFormat } }, methods: { @@ -111,7 +117,7 @@ export default { }, submitCreateBookmark() { if (!this.newBookmarkTitle) { - this.newBookmarkTitle = this.$formatDate(Date.now(), 'MMM dd, yyyy HH:mm') + this.newBookmarkTitle = this.$formatDatetime(Date.now(), this.dateFormat, this.timeFormat) } var bookmark = { title: this.newBookmarkTitle, @@ -134,4 +140,4 @@ export default { } } } - \ No newline at end of file + diff --git a/client/components/modals/ListeningSessionModal.vue b/client/components/modals/ListeningSessionModal.vue index fb435107d..79e66cc76 100644 --- a/client/components/modals/ListeningSessionModal.vue +++ b/client/components/modals/ListeningSessionModal.vue @@ -19,13 +19,13 @@
{{ $strings.LabelStartedAt }}
- {{ $formatDate(_session.startedAt, 'MMMM do, yyyy HH:mm') }} + {{ $formatDatetime(_session.startedAt, dateFormat, timeFormat) }}
{{ $strings.LabelUpdatedAt }}
- {{ $formatDate(_session.updatedAt, 'MMMM do, yyyy HH:mm') }} + {{ $formatDatetime(_session.updatedAt, dateFormat, timeFormat) }}
@@ -151,6 +151,12 @@ export default { else if (playMethod === this.$constants.PlayMethod.DIRECTSTREAM) return 'Direct Stream' else if (playMethod === this.$constants.PlayMethod.LOCAL) return 'Local' return 'Unknown' + }, + dateFormat() { + return this.$store.state.serverSettings.dateFormat + }, + timeFormat() { + return this.$store.state.serverSettings.timeFormat } }, methods: { @@ -186,4 +192,4 @@ export default { }, mounted() {} } - \ No newline at end of file + diff --git a/client/components/tables/BackupsTable.vue b/client/components/tables/BackupsTable.vue index 98f017ad3..6df009b82 100644 --- a/client/components/tables/BackupsTable.vue +++ b/client/components/tables/BackupsTable.vue @@ -17,7 +17,7 @@

/{{ backup.path.replace(/\\/g, '/') }}

- {{ backup.datePretty }} + {{ $formatDatetime(backup.createdAt, dateFormat, timeFormat) }} {{ $bytesPretty(backup.fileSize) }}
@@ -46,7 +46,7 @@

{{ $strings.MessageImportantNotice }}

-

{{ $strings.MessageRestoreBackupConfirm }} {{ selectedBackup.datePretty }}?

+

{{ $strings.MessageRestoreBackupConfirm }} {{ $formatDatetime(selectedBackup.createdAt, dateFormat, timeFormat) }}?

{{ $strings.ButtonNevermind }}
@@ -71,6 +71,12 @@ export default { computed: { userToken() { return this.$store.getters['user/getToken'] + }, + dateFormat() { + return this.$store.state.serverSettings.dateFormat + }, + timeFormat() { + return this.$store.state.serverSettings.timeFormat } }, methods: { @@ -90,7 +96,7 @@ export default { }) }, deleteBackupClick(backup) { - if (confirm(this.$getString('MessageConfirmDeleteBackup', [backup.datePretty]))) { + if (confirm(this.$getString('MessageConfirmDeleteBackup', [this.$formatDatetime(backup.createdAt, this.dateFormat, this.timeFormat)]))) { this.processing = true this.$axios .$delete(`/api/backups/${backup.id}`) @@ -208,4 +214,4 @@ export default { padding-bottom: 5px; background-color: #333; } - \ No newline at end of file + diff --git a/client/components/tables/UsersTable.vue b/client/components/tables/UsersTable.vue index 79876ca32..88323a74c 100644 --- a/client/components/tables/UsersTable.vue +++ b/client/components/tables/UsersTable.vue @@ -25,13 +25,13 @@
- + {{ $dateDistanceFromNow(user.lastSeen) }} - - {{ $formatDate(user.createdAt, 'MMM d, yyyy') }} + + {{ $formatDate(user.createdAt, dateFormat) }} @@ -74,6 +74,12 @@ export default { var usermap = {} this.$store.state.users.usersOnline.forEach((u) => (usermap[u.id] = u)) return usermap + }, + dateFormat() { + return this.$store.state.serverSettings.dateFormat + }, + timeFormat() { + return this.$store.state.serverSettings.timeFormat } }, methods: { @@ -201,4 +207,4 @@ export default { padding-bottom: 5px; background-color: #272727; } - \ No newline at end of file + diff --git a/client/components/tables/podcast/EpisodeTableRow.vue b/client/components/tables/podcast/EpisodeTableRow.vue index a184b3c89..d2436316b 100644 --- a/client/components/tables/podcast/EpisodeTableRow.vue +++ b/client/components/tables/podcast/EpisodeTableRow.vue @@ -11,7 +11,7 @@

Season #{{ episode.season }}

Episode #{{ episode.episode }}

-

Published {{ $formatDate(publishedAt, 'MMM do, yyyy') }}

+

Published {{ $formatDate(publishedAt, dateFormat) }}

@@ -128,6 +128,9 @@ export default { }, publishedAt() { return this.episode.publishedAt + }, + dateFormat() { + return this.$store.state.serverSettings.dateFormat } }, methods: { @@ -205,4 +208,4 @@ export default { } } } - \ No newline at end of file + diff --git a/client/components/tables/podcast/EpisodesTable.vue b/client/components/tables/podcast/EpisodesTable.vue index 39228a39e..1f0baf351 100644 --- a/client/components/tables/podcast/EpisodesTable.vue +++ b/client/components/tables/podcast/EpisodesTable.vue @@ -143,6 +143,12 @@ export default { var itemProgress = this.$store.getters['user/getUserMediaProgress'](this.libraryItem.id, episode.id) return !itemProgress || !itemProgress.isFinished }) + }, + dateFormat() { + return this.$store.state.serverSettings.dateFormat + }, + timeFormat() { + return this.$store.state.serverSettings.timeFormat } }, methods: { @@ -195,7 +201,7 @@ export default { episodeId: episode.id, title: episode.title, subtitle: this.mediaMetadata.title, - caption: episode.publishedAt ? `Published ${this.$formatDate(episode.publishedAt, 'MMM do, yyyy')}` : 'Unknown publish date', + caption: episode.publishedAt ? `Published ${this.$formatDate(episode.publishedAt, this.dateFormat)}` : 'Unknown publish date', duration: episode.audioFile.duration || null, coverPath: this.media.coverPath || null } @@ -263,7 +269,7 @@ export default { episodeId: episode.id, title: episode.title, subtitle: this.mediaMetadata.title, - caption: episode.publishedAt ? `Published ${this.$formatDate(episode.publishedAt, 'MMM do, yyyy')}` : 'Unknown publish date', + caption: episode.publishedAt ? `Published ${this.$formatDate(episode.publishedAt, this.dateFormat)}` : 'Unknown publish date', duration: episode.audioFile.duration || null, coverPath: this.media.coverPath || null }) @@ -314,4 +320,4 @@ export default { .episode-leave-active { position: absolute; } - \ No newline at end of file + diff --git a/client/components/widgets/CronExpressionBuilder.vue b/client/components/widgets/CronExpressionBuilder.vue index 168ff1d09..04ec2fd1a 100644 --- a/client/components/widgets/CronExpressionBuilder.vue +++ b/client/components/widgets/CronExpressionBuilder.vue @@ -36,6 +36,10 @@

{{ $strings.MessageValidCronExpression }}

+
+ event +

{{ $strings.LabelNextScheduledRun }}: {{ nextRun }}

+
@@ -78,6 +82,11 @@ export default { hourIsValid() { return !(isNaN(this.selectedHour) || this.selectedHour === '' || this.selectedHour < 0 || this.selectedHour > 23) }, + nextRun() { + if (!this.cronExpression) return '' + const parsed = this.$getNextScheduledDate(this.cronExpression) + return this.$formatJsDatetime(parsed, this.$store.state.serverSettings.dateFormat, this.$store.state.serverSettings.timeFormat) || '' + }, description() { if ((this.selectedInterval !== 'custom' || !this.selectedWeekdays.length) && this.selectedInterval !== 'daily') return '' diff --git a/client/pages/config/backups.vue b/client/pages/config/backups.vue index c263119a7..8c936ded6 100644 --- a/client/pages/config/backups.vue +++ b/client/pages/config/backups.vue @@ -9,10 +9,17 @@
-
- schedule -

{{ scheduleDescription }}

- edit +
+ schedule +
{{ $strings.HeaderSchedule }}:
+
{{ scheduleDescription }}
+ edit +
+ +
+ event +
{{ $strings.LabelNextBackupDate }}:
+
{{ nextBackupDate }}
@@ -64,10 +71,21 @@ export default { serverSettings() { return this.$store.state.serverSettings }, + dateFormat() { + return this.serverSettings.dateFormat + }, + timeFormat() { + return this.serverSettings.timeFormat + }, scheduleDescription() { if (!this.cronExpression) return '' const parsed = this.$parseCronExpression(this.cronExpression) - return parsed ? parsed.description : 'Custom cron expression ' + this.cronExpression + return parsed ? parsed.description : `${this.$strings.LabelCustomCronExpression} ${this.cronExpression}` + }, + nextBackupDate() { + if (!this.cronExpression) return '' + const parsed = this.$getNextScheduledDate(this.cronExpression) + return this.$formatJsDatetime(parsed, this.dateFormat, this.timeFormat) || '' } }, methods: { @@ -90,15 +108,15 @@ export default { updateServerSettings(payload) { this.updatingServerSettings = true this.$store - .dispatch('updateServerSettings', payload) - .then((success) => { - console.log('Updated Server Settings', success) - this.updatingServerSettings = false - }) - .catch((error) => { - console.error('Failed to update server settings', error) - this.updatingServerSettings = false - }) + .dispatch('updateServerSettings', payload) + .then((success) => { + console.log('Updated Server Settings', success) + this.updatingServerSettings = false + }) + .catch((error) => { + console.error('Failed to update server settings', error) + this.updatingServerSettings = false + }) }, initServerSettings() { this.newServerSettings = this.serverSettings ? { ...this.serverSettings } : {} @@ -113,4 +131,4 @@ export default { this.initServerSettings() } } - \ No newline at end of file + diff --git a/client/pages/config/index.vue b/client/pages/config/index.vue index 64f43e974..e354edba2 100644 --- a/client/pages/config/index.vue +++ b/client/pages/config/index.vue @@ -68,8 +68,14 @@
-
+
+ {{ dateExample }} +
+ +
+ + {{ timeExample }}
@@ -293,7 +299,18 @@ export default { }, dateFormats() { return this.$store.state.globals.dateFormats - } + }, + timeFormats() { + return this.$store.state.globals.timeFormats + }, + dateExample() { + const date = new Date(2014, 2, 25); + return this.$formatJsDate(date, this.newServerSettings.dateFormat) + }, + timeExample() { + const date = new Date(2014, 2, 25, 17, 30, 0); + return this.$formatJsTime(date, this.newServerSettings.timeFormat) + }, }, methods: { updateSortingPrefixes(val) { @@ -420,4 +437,4 @@ export default { this.initServerSettings() } } - \ No newline at end of file + diff --git a/client/pages/config/sessions.vue b/client/pages/config/sessions.vue index a3213c81f..c31ce2cd6 100644 --- a/client/pages/config/sessions.vue +++ b/client/pages/config/sessions.vue @@ -39,7 +39,7 @@

{{ $secondsToTimestamp(session.currentTime) }}

- +

{{ $dateDistanceFromNow(session.updatedAt) }}

@@ -105,6 +105,12 @@ export default { if (!this.userFilter) return null var user = this.users.find((u) => u.id === this.userFilter) return user ? user.username : null + }, + dateFormat() { + return this.$store.state.serverSettings.dateFormat + }, + timeFormat() { + return this.$store.state.serverSettings.timeFormat } }, methods: { @@ -149,7 +155,7 @@ export default { episodeId: episode.id, title: episode.title, subtitle: libraryItem.media.metadata.title, - caption: episode.publishedAt ? `Published ${this.$formatDate(episode.publishedAt, 'MMM do, yyyy')}` : 'Unknown publish date', + caption: episode.publishedAt ? `Published ${this.$formatDate(episode.publishedAt, this.dateFormat)}` : 'Unknown publish date', duration: episode.audioFile.duration || null, coverPath: libraryItem.media.coverPath || null } @@ -266,4 +272,4 @@ export default { padding: 4px 8px; font-size: 0.75rem; } - \ No newline at end of file + diff --git a/client/pages/config/users/_id/index.vue b/client/pages/config/users/_id/index.vue index eb0412582..ee136b437 100644 --- a/client/pages/config/users/_id/index.vue +++ b/client/pages/config/users/_id/index.vue @@ -79,12 +79,12 @@

{{ Math.floor(item.progress * 100) }}%

- +

{{ $dateDistanceFromNow(item.startedAt) }}

- +

{{ $dateDistanceFromNow(item.lastUpdate) }}

@@ -149,6 +149,12 @@ export default { latestSession() { if (!this.listeningSessions.sessions || !this.listeningSessions.sessions.length) return null return this.listeningSessions.sessions[0] + }, + dateFormat() { + return this.$store.state.serverSettings.dateFormat + }, + timeFormat() { + return this.$store.state.serverSettings.timeFormat } }, methods: { diff --git a/client/pages/config/users/_id/sessions.vue b/client/pages/config/users/_id/sessions.vue index 7377a91dc..4dd4e643d 100644 --- a/client/pages/config/users/_id/sessions.vue +++ b/client/pages/config/users/_id/sessions.vue @@ -46,7 +46,7 @@

{{ $secondsToTimestamp(session.currentTime) }}

- +

{{ $dateDistanceFromNow(session.updatedAt) }}

@@ -96,6 +96,12 @@ export default { }, userOnline() { return this.$store.getters['users/getIsUserOnline'](this.user.id) + }, + dateFormat() { + return this.$store.state.serverSettings.dateFormat + }, + timeFormat() { + return this.$store.state.serverSettings.timeFormat } }, methods: { @@ -140,7 +146,7 @@ export default { episodeId: episode.id, title: episode.title, subtitle: libraryItem.media.metadata.title, - caption: episode.publishedAt ? `Published ${this.$formatDate(episode.publishedAt, 'MMM do, yyyy')}` : 'Unknown publish date', + caption: episode.publishedAt ? `Published ${this.$formatDate(episode.publishedAt, this.dateFormat)}` : 'Unknown publish date', duration: episode.audioFile.duration || null, coverPath: libraryItem.media.coverPath || null } @@ -252,4 +258,4 @@ export default { padding: 4px 8px; font-size: 0.75rem; } - \ No newline at end of file + diff --git a/client/pages/item/_id/index.vue b/client/pages/item/_id/index.vue index 684602f41..c77bca40d 100644 --- a/client/pages/item/_id/index.vue +++ b/client/pages/item/_id/index.vue @@ -638,7 +638,7 @@ export default { episodeId: episode.id, title: episode.title, subtitle: this.title, - caption: episode.publishedAt ? `Published ${this.$formatDate(episode.publishedAt, 'MMM do, yyyy')}` : 'Unknown publish date', + caption: episode.publishedAt ? `Published ${this.$formatDate(episode.publishedAt, this.dateFormat)}` : 'Unknown publish date', duration: episode.audioFile.duration || null, coverPath: this.libraryItem.media.coverPath || null }) diff --git a/client/pages/library/_library/podcast/latest.vue b/client/pages/library/_library/podcast/latest.vue index 344b327e1..c9f0d2561 100644 --- a/client/pages/library/_library/podcast/latest.vue +++ b/client/pages/library/_library/podcast/latest.vue @@ -117,6 +117,9 @@ export default { if (i.episodeId) episodeIds[i.episodeId] = true }) return episodeIds + }, + dateFormat() { + return this.$store.state.serverSettings.dateFormat } }, methods: { @@ -160,7 +163,7 @@ export default { episodeId: episode.id, title: episode.title, subtitle: episode.podcast.metadata.title, - caption: episode.publishedAt ? `Published ${this.$formatDate(episode.publishedAt, 'MMM do, yyyy')}` : 'Unknown publish date', + caption: episode.publishedAt ? `Published ${this.$formatDate(episode.publishedAt, this.dateFormat)}` : 'Unknown publish date', duration: episode.duration || null, coverPath: episode.podcast.coverPath || null }) @@ -198,7 +201,7 @@ export default { episodeId: episode.id, title: episode.title, subtitle: episode.podcast.metadata.title, - caption: episode.publishedAt ? `Published ${this.$formatDate(episode.publishedAt, 'MMM do, yyyy')}` : 'Unknown publish date', + caption: episode.publishedAt ? `Published ${this.$formatDate(episode.publishedAt, this.dateFormat)}` : 'Unknown publish date', duration: episode.duration || null, coverPath: episode.podcast.coverPath || null } diff --git a/client/plugins/init.client.js b/client/plugins/init.client.js index 553d9b2e1..ea6db96a6 100644 --- a/client/plugins/init.client.js +++ b/client/plugins/init.client.js @@ -23,6 +23,22 @@ Vue.prototype.$formatJsDate = (jsdate, fnsFormat = 'MM/dd/yyyy HH:mm') => { if (!jsdate || !isDate(jsdate)) return '' return format(jsdate, fnsFormat) } +Vue.prototype.$formatTime = (unixms, fnsFormat = 'HH:mm') => { + if (!unixms) return '' + return format(unixms, fnsFormat) +} +Vue.prototype.$formatJsTime = (jsdate, fnsFormat = 'HH:mm') => { + if (!jsdate || !isDate(jsdate)) return '' + return format(jsdate, fnsFormat) +} +Vue.prototype.$formatDatetime = (unixms, fnsDateFormart = 'MM/dd/yyyy', fnsTimeFormat = 'HH:mm') => { + if (!unixms) return '' + return format(unixms, `${fnsDateFormart} ${fnsTimeFormat}`) +} +Vue.prototype.$formatJsDatetime = (jsdate, fnsDateFormart = 'MM/dd/yyyy', fnsTimeFormat = 'HH:mm') => { + if (!jsdate || !isDate(jsdate)) return '' + return format(jsdate, `${fnsDateFormart} ${fnsTimeFormat}`) +} Vue.prototype.$addDaysToToday = (daysToAdd) => { var date = addDays(new Date(), daysToAdd) if (!date || !isDate(date)) return null @@ -167,4 +183,4 @@ export default ({ app, store }, inject) => { inject('isDev', process.env.NODE_ENV !== 'production') store.commit('setRouterBasePath', app.$config.routerBasePath) -} \ No newline at end of file +} diff --git a/client/plugins/utils.js b/client/plugins/utils.js index 76dda3c9b..57982c0f9 100644 --- a/client/plugins/utils.js +++ b/client/plugins/utils.js @@ -136,6 +136,47 @@ Vue.prototype.$parseCronExpression = (expression) => { } } +Vue.prototype.$getNextScheduledDate = (expression) => { + const cronFields = expression.split(' '); + const currentDate = new Date(); + const nextDate = new Date(); + + // Calculate next minute + const minute = cronFields[0]; + const currentMinute = currentDate.getMinutes(); + const nextMinute = getNextValue(minute, currentMinute, 59); + nextDate.setMinutes(nextMinute); + + // Calculate next hour + const hour = cronFields[1]; + const currentHour = currentDate.getHours(); + const nextHour = getNextValue(hour, currentHour, 23); + nextDate.setHours(nextHour); + + // Calculate next day of month + const dayOfMonth = cronFields[2]; + const currentDayOfMonth = currentDate.getDate(); + const lastDayOfMonth = new Date(currentDate.getFullYear(), currentDate.getMonth() + 1, 0).getDate(); + const nextDayOfMonth = getNextValue(dayOfMonth, currentDayOfMonth, lastDayOfMonth); + nextDate.setDate(nextDayOfMonth); + + // Calculate next month + const month = cronFields[3]; + const currentMonth = currentDate.getMonth() + 1; + const nextMonth = getNextValue(month, currentMonth, 12); + nextDate.setMonth(nextMonth - 1); + + // Calculate next day of week + const dayOfWeek = cronFields[4]; + const currentDayOfWeek = currentDate.getDay(); + const nextDayOfWeek = getNextValue(dayOfWeek, currentDayOfWeek, 6); + const daysUntilNextDayOfWeek = getNextDaysUntilDayOfWeek(currentDate, nextDayOfWeek); + nextDate.setDate(currentDate.getDate() + daysUntilNextDayOfWeek); + + // Return the next scheduled date + return nextDate; +} + export function supplant(str, subs) { // source: http://crockford.com/javascript/remedial.html return str.replace(/{([^{}]*)}/g, @@ -144,4 +185,26 @@ export function supplant(str, subs) { return typeof r === 'string' || typeof r === 'number' ? r : a } ) -} \ No newline at end of file +} + +function getNextValue(cronField, currentValue, maxValue) { + if (cronField === '*') { + return currentValue + 1 <= maxValue ? currentValue + 1 : 0; + } + const values = cronField.split(','); + const len = values.length; + let nextValue = parseInt(values[0]); + for (let i = 0; i < len; i++) { + const value = parseInt(values[i]); + if (value > currentValue) { + nextValue = value; + break; + } + } + return nextValue <= maxValue ? nextValue : parseInt(values[0]); +} + +function getNextDaysUntilDayOfWeek(currentDate, nextDayOfWeek) { + const daysUntilNextDayOfWeek = (nextDayOfWeek + 7 - currentDate.getDay()) % 7; + return daysUntilNextDayOfWeek === 0 ? 7 : daysUntilNextDayOfWeek; +} diff --git a/client/store/globals.js b/client/store/globals.js index 953398c1c..c3c2e4b76 100644 --- a/client/store/globals.js +++ b/client/store/globals.js @@ -35,6 +35,32 @@ export const state = () => ({ { text: 'YYYY-MM-DD', value: 'yyyy-MM-dd' + }, + { + text: 'MMM do, yyyy', + value: 'MMM do, yyyy' + }, + { + text: 'MMMM do, yyyy', + value: 'MMMM do, yyyy' + }, + { + text: 'dd MMM yyyy', + value: 'dd MMM yyyy' + }, + { + text: 'dd MMMM yyyy', + value: 'dd MMMM yyyy' + } + ], + timeFormats: [ + { + text: 'h:mma (am/pm)', + value: 'h:mma' + }, + { + text: 'HH:mm (24-hour)', + value: 'HH:mm' } ], podcastTypes: [ diff --git a/client/strings/de.json b/client/strings/de.json index 57915ccdf..d46fb3fbd 100644 --- a/client/strings/de.json +++ b/client/strings/de.json @@ -195,6 +195,7 @@ "LabelCronExpression": "Cron Ausdruck", "LabelCurrent": "Aktuell", "LabelCurrently": "Aktuell:", + "LabelCustomCronExpression": "Custom Cron Expression:", "LabelDatetime": "Datum & Uhrzeit", "LabelDescription": "Beschreibung", "LabelDeselectAll": "Alles abwählen", @@ -273,6 +274,8 @@ "LabelNewestAuthors": "Neuste Autoren", "LabelNewestEpisodes": "Neueste Episoden", "LabelNewPassword": "Neues Passwort", + "LabelNextBackupDate": "Next backup date", + "LabelNextScheduledRun": "Next scheduled run", "LabelNotes": "Hinweise", "LabelNotFinished": "nicht beendet", "LabelNotificationAppriseURL": "Apprise URL(s)", @@ -365,6 +368,7 @@ "LabelSettingsStoreCoversWithItemHelp": "Standardmäßig werden die Titelbilder in /metadata/items gespeichert. Wenn diese Option aktiviert ist, werden die Titelbilder als jpg Datei in dem gleichen Ordner gespeichert in welchem sich auch das Medium befindet. Es wird immer nur eine Datei mit dem Namen \"cover.jpg\" gespeichert.", "LabelSettingsStoreMetadataWithItem": "Metadaten als OPF-Datei im Medienordner speichern", "LabelSettingsStoreMetadataWithItemHelp": "Standardmäßig werden die Metadaten in /metadata/items gespeichert. Wenn diese Option aktiviert ist, werden die Metadaten als OPF-Datei (Textdatei) in dem gleichen Ordner gespeichert in welchem sich auch das Medium befindet. Es wird immer nur eine Datei mit dem Namen \"matadata.abs\" gespeichert.", + "LabelSettingsTimeFormat": "Time Format", "LabelShowAll": "Alles anzeigen", "LabelSize": "Größe", "LabelSleepTimer": "Einschlaf-Timer", @@ -623,4 +627,4 @@ "ToastSocketFailedToConnect": "Verbindung zum WebSocket fehlgeschlagen", "ToastUserDeleteFailed": "Benutzer konnte nicht gelöscht werden", "ToastUserDeleteSuccess": "Benutzer gelöscht" -} \ No newline at end of file +} diff --git a/client/strings/en-us.json b/client/strings/en-us.json index 710d5ef8d..b658c52f2 100644 --- a/client/strings/en-us.json +++ b/client/strings/en-us.json @@ -195,6 +195,7 @@ "LabelCronExpression": "Cron Expression", "LabelCurrent": "Current", "LabelCurrently": "Currently:", + "LabelCustomCronExpression": "Custom Cron Expression:", "LabelDatetime": "Datetime", "LabelDescription": "Description", "LabelDeselectAll": "Deselect All", @@ -273,6 +274,8 @@ "LabelNewestAuthors": "Newest Authors", "LabelNewestEpisodes": "Newest Episodes", "LabelNewPassword": "New Password", + "LabelNextBackupDate": "Next backup date", + "LabelNextScheduledRun": "Next scheduled run", "LabelNotes": "Notes", "LabelNotFinished": "Not Finished", "LabelNotificationAppriseURL": "Apprise URL(s)", @@ -365,6 +368,7 @@ "LabelSettingsStoreCoversWithItemHelp": "By default covers are stored in /metadata/items, enabling this setting will store covers in your library item folder. Only one file named \"cover\" will be kept", "LabelSettingsStoreMetadataWithItem": "Store metadata with item", "LabelSettingsStoreMetadataWithItemHelp": "By default metadata files are stored in /metadata/items, enabling this setting will store metadata files in your library item folders. Uses .abs file extension", + "LabelSettingsTimeFormat": "Time Format", "LabelShowAll": "Show All", "LabelSize": "Size", "LabelSleepTimer": "Sleep timer", diff --git a/client/strings/es.json b/client/strings/es.json index 710d5ef8d..b658c52f2 100644 --- a/client/strings/es.json +++ b/client/strings/es.json @@ -195,6 +195,7 @@ "LabelCronExpression": "Cron Expression", "LabelCurrent": "Current", "LabelCurrently": "Currently:", + "LabelCustomCronExpression": "Custom Cron Expression:", "LabelDatetime": "Datetime", "LabelDescription": "Description", "LabelDeselectAll": "Deselect All", @@ -273,6 +274,8 @@ "LabelNewestAuthors": "Newest Authors", "LabelNewestEpisodes": "Newest Episodes", "LabelNewPassword": "New Password", + "LabelNextBackupDate": "Next backup date", + "LabelNextScheduledRun": "Next scheduled run", "LabelNotes": "Notes", "LabelNotFinished": "Not Finished", "LabelNotificationAppriseURL": "Apprise URL(s)", @@ -365,6 +368,7 @@ "LabelSettingsStoreCoversWithItemHelp": "By default covers are stored in /metadata/items, enabling this setting will store covers in your library item folder. Only one file named \"cover\" will be kept", "LabelSettingsStoreMetadataWithItem": "Store metadata with item", "LabelSettingsStoreMetadataWithItemHelp": "By default metadata files are stored in /metadata/items, enabling this setting will store metadata files in your library item folders. Uses .abs file extension", + "LabelSettingsTimeFormat": "Time Format", "LabelShowAll": "Show All", "LabelSize": "Size", "LabelSleepTimer": "Sleep timer", diff --git a/client/strings/fr.json b/client/strings/fr.json index 47f842095..c67579ebf 100644 --- a/client/strings/fr.json +++ b/client/strings/fr.json @@ -195,6 +195,7 @@ "LabelCronExpression": "Expression Cron", "LabelCurrent": "Courrant", "LabelCurrently": "En ce moment :", + "LabelCustomCronExpression": "Custom Cron Expression:", "LabelDatetime": "Datetime", "LabelDescription": "Description", "LabelDeselectAll": "Tout déselectionner", @@ -273,6 +274,8 @@ "LabelNewestAuthors": "Nouveaux auteurs", "LabelNewestEpisodes": "Derniers épisodes", "LabelNewPassword": "Nouveau mot de passe", + "LabelNextBackupDate": "Next backup date", + "LabelNextScheduledRun": "Next scheduled run", "LabelNotes": "Notes", "LabelNotFinished": "Non terminé(e)", "LabelNotificationAppriseURL": "URL(s) d'apprise", @@ -365,6 +368,7 @@ "LabelSettingsStoreCoversWithItemHelp": "Par défaut, les couvertures sont enregistrées dans /metadata/items. Activer ce paramètre enregistrera les couvertures dans le dossier avec les fichiersde l'article. Seul un fichier nommé \"cover\" sera gardé.", "LabelSettingsStoreMetadataWithItem": "Enregistrer les Métadonnées avec les articles", "LabelSettingsStoreMetadataWithItemHelp": "Par défaut, les métadonnées sont enregistrées dans /metadata/items. Activer ce paramètre enregistrera les métadonnées dans le dossier de l'article avec une extension \".abs\".", + "LabelSettingsTimeFormat": "Time Format", "LabelShowAll": "Afficher Tout", "LabelSize": "Taille", "LabelSleepTimer": "Minuterie", diff --git a/client/strings/hr.json b/client/strings/hr.json index 36e674445..7f2eea9eb 100644 --- a/client/strings/hr.json +++ b/client/strings/hr.json @@ -195,6 +195,7 @@ "LabelCronExpression": "Cron Expression", "LabelCurrent": "Trenutan", "LabelCurrently": "Trenutno:", + "LabelCustomCronExpression": "Custom Cron Expression:", "LabelDatetime": "Datetime", "LabelDescription": "Opis", "LabelDeselectAll": "Odznači sve", @@ -273,6 +274,8 @@ "LabelNewestAuthors": "Najnoviji autori", "LabelNewestEpisodes": "Najnovije epizode", "LabelNewPassword": "Nova lozinka", + "LabelNextBackupDate": "Next backup date", + "LabelNextScheduledRun": "Next scheduled run", "LabelNotes": "Bilješke", "LabelNotFinished": "Nedovršeno", "LabelNotificationAppriseURL": "Apprise URL(s)", @@ -365,6 +368,7 @@ "LabelSettingsStoreCoversWithItemHelp": "By default covers are stored in /metadata/items, enabling this setting will store covers in your library item folder. Only one file named \"cover\" will be kept", "LabelSettingsStoreMetadataWithItem": "Spremi metapodatke uz stavku", "LabelSettingsStoreMetadataWithItemHelp": "Po defaultu metapodatci su spremljeni u /metadata/items, uključujućite li ovu postavku, metapodatci će biti spremljeni u folderima od biblioteke. Koristi .abs ekstenziju.", + "LabelSettingsTimeFormat": "Time Format", "LabelShowAll": "Prikaži sve", "LabelSize": "Veličina", "LabelSleepTimer": "Sleep timer", diff --git a/client/strings/it.json b/client/strings/it.json index 21bc3ae91..1b4289415 100644 --- a/client/strings/it.json +++ b/client/strings/it.json @@ -195,6 +195,7 @@ "LabelCronExpression": "Espressione Cron", "LabelCurrent": "Attuale", "LabelCurrently": "Attualmente:", + "LabelCustomCronExpression": "Custom Cron Expression:", "LabelDatetime": "Data & Ora", "LabelDescription": "Descrizione", "LabelDeselectAll": "Deseleziona Tutto", @@ -273,6 +274,8 @@ "LabelNewestAuthors": "Autori Recenti", "LabelNewestEpisodes": "Episodi Recenti", "LabelNewPassword": "Nuova Password", + "LabelNextBackupDate": "Next backup date", + "LabelNextScheduledRun": "Next scheduled run", "LabelNotes": "Note", "LabelNotFinished": "Da Completare", "LabelNotificationAppriseURL": "Apprendi URL(s)", @@ -365,6 +368,7 @@ "LabelSettingsStoreCoversWithItemHelp": "Di default, le immagini di copertina sono salvate dentro /metadata/items, abilitando questa opzione le copertine saranno archiviate nella cartella della libreria corrispondente. Verrà conservato solo un file denominato \"cover\"", "LabelSettingsStoreMetadataWithItem": "Archivia i metadata con il file", "LabelSettingsStoreMetadataWithItemHelp": "Di default, i metadati sono salvati dentro /metadata/items, abilitando questa opzione si memorizzeranno i metadata nella cartella della libreria. I file avranno estensione .abs", + "LabelSettingsTimeFormat": "Time Format", "LabelShowAll": "Mostra Tutto", "LabelSize": "Dimensione", "LabelSleepTimer": "Sleep timer", diff --git a/client/strings/pl.json b/client/strings/pl.json index c907e6120..346a59ccc 100644 --- a/client/strings/pl.json +++ b/client/strings/pl.json @@ -195,6 +195,7 @@ "LabelCronExpression": "Wyrażenie CRON", "LabelCurrent": "Aktualny", "LabelCurrently": "Obecnie:", + "LabelCustomCronExpression": "Custom Cron Expression:", "LabelDatetime": "Data i godzina", "LabelDescription": "Opis", "LabelDeselectAll": "Odznacz wszystko", @@ -273,6 +274,8 @@ "LabelNewestAuthors": "Najnowsi autorzy", "LabelNewestEpisodes": "Najnowsze odcinki", "LabelNewPassword": "Nowe hasło", + "LabelNextBackupDate": "Next backup date", + "LabelNextScheduledRun": "Next scheduled run", "LabelNotes": "Uwagi", "LabelNotFinished": "Nieukończone", "LabelNotificationAppriseURL": "URLe Apprise", @@ -365,6 +368,7 @@ "LabelSettingsStoreCoversWithItemHelp": "Domyślnie okładki są przechowywane w folderze /metadata/items, włączenie tej opcji spowoduje, że okładka będzie przechowywana w folderze ksiązki. Tylko jedna okładka o nazwie pliku \"cover\" będzie przechowywana.", "LabelSettingsStoreMetadataWithItem": "Przechowuj metadane w folderze książki", "LabelSettingsStoreMetadataWithItemHelp": "Domyślnie metadane są przechowywane w folderze /metadata/items, włączenie tej opcji spowoduje, że okładka będzie przechowywana w folderze ksiązki. Tylko jedna okładka o nazwie pliku \"cover\" będzie przechowywana. Rozszerzenie pliku metadanych: .abs", + "LabelSettingsTimeFormat": "Time Format", "LabelShowAll": "Pokaż wszystko", "LabelSize": "Rozmiar", "LabelSleepTimer": "Wyłącznik czasowy", diff --git a/client/strings/ru.json b/client/strings/ru.json index a1a34f70c..f2b497f2a 100644 --- a/client/strings/ru.json +++ b/client/strings/ru.json @@ -195,6 +195,7 @@ "LabelCronExpression": "Выражение Cron", "LabelCurrent": "Текущий", "LabelCurrently": "Текущее:", + "LabelCustomCronExpression": "Custom Cron Expression:", "LabelDatetime": "Дата и время", "LabelDescription": "Описание", "LabelDeselectAll": "Снять Выделение", @@ -273,6 +274,8 @@ "LabelNewestAuthors": "Новые Авторы", "LabelNewestEpisodes": "Новые Эпизоды", "LabelNewPassword": "Новый Пароль", + "LabelNextBackupDate": "Next backup date", + "LabelNextScheduledRun": "Next scheduled run", "LabelNotes": "Заметки", "LabelNotFinished": "Не Завершено", "LabelNotificationAppriseURL": "URL(ы) для извещений", @@ -365,6 +368,7 @@ "LabelSettingsStoreCoversWithItemHelp": "По умолчанию обложки сохраняются в папке /metadata/items, при включении этой настройки обложка будет храниться в папке элемента. Будет сохраняться только один файл с именем \"cover\"", "LabelSettingsStoreMetadataWithItem": "Хранить метаинформацию с элементом", "LabelSettingsStoreMetadataWithItemHelp": "По умолчанию метаинформация сохраняется в папке /metadata/items, при включении этой настройки метаинформация будет храниться в папке элемента. Используется расширение файла .abs", + "LabelSettingsTimeFormat": "Time Format", "LabelShowAll": "Показать Все", "LabelSize": "Размер", "LabelSleepTimer": "Таймер сна", diff --git a/client/strings/zh-cn.json b/client/strings/zh-cn.json index 433a87bc6..0401909ec 100644 --- a/client/strings/zh-cn.json +++ b/client/strings/zh-cn.json @@ -195,6 +195,7 @@ "LabelCronExpression": "计划任务表达式", "LabelCurrent": "当前", "LabelCurrently": "当前:", + "LabelCustomCronExpression": "Custom Cron Expression:", "LabelDatetime": "日期时间", "LabelDescription": "描述", "LabelDeselectAll": "全部取消选择", @@ -273,6 +274,8 @@ "LabelNewestAuthors": "最新作者", "LabelNewestEpisodes": "最新剧集", "LabelNewPassword": "新密码", + "LabelNextBackupDate": "Next backup date", + "LabelNextScheduledRun": "Next scheduled run", "LabelNotes": "注释", "LabelNotFinished": "未听完", "LabelNotificationAppriseURL": "通知 URL(s)", @@ -365,6 +368,7 @@ "LabelSettingsStoreCoversWithItemHelp": "默认情况下封面存储在/metadata/items文件夹中, 启用此设置将存储封面在你媒体项目文件夹中. 只保留一个名为 \"cover\" 的文件", "LabelSettingsStoreMetadataWithItem": "存储项目元数据", "LabelSettingsStoreMetadataWithItemHelp": "默认情况下元数据文件存储在/metadata/items文件夹中, 启用此设置将存储元数据在你媒体项目文件夹中. 使 .abs 文件护展名", + "LabelSettingsTimeFormat": "Time Format", "LabelShowAll": "全部显示", "LabelSize": "文件大小", "LabelSleepTimer": "睡眠定时", diff --git a/server/objects/settings/ServerSettings.js b/server/objects/settings/ServerSettings.js index 38b753706..1dbd3417b 100644 --- a/server/objects/settings/ServerSettings.js +++ b/server/objects/settings/ServerSettings.js @@ -51,6 +51,7 @@ class ServerSettings { this.chromecastEnabled = false this.enableEReader = false this.dateFormat = 'MM/dd/yyyy' + this.timeFormat = 'HH:mm' this.language = 'en-us' this.logLevel = Logger.logLevel @@ -96,6 +97,7 @@ class ServerSettings { this.chromecastEnabled = !!settings.chromecastEnabled this.enableEReader = !!settings.enableEReader this.dateFormat = settings.dateFormat || 'MM/dd/yyyy' + this.timeFormat = settings.timeFormat || 'HH:mm' this.language = settings.language || 'en-us' this.logLevel = settings.logLevel || Logger.logLevel this.version = settings.version || null @@ -146,6 +148,7 @@ class ServerSettings { chromecastEnabled: this.chromecastEnabled, enableEReader: this.enableEReader, dateFormat: this.dateFormat, + timeFormat: this.timeFormat, language: this.language, logLevel: this.logLevel, version: this.version @@ -178,4 +181,4 @@ class ServerSettings { return hasUpdates } } -module.exports = ServerSettings \ No newline at end of file +module.exports = ServerSettings