mirror of
https://github.com/unraid/api.git
synced 2026-01-06 16:49:49 -06:00
…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 -->
254 lines
7.8 KiB
TypeScript
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;
|