Files
api/web/src/composables/dateTime.ts
Eli Bosley 73b2ce360c fix: update @unraid/shared-callbacks to version 3.0.0 (#1831)
…on and pnpm-lock.yaml

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Added a standalone redirect page that shows "Redirecting..." and
navigates automatically.

* **Improvements**
* Redirect preserves hash callback data, validates targets, and logs the
computed redirect.
  * Purchase callback origin changed to a different account host.
* Date/time formatting now tolerates missing or empty server formats
with safe fallbacks.
  * Redirect page included in backup/restore.

* **Tests**
  * Added tests covering date/time formatting fallbacks.

* **Chores**
  * Dependency @unraid/shared-callbacks upgraded.
  * Removed multiple demo/debug pages and related test UIs.

<sub>✏️ Tip: You can customize this high-level summary in your review
settings.</sub>
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-12-15 16:20:18 -05:00

254 lines
7.8 KiB
TypeScript

import { computed, onBeforeMount, onBeforeUnmount, ref } from 'vue';
import dayjs, { extend } from 'dayjs';
import localizedFormat from 'dayjs/plugin/localizedFormat';
import type { DateFormatOption, ServerDateTimeFormat, TimeFormatOption } from '~/types/server';
import type { ComposerTranslation } from 'vue-i18n';
/** @see https://day.js.org/docs/en/display/format#localized-formats */
extend(localizedFormat);
export interface TimeStringsObject {
years: number;
months: number;
days: number;
hours: number;
minutes: number;
seconds: number;
firstDateWasLater: boolean;
displaySeconds?: boolean;
}
const dateFormatOptions: DateFormatOption[] = [
{ format: '%c', display: 'ddd, D MMMM YYYY' }, // aka "system settings" in the date time settings of the webgui
{ format: '%A, %Y %B %e', display: 'ddd, YYYY MMMM D' },
{ format: '%A, %e %B %Y', display: 'ddd, D MMMM YYYY' },
{ format: '%A, %B %e, %Y', display: 'ddd, MMMM D, YYYY' },
{ format: '%A, %m/%d/%Y', display: 'ddd, MM/DD/YYYY' },
{ format: '%A, %d-%m-%Y', display: 'ddd, DD-MM-YYYY' },
{ format: '%A, %d.%m.%Y', display: 'ddd, DD.MM.YYYY' },
{ format: '%A, %Y-%m-%d', display: 'ddd, YYYY-MM-DD' },
];
const timeFormatOptions: TimeFormatOption[] = [
{ format: '%I:%M %p', display: 'hh:mma' },
{ format: '%R', display: 'HH:mm' },
];
/**
* the provided ref may not have a value until we get a response from the refreshServerState action
* So we need to watch for this value to be able to format it based on the user's date time preferences.
* @example below is how to use this composable
* const formattedRegExp = ref<any>();
* const setFormattedRegExp = () => { // ran in watch on regExp and onBeforeMount
* if (!regExp.value) { return; }
* const { outputDateTimeFormatted } = useDateTimeHelper(dateTimeFormat.value, props.t, true, regExp.value);
* formattedRegExp.value = outputDateTimeFormatted.value;
* };
* watch(regExp, (_newV) => {
* setFormattedRegExp();
* });
* onBeforeMount(() => {
* setFormattedRegExp();
* });
*
* @param format provided by Unraid server's state.php and set in the server store
* @param t translations
* @param hideMinutesSeconds true will hide minutes and seconds from the output
* @param providedDateTime optional provided date time to use instead of Date.now()
* @param diffCountUp true will count up from the provided date time instead of down
*/
const useDateTimeHelper = (
format: ServerDateTimeFormat | undefined,
t: ComposerTranslation,
hideMinutesSeconds?: boolean,
providedDateTime?: number | undefined,
diffCountUp?: boolean
) => {
const buildStringFromValues = (payload: TimeStringsObject) => {
const { years, months, days, hours, minutes, seconds, firstDateWasLater, displaySeconds } = payload;
const result = [];
if (years) {
result.push(t('composables.dateTime.year', years));
}
if (months) {
result.push(t('composables.dateTime.month', months));
}
if (days) {
result.push(t('composables.dateTime.day', days));
}
if (hours) {
result.push(t('composables.dateTime.hour', hours));
}
if (minutes) {
result.push(t('composables.dateTime.minute', minutes));
}
if (seconds && ((!years && !months && !days && !hours && !minutes) || displaySeconds)) {
result.push(t('composables.dateTime.second', seconds));
}
if (firstDateWasLater) {
result.push(t('composables.dateTime.ago'));
}
return result.join(' ');
};
// use the format from the server to determine the format to use
const findMatchingFormat = (
selectedFormat: string,
formats: DateFormatOption[] | TimeFormatOption[]
): DateFormatOption | TimeFormatOption | undefined =>
formats.find((formatOption) => formatOption.format === selectedFormat);
const defaultTimeFormat = timeFormatOptions[0];
const fallbackDateDisplayFormat = 'dddd, MMMM D, YYYY';
const dateFormatFromServer = (format?.date ?? '').trim();
const dateFormat = dateFormatFromServer
? (findMatchingFormat(dateFormatFromServer, dateFormatOptions) as DateFormatOption | undefined)
: undefined;
let displayFormat = dateFormat?.display ?? fallbackDateDisplayFormat;
if (!hideMinutesSeconds) {
const timeFormatFromServer = (format?.time ?? '').trim();
const timeFormat = timeFormatFromServer
? (findMatchingFormat(timeFormatFromServer, timeFormatOptions) as TimeFormatOption | undefined)
: undefined;
const timeDisplay = timeFormat?.display ?? defaultTimeFormat.display;
displayFormat = `${displayFormat} ${timeDisplay}`;
}
const formatDate = (date: number): string => dayjs(date).format(displayFormat);
/**
* Original meat and potatos from:
* @version: 1.0.1
* @author: huangjinlin
* @repo: https://github.com/huangjinlin/dayjs-precise-range
*/
const buildValueObject = (
yDiff: number,
mDiff: number,
dDiff: number,
hourDiff: number,
minDiff: number,
secDiff: number,
firstDateWasLater: boolean
): TimeStringsObject => ({
years: yDiff,
months: mDiff,
days: dDiff,
hours: hourDiff,
minutes: minDiff,
seconds: secDiff,
firstDateWasLater,
});
const preciseDateDiff = (d1: dayjs.Dayjs, d2: dayjs.Dayjs): TimeStringsObject => {
let m1 = dayjs(d1);
let m2 = dayjs(d2);
let firstDateWasLater;
if (m1.isSame(m2)) {
return buildValueObject(0, 0, 0, 0, 0, 0, false);
}
if (m1.isAfter(m2)) {
const tmp = m1;
m1 = m2;
m2 = tmp;
firstDateWasLater = true;
} else {
firstDateWasLater = false;
}
let yDiff = m2.year() - m1.year();
let mDiff = m2.month() - m1.month();
let dDiff = m2.date() - m1.date();
let hourDiff = m2.hour() - m1.hour();
let minDiff = m2.minute() - m1.minute();
let secDiff = m2.second() - m1.second();
if (secDiff < 0) {
secDiff = 60 + secDiff;
minDiff -= 1;
}
if (minDiff < 0) {
minDiff = 60 + minDiff;
hourDiff -= 1;
}
if (hourDiff < 0) {
hourDiff = 24 + hourDiff;
dDiff -= 1;
}
if (dDiff < 0) {
const daysInLastFullMonth = dayjs(`${m2.year()}-${m2.month() + 1}`)
.subtract(1, 'M')
.daysInMonth();
if (daysInLastFullMonth < m1.date()) {
// 31/01 -> 2/03
dDiff = daysInLastFullMonth + dDiff + (m1.date() - daysInLastFullMonth);
} else {
dDiff = daysInLastFullMonth + dDiff;
}
mDiff -= 1;
}
if (mDiff < 0) {
mDiff = 12 + mDiff;
yDiff -= 1;
}
return buildValueObject(yDiff, mDiff, dDiff, hourDiff, minDiff, secDiff, firstDateWasLater);
};
const readableDifference = (a = '', b = '') => {
try {
const x = a ? dayjs(parseInt(a, 10)) : dayjs();
const y = b ? dayjs(parseInt(b, 10)) : dayjs();
return preciseDateDiff(x, y);
} catch (error) {
throw new Error(
`Couldn't calculate date difference with error: ${error instanceof Error ? error.message : 'Unknown error'}`
);
}
};
const dateDiff = (time: string, countUp: boolean) =>
countUp ? readableDifference(time, '') : readableDifference('', time);
// provide outputs for components
const outputDateTimeReadableDiff = ref<string>('');
const outputDateTimeFormatted = computed(() => formatDate(providedDateTime ?? Date.now()));
const runDiff = () => {
outputDateTimeReadableDiff.value = buildStringFromValues(
dateDiff((providedDateTime ?? Date.now()).toString(), diffCountUp ?? false)
);
};
let interval: string | number | ReturnType<typeof setInterval> | undefined;
onBeforeMount(() => {
if (providedDateTime) {
runDiff();
interval = setInterval(() => {
runDiff();
}, 1000);
}
});
onBeforeUnmount(() => {
if (interval) {
clearInterval(interval);
}
});
return {
formatDate,
outputDateTimeReadableDiff,
outputDateTimeFormatted,
};
};
export default useDateTimeHelper;