diff --git a/src/consts/timezone.ts b/src/consts/timezone.ts index ebe35d70..3ef74de4 100644 --- a/src/consts/timezone.ts +++ b/src/consts/timezone.ts @@ -1,7 +1,4 @@ -export interface TimezoneInfo { - readonly displayName: string; - readonly timezoneName: string; -} +import type { TimezoneInfo } from '@/core/timezone.ts'; export const UTC_TIMEZONE: TimezoneInfo = { displayName: 'Coordinated Universal Time', diff --git a/src/core/base.ts b/src/core/base.ts index 1011a469..df67340f 100644 --- a/src/core/base.ts +++ b/src/core/base.ts @@ -13,4 +13,9 @@ export interface TypeAndDisplayName { readonly displayName: string; } +export interface LocalizedSwitchOption { + readonly value: boolean; + readonly displayName: string; +} + export type BeforeResolveFunction = (callback: () => void) => void; diff --git a/src/core/timezone.ts b/src/core/timezone.ts index 6446d8e8..f7581172 100644 --- a/src/core/timezone.ts +++ b/src/core/timezone.ts @@ -1,5 +1,18 @@ import type { TypeAndName } from './base.ts'; +export interface TimezoneInfo { + readonly displayName: string; + readonly timezoneName: string; +} + +export interface LocalizedTimezoneInfo { + readonly name: string; + readonly utcOffset: string; + readonly utcOffsetMinutes: number; + readonly displayName: string; + readonly displayNameWithUtcOffset: string; +} + export class TimezoneTypeForStatistics implements TypeAndName { public static readonly ApplicationTimezone = new TimezoneTypeForStatistics(0, 'Application Timezone'); public static readonly TransactionTimezone = new TimezoneTypeForStatistics(1, 'Transaction Timezone'); diff --git a/src/lib/ui/mobile.ts b/src/lib/ui/mobile.ts index 092a1a21..4ae3e916 100644 --- a/src/lib/ui/mobile.ts +++ b/src/lib/ui/mobile.ts @@ -37,7 +37,7 @@ export function showAlert(message: string, confirmCallback: (dialog: Dialog.Dial }); } -export function showConfirm(message: string, confirmCallback: (dialog: Dialog.Dialog, e: Event) => void, cancelCallback: (dialog: Dialog.Dialog, e: Event) => void, translateFn: TranslateFunction): void { +export function showConfirm(message: string, confirmCallback: (dialog: Dialog.Dialog, e: Event) => void, cancelCallback: ((dialog: Dialog.Dialog, e: Event) => void) | undefined, translateFn: TranslateFunction): void { f7ready((f7) => { f7.dialog.create({ title: translateFn('global.app.title'), @@ -230,7 +230,7 @@ export function useI18nUIComponents() { return { showAlert: (message: string, confirmCallback: (dialog: Dialog.Dialog, e: Event) => void) => showAlert(message, confirmCallback, i18nGlobal.t), - showConfirm: (message: string, confirmCallback: (dialog: Dialog.Dialog, e: Event) => void, cancelCallback: (dialog: Dialog.Dialog, e: Event) => void): void => showConfirm(message, confirmCallback, cancelCallback, i18nGlobal.t), + showConfirm: (message: string, confirmCallback: (dialog: Dialog.Dialog, e: Event) => void, cancelCallback?: (dialog: Dialog.Dialog, e: Event) => void): void => showConfirm(message, confirmCallback, cancelCallback, i18nGlobal.t), showToast: (message: string, timeout?: number): void => showToast(message, timeout, i18nGlobal.t), routeBackOnError } diff --git a/src/locales/helper.js b/src/locales/helper.js index 1fe443e9..10923eed 100644 --- a/src/locales/helper.js +++ b/src/locales/helper.js @@ -1216,16 +1216,6 @@ function getAllSupportedImportFileTypes(i18nGlobal, translateFn) { return allSupportedImportFileTypes; } -function getEnableDisableOptions(translateFn) { - return [{ - value: true, - displayName: translateFn('Enable') - },{ - value: false, - displayName: translateFn('Disable') - }]; -} - function getCategorizedAccountsWithDisplayBalance(allVisibleAccounts, showAccountBalance, defaultCurrency, userStore, settingsStore, exchangeRatesStore, translateFn) { const ret = []; const allCategories = AccountCategory.values(); @@ -1546,7 +1536,6 @@ export function i18nFunctions(i18nGlobal) { getAllTransactionDefaultCategories: (categoryType, locale) => getAllTransactionDefaultCategories(categoryType, locale, i18nGlobal.t), getAllDisplayExchangeRates: (settingsStore, exchangeRatesData) => getAllDisplayExchangeRates(settingsStore, exchangeRatesData, i18nGlobal.t), getAllSupportedImportFileTypes: () => getAllSupportedImportFileTypes(i18nGlobal, i18nGlobal.t), - getEnableDisableOptions: () => getEnableDisableOptions(i18nGlobal.t), getCategorizedAccountsWithDisplayBalance: (allVisibleAccounts, showAccountBalance, defaultCurrency, settingsStore, userStore, exchangeRatesStore) => getCategorizedAccountsWithDisplayBalance(allVisibleAccounts, showAccountBalance, defaultCurrency, userStore, settingsStore, exchangeRatesStore, i18nGlobal.t), getServerTipContent: (tipConfig) => getServerTipContent(tipConfig, i18nGlobal), joinMultiText: (textArray) => joinMultiText(textArray, i18nGlobal.t), diff --git a/src/locales/helpers.ts b/src/locales/helpers.ts index e06556a4..e18b78b5 100644 --- a/src/locales/helpers.ts +++ b/src/locales/helpers.ts @@ -1,7 +1,7 @@ import { useI18n as useVueI18n } from 'vue-i18n'; import moment from 'moment-timezone'; -import type { TypeAndName, TypeAndDisplayName } from '@/core/base.ts'; +import type {TypeAndName, TypeAndDisplayName, LocalizedSwitchOption } from '@/core/base.ts'; import { type LanguageInfo, allLanguages, DEFAULT_LANGUAGE } from '@/locales/index.ts'; @@ -23,6 +23,7 @@ import { } from '@/core/datetime.ts'; import { + type LocalizedTimezoneInfo, TimezoneTypeForStatistics } from '@/core/timezone.ts'; @@ -69,6 +70,7 @@ import { import type { LocaleDefaultSettings } from '@/core/setting.ts'; import type { ErrorResponse } from '@/core/api.ts'; +import { UTC_TIMEZONE, ALL_TIMEZONES } from '@/consts/timezone.ts'; import { ALL_CURRENCIES } from '@/consts/currency.ts'; import { KnownErrorCode, SPECIFIED_API_NOT_FOUND_ERRORS, PARAMETERIZED_ERRORS } from '@/consts/api.ts'; @@ -85,6 +87,10 @@ import { isPM, formatUnixTime, getTimezoneOffset, + getTimezoneOffsetMinutes, + getBrowserTimezoneOffset, + getBrowserTimezoneOffsetMinutes, + getTimeDifferenceHoursAndMinutes, getDateTimeFormatType, getRecentMonthDateRanges } from '@/lib/datetime.ts'; @@ -534,6 +540,16 @@ export function useI18n() { return t('default.firstDayOfWeek'); } + function getAllEnableDisableOptions(): LocalizedSwitchOption[] { + return [{ + value: true, + displayName: t('Enable') + },{ + value: false, + displayName: t('Disable') + }]; + } + function getAllMeridiemIndicators(): LocalizedMeridiemIndicator { const allMeridiemIndicators = MeridiemIndicator.values(); const meridiemIndicatorNames = []; @@ -674,6 +690,50 @@ export function useI18n() { return allRecentMonthDateRanges; } + function getAllTimezones(includeSystemDefault?: boolean): LocalizedTimezoneInfo[] { + const defaultTimezoneOffset = getBrowserTimezoneOffset(); + const defaultTimezoneOffsetMinutes = getBrowserTimezoneOffsetMinutes(); + const allTimezoneInfos: LocalizedTimezoneInfo[] = []; + + for (let i = 0; i < ALL_TIMEZONES.length; i++) { + const utcOffset = (ALL_TIMEZONES[i].timezoneName !== UTC_TIMEZONE.timezoneName ? getTimezoneOffset(ALL_TIMEZONES[i].timezoneName) : ''); + const displayName = t(`timezone.${ALL_TIMEZONES[i].displayName}`); + + allTimezoneInfos.push({ + name: ALL_TIMEZONES[i].timezoneName, + utcOffset: utcOffset, + utcOffsetMinutes: getTimezoneOffsetMinutes(ALL_TIMEZONES[i].timezoneName), + displayName: displayName, + displayNameWithUtcOffset: `(UTC${utcOffset}) ${displayName}` + }); + } + + if (includeSystemDefault) { + const defaultDisplayName = t('System Default'); + + allTimezoneInfos.push({ + name: '', + utcOffset: defaultTimezoneOffset, + utcOffsetMinutes: defaultTimezoneOffsetMinutes, + displayName: defaultDisplayName, + displayNameWithUtcOffset: `(UTC${defaultTimezoneOffset}) ${defaultDisplayName}` + }); + } + + allTimezoneInfos.sort(function(c1, c2) { + const utcOffset1 = parseInt(c1.utcOffset.replace(':', '')); + const utcOffset2 = parseInt(c2.utcOffset.replace(':', '')); + + if (utcOffset1 !== utcOffset2) { + return utcOffset1 - utcOffset2; + } + + return c1.displayName.localeCompare(c2.displayName); + }) + + return allTimezoneInfos; + } + function getAllTimezoneTypesUsedForStatistics(currentTimezone?: string): TypeAndDisplayName[] { const currentTimezoneOffset = getTimezoneOffset(currentTimezone); @@ -934,6 +994,37 @@ export function useI18n() { } } + function getTimezoneDifferenceDisplayText(utcOffset: number): string { + const defaultTimezoneOffset = getTimezoneOffsetMinutes(); + const offsetTime = getTimeDifferenceHoursAndMinutes(utcOffset - defaultTimezoneOffset); + + if (utcOffset > defaultTimezoneOffset) { + if (offsetTime.offsetMinutes) { + return t('format.misc.hoursMinutesAheadOfDefaultTimezone', { + hours: offsetTime.offsetHours, + minutes: offsetTime.offsetMinutes + }); + } else { + return t('format.misc.hoursAheadOfDefaultTimezone', { + hours: offsetTime.offsetHours + }); + } + } else if (utcOffset < defaultTimezoneOffset) { + if (offsetTime.offsetMinutes) { + return t('format.misc.hoursMinutesBehindDefaultTimezone', { + hours: offsetTime.offsetHours, + minutes: offsetTime.offsetMinutes + }); + } else { + return t('format.misc.hoursBehindDefaultTimezone', { + hours: offsetTime.offsetHours + }); + } + } else { + return t('Same time as default timezone'); + } + } + function getNumberWithDigitGroupingSymbol(value: number | string): string { const numberFormatOptions = getNumberFormatOptions(); return appendDigitGroupingSymbol(value, numberFormatOptions); @@ -1105,6 +1196,7 @@ export function useI18n() { getDefaultCurrency, getDefaultFirstDayOfWeek, // get all localized info of specified type + getAllEnableDisableOptions, getAllMeridiemIndicators, getAllLongMonthNames, getAllShortMonthNames, @@ -1114,6 +1206,7 @@ export function useI18n() { getAllWeekDays, getAllDateRanges, getAllRecentMonthDateRanges, + getAllTimezones, getAllTimezoneTypesUsedForStatistics, getAllDecimalSeparators: () => getLocalizedNumeralSeparatorFormats(DecimalSeparator.values(), DecimalSeparator.parse(t('default.decimalSeparator')), DecimalSeparator.Default, DecimalSeparator.LanguageDefaultType), getAllDigitGroupingSymbols: () => getLocalizedNumeralSeparatorFormats(DigitGroupingSymbol.values(), DigitGroupingSymbol.parse(t('default.digitGroupingSymbol')), DigitGroupingSymbol.Default, DigitGroupingSymbol.LanguageDefaultType), @@ -1164,6 +1257,7 @@ export function useI18n() { formatUnixTimeToLongTime: (unixTime: number, utcOffset?: number, currentUtcOffset?: number) => formatUnixTime(unixTime, getLocalizedLongTimeFormat(), utcOffset, currentUtcOffset), formatUnixTimeToShortTime: (unixTime: number, utcOffset?: number, currentUtcOffset?: number) => formatUnixTime(unixTime, getLocalizedShortTimeFormat(), utcOffset, currentUtcOffset), formatYearQuarter, + getTimezoneDifferenceDisplayText, appendDigitGroupingSymbol: getNumberWithDigitGroupingSymbol, parseAmount: getParsedAmountNumber, formatAmount: getFormattedAmount, diff --git a/src/views/base/settings/AppSettingsPageBase.ts b/src/views/base/settings/AppSettingsPageBase.ts new file mode 100644 index 00000000..d1948477 --- /dev/null +++ b/src/views/base/settings/AppSettingsPageBase.ts @@ -0,0 +1,114 @@ +import { computed } from 'vue'; + +import { useI18n } from '@/locales/helpers.ts'; + +import { useSettingsStore } from '@/stores/setting.ts'; +// @ts-expect-error the above file is migrating to ts +import { useTransactionsStore } from '@/stores/transaction.js'; +import { useOverviewStore } from '@/stores/overview.ts'; +// @ts-expect-error the above file is migrating to ts +import { useStatisticsStore } from '@/stores/statistics.js'; + +import type { TypeAndDisplayName } from '@/core/base.ts'; +import type { LocalizedTimezoneInfo } from '@/core/timezone.ts'; + +export function useAppSettingPageBase() { + const { getAllTimezones, getAllTimezoneTypesUsedForStatistics, getAllCurrencySortingTypes, setTimeZone } = useI18n(); + + const settingsStore = useSettingsStore(); + const transactionsStore = useTransactionsStore(); + const overviewStore = useOverviewStore(); + const statisticsStore = useStatisticsStore(); + + const allTimezones = computed(() => getAllTimezones(true)); + const allTimezoneTypesUsedForStatistics = computed(() => getAllTimezoneTypesUsedForStatistics()); + const allCurrencySortingTypes = computed(() => getAllCurrencySortingTypes()); + + const timeZone = computed({ + get: () => settingsStore.appSettings.timeZone, + set: (value) => { + settingsStore.setTimeZone(value); + setTimeZone(value); + transactionsStore.updateTransactionListInvalidState(true); + overviewStore.updateTransactionOverviewInvalidState(true); + statisticsStore.updateTransactionStatisticsInvalidState(true); + } + }); + + const isAutoUpdateExchangeRatesData = computed({ + get: () => settingsStore.appSettings.autoUpdateExchangeRatesData, + set: (value) => settingsStore.setAutoUpdateExchangeRatesData(value) + }); + + const showAccountBalance = computed({ + get: () => settingsStore.appSettings.showAccountBalance, + set: (value) => settingsStore.setShowAccountBalance(value) + }); + + const showAmountInHomePage = computed({ + get: () => settingsStore.appSettings.showAmountInHomePage, + set: (value) => settingsStore.setShowAmountInHomePage(value) + }); + + const timezoneUsedForStatisticsInHomePage = computed({ + get: () => settingsStore.appSettings.timezoneUsedForStatisticsInHomePage, + set: (value: number) => { + settingsStore.setTimezoneUsedForStatisticsInHomePage(value); + overviewStore.updateTransactionOverviewInvalidState(true); + } + }); + + const showTotalAmountInTransactionListPage = computed({ + get: () => settingsStore.appSettings.showTotalAmountInTransactionListPage, + set: (value) => settingsStore.setShowTotalAmountInTransactionListPage(value) + }); + + const showTagInTransactionListPage = computed({ + get: () => settingsStore.appSettings.showTagInTransactionListPage, + set: (value) => settingsStore.setShowTagInTransactionListPage(value) + }); + + const itemsCountInTransactionListPage = computed({ + get: () => settingsStore.appSettings.itemsCountInTransactionListPage, + set: (value) => settingsStore.setItemsCountInTransactionListPage(value) + }); + + const autoSaveTransactionDraft = computed({ + get: () => settingsStore.appSettings.autoSaveTransactionDraft, + set: (value: string) => { + settingsStore.setAutoSaveTransactionDraft(value); + + if (value === 'disabled') { + transactionsStore.clearTransactionDraft(); + } + } + }); + + const isAutoGetCurrentGeoLocation = computed({ + get: () => settingsStore.appSettings.autoGetCurrentGeoLocation, + set: (value) => settingsStore.setAutoGetCurrentGeoLocation(value) + }); + + const currencySortByInExchangeRatesPage = computed({ + get: () => settingsStore.appSettings.currencySortByInExchangeRatesPage, + set: (value: number) => settingsStore.setCurrencySortByInExchangeRatesPage(value) + }); + + return { + // computed states + allTimezones, + allTimezoneTypesUsedForStatistics, + allCurrencySortingTypes, + timeZone, + isAutoUpdateExchangeRatesData, + showAccountBalance, + showAmountInHomePage, + itemsCountInTransactionListPage, + timezoneUsedForStatisticsInHomePage, + showTotalAmountInTransactionListPage, + showTagInTransactionListPage, + autoSaveTransactionDraft, + isAutoGetCurrentGeoLocation, + currencySortByInExchangeRatesPage + }; +} diff --git a/src/views/desktop/app/settings/tabs/AppBasicSettingTab.vue b/src/views/desktop/app/settings/tabs/AppBasicSettingTab.vue index 5e0f9396..8c8df078 100644 --- a/src/views/desktop/app/settings/tabs/AppBasicSettingTab.vue +++ b/src/views/desktop/app/settings/tabs/AppBasicSettingTab.vue @@ -1,7 +1,7 @@ - diff --git a/src/views/mobile/SettingsPage.vue b/src/views/mobile/SettingsPage.vue index e5f63b12..73db7383 100644 --- a/src/views/mobile/SettingsPage.vue +++ b/src/views/mobile/SettingsPage.vue @@ -1,212 +1,174 @@ - diff --git a/src/views/mobile/settings/PageSettingsPage.vue b/src/views/mobile/settings/PageSettingsPage.vue index 079f4c66..65f047be 100644 --- a/src/views/mobile/settings/PageSettingsPage.vue +++ b/src/views/mobile/settings/PageSettingsPage.vue @@ -1,17 +1,17 @@ -