diff --git a/.github/workflows/frontend-deploy.yml b/.github/workflows/frontend-deploy.yml index 703431d240..79b73c57ef 100644 --- a/.github/workflows/frontend-deploy.yml +++ b/.github/workflows/frontend-deploy.yml @@ -125,8 +125,7 @@ jobs: run: pnpm install - name: Build frontend - working-directory: ./apps/frontend - run: pnpm build + run: pnpm web:build env: CF_PAGES_BRANCH: ${{ inputs.head_ref || github.ref_name }} CF_PAGES_COMMIT_SHA: ${{ inputs.head_sha || github.sha }} diff --git a/.github/workflows/frontend-docker.yml b/.github/workflows/frontend-docker.yml new file mode 100644 index 0000000000..484f13125c --- /dev/null +++ b/.github/workflows/frontend-docker.yml @@ -0,0 +1,104 @@ +name: Frontend Docker + +on: + push: + branches: + - main + - prod + paths: + - 'apps/frontend/**/*' + - 'packages/ui/**/*' + - 'packages/utils/**/*' + - 'packages/assets/**/*' + - 'packages/moderation/**/*' + - 'packages/api-client/**/*' + - 'packages/blog/**/*' + - '**/pnpm-*.yaml' + - .github/workflows/frontend-docker.yml + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.ref != 'refs/heads/main' && github.ref != 'refs/heads/prod' }} + +jobs: + build: + name: Build frontend + runs-on: namespace-profile-modrinth-frontend + permissions: + contents: read + steps: + - name: Checkout code + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + + - name: Configure environment + id: meta + env: + REF: ${{ github.ref }} + run: | + if [ "$REF" = "refs/heads/main" ]; then + echo "env=staging" >> $GITHUB_OUTPUT + else + echo "env=production" >> $GITHUB_OUTPUT + fi + + - name: Setup Node + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version-file: .nvmrc + + - name: Enable Corepack + run: corepack enable + + - name: Set up caches + uses: namespacelabs/nscloud-cache-action@c5f8dab7560444c4bf8dbc64f1b203431873c547 # v1.6.1 + with: + cache: | + pnpm + + - name: Inject build variables + working-directory: ./apps/frontend + run: | + if [ "${{ steps.meta.outputs.env }}" == "staging" ]; then + echo "Injecting staging variables from wrangler.jsonc..." + jq -r '.env.staging.vars | to_entries[] | "\(.key)=\(.value)"' wrangler.jsonc >> $GITHUB_ENV + else + echo "Injecting production variables from wrangler.jsonc..." + jq -r '.vars | to_entries[] | "\(.key)=\(.value)"' wrangler.jsonc >> $GITHUB_ENV + fi + + - name: Install dependencies + working-directory: ./apps/frontend + run: pnpm install + + # nuxi is called directly because `pnpm build` hardcodes NODE_OPTIONS, which would + # drop the semi-space bump that cuts roughly 5% off the build. + - name: Build frontend + working-directory: ./apps/frontend + run: pnpm exec nuxi build + env: + NITRO_PRESET: node-server + NODE_OPTIONS: '--max-old-space-size=8192 --max-semi-space-size=64' + BUILD_ENV: ${{ steps.meta.outputs.env }} + CF_PAGES_BRANCH: ${{ github.ref_name }} + CF_PAGES_COMMIT_SHA: ${{ github.sha }} + + - name: Stage Docker context + run: | + mkdir -p apps/frontend/docker-stage + cp -r apps/frontend/.output apps/frontend/docker-stage/.output + + - name: Upload Docker context + uses: namespace-actions/upload-artifact@f6ccaacc655aec41b93af180d1d7eef21af862d2 # v1.0.3 + with: + name: frontend-docker-context + retention-days: 1 + path: apps/frontend/docker-stage + + docker-build: + needs: [build] + uses: SparkUniverse/workflows/.github/workflows/docker-build.yaml@main + with: + image-name: frontend + dockerfile-path: apps/frontend/Dockerfile + artifacts-name: frontend-docker-context diff --git a/.github/workflows/frontend-preview.yml b/.github/workflows/frontend-preview.yml index df17e1786c..16e76136fd 100644 --- a/.github/workflows/frontend-preview.yml +++ b/.github/workflows/frontend-preview.yml @@ -70,10 +70,9 @@ jobs: run: pnpm install - name: Build Storybook - working-directory: ./packages/ui env: NODE_OPTIONS: --max-old-space-size=6144 - run: pnpm run build-storybook + run: pnpm build-storybook - name: Configure short SHA id: meta diff --git a/.github/workflows/i18n-pull.yml b/.github/workflows/i18n-pull.yml index df5ceffa58..185f7815e6 100644 --- a/.github/workflows/i18n-pull.yml +++ b/.github/workflows/i18n-pull.yml @@ -8,6 +8,10 @@ on: concurrency: group: i18n-management +permissions: + contents: write + pull-requests: write + jobs: pull_translations: name: 'Pull translations from Crowdin' @@ -38,23 +42,17 @@ jobs: flight_failure "CROWDIN_PERSONAL_TOKEN secret is not defined (required to push)" fi - if [ "$CROWDIN_GH_TOKEN_DEFINED" != true ]; then - flight_failure "CROWDIN_GH_TOKEN secret is not defined (required to make pull requests)" - fi - if [ "$PREFLIGHT_CHECK_RESULT" = false ]; then exit 1 fi env: CROWDIN_PROJECT_ID_DEFINED: ${{ vars.CROWDIN_PROJECT_ID != '' }} CROWDIN_PERSONAL_TOKEN_DEFINED: ${{ secrets.CROWDIN_PERSONAL_TOKEN != '' }} - CROWDIN_GH_TOKEN_DEFINED: ${{ secrets.CROWDIN_GH_TOKEN != '' }} - name: Checkout uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: ref: ${{ github.ref }} - token: ${{ secrets.CROWDIN_GH_TOKEN }} - name: Setup Node uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 @@ -67,12 +65,6 @@ jobs: - name: Install script dependencies run: pnpm install --frozen-lockfile --ignore-scripts - - name: Configure Git author - id: git-author - uses: MarcoIeni/git-config@59144859caf016f8b817a2ac9b051578729173c4 # v0.1.2 - env: - GITHUB_TOKEN: ${{ secrets.CROWDIN_GH_TOKEN }} - # # Because --all flag of Crowdin CLI is currently broken we need to create a fake source file # # so that the CLI won't omit translations for it. See https://github.com/crowdin/crowdin-cli/issues/724 # - name: Write fake sources @@ -116,7 +108,4 @@ jobs: body-path: .github/templates/crowdin-pr.md commit-message: 'New translations from Crowdin (${{ steps.branch-name.outputs.branch_name }})' branch: crowdin-pull/${{ steps.branch-name.outputs.branch_name }} - author: '${{ steps.git-author.outputs.name }} <${{ steps.git-author.outputs.email }}>' - committer: '${{ steps.git-author.outputs.name }} <${{ steps.git-author.outputs.email }}>' labels: sync - token: ${{ secrets.CROWDIN_GH_TOKEN }} diff --git a/.gitignore b/.gitignore index 9d27dcaf5e..d442251099 100644 --- a/.gitignore +++ b/.gitignore @@ -55,6 +55,7 @@ Thumbs.db # frontend generated files apps/frontend/src/generated +/packages/ui/src/layouts/wrapped/settings/language-settings/language-settings-coverage.generated.ts .turbo target diff --git a/apps/app-frontend/package.json b/apps/app-frontend/package.json index 5a88bd2d92..abe6e55c0f 100644 --- a/apps/app-frontend/package.json +++ b/apps/app-frontend/package.json @@ -38,7 +38,6 @@ "intl-messageformat": "^10.7.7", "ofetch": "^1.3.4", "overlayscrollbars": "^2.15.1", - "pinia": "^3.0.0", "posthog-js": "^1.158.2", "three": "^0.172.0", "vite-svg-loader": "^5.1.0", diff --git a/apps/app-frontend/src/App.vue b/apps/app-frontend/src/App.vue index eeb37273f6..7456fa2dec 100644 --- a/apps/app-frontend/src/App.vue +++ b/apps/app-frontend/src/App.vue @@ -94,6 +94,9 @@ import SurveyPopup from '@/components/ui/SurveyPopup.vue' import WindowControls from '@/components/ui/WindowControls.vue' import { useCheckDisableMouseover } from '@/composables/macCssFix.js' import { useAppEvent } from '@/composables/use-app-event' +import { useAppSettings } from '@/composables/use-app-settings.ts' +import { useError } from '@/composables/use-error.js' +import { useTheme } from '@/composables/use-theme.ts' import { config } from '@/config' import { hide_ads_window, @@ -147,8 +150,7 @@ import { setupProviders } from '@/providers/setup' import { setupAppEventsProvider } from '@/providers/setup/app-events' import { setupAuthProvider } from '@/providers/setup/auth' import { setupLoadingStateProvider } from '@/providers/setup/loading-state' -import { useError } from '@/store/error.js' -import { useTheming } from '@/store/state' +import { setupAppUserPreferencesProvider } from '@/providers/setup/user-preferences.ts' import { appMessages } from '@/utils/app-messages' import { generateSkinPreviews } from './helpers/rendering/batch-skin-renderer' @@ -157,7 +159,8 @@ import { AppNotificationManager } from './providers/app-notifications' import { AppPopupNotificationManager } from './providers/app-popup-notifications' import { appSettingsModalOpenProfileKey } from './providers/app-settings-modal' -const themeStore = useTheming() +const appSettings = useAppSettings() +const appTheme = useTheme() const router = useRouter() const route = useRoute() const { channel: appEventChannel, events: appEvents } = setupAppEventsProvider() @@ -201,7 +204,7 @@ const credentials = ref() let credentialsRefreshId = 0 const sidebarToggled = ref(true) watch( - () => themeStore.toggleSidebar, + () => appSettings.toggleSidebar, (toggleSidebar) => { sidebarToggled.value = !toggleSidebar }, @@ -221,7 +224,7 @@ const hostingUpdateRequired = computed( appUpdateState.updatesEnabled.value, ) const prideFundraiserEnabled = computed( - () => themeStore.getFeatureFlag('pride_fundraiser') && Date.now() < PRIDE_FUNDRAISER_END_DATE, + () => appSettings.getFeatureFlag('pride_fundraiser') && Date.now() < PRIDE_FUNDRAISER_END_DATE, ) const hostingIntercomIdentityKey = computed(() => { const rawServerId = route.params.id @@ -324,13 +327,13 @@ providePageContext({ intercomBubble: hostingIntercom.intercomBubble, featureFlags: { serverRamAsBytesAlwaysOn: computed(() => - themeStore.getFeatureFlag('server_ram_as_bytes_always_on'), + appSettings.getFeatureFlag('server_ram_as_bytes_always_on'), ), }, openExternalUrl: (url) => void openUrl(url), }) provideModalBehavior({ - noblur: computed(() => !themeStore.advancedRendering), + noblur: computed(() => !appTheme.advancedRendering), onShow: () => take_ads_window_hold(), onHide: () => release_ads_window_hold(), }) @@ -634,10 +637,11 @@ async function setupApp() { theme, locale, telemetry, - collapsed_navigation, hide_nametag_skins_page, advanced_rendering, toggle_sidebar, + sync_theme_across_devices, + sync_behavior_across_devices, developer_mode, feature_flags, pending_update_toast_for_version, @@ -655,13 +659,14 @@ async function setupApp() { nativeDecorations.value = native_decorations if (os.value !== 'MacOS') await getCurrentWindow().setDecorations(native_decorations) - themeStore.setThemeState(theme) - themeStore.collapsedNavigation = collapsed_navigation - themeStore.advancedRendering = advanced_rendering - themeStore.hideNametagSkinsPage = hide_nametag_skins_page - themeStore.toggleSidebar = toggle_sidebar - themeStore.devMode = developer_mode - themeStore.featureFlags = feature_flags + appTheme.preferred = theme + appTheme.advancedRendering = advanced_rendering + appTheme.syncAcrossDevices = sync_theme_across_devices + appSettings.syncBehaviorAcrossDevices = sync_behavior_across_devices + appSettings.hideNametagSkinsPage = hide_nametag_skins_page + appSettings.toggleSidebar = toggle_sidebar + appSettings.devMode = developer_mode + Object.assign(appSettings.featureFlags, feature_flags) stateInitialized.value = true isMaximized.value = await getCurrentWindow().isMaximized() @@ -959,7 +964,7 @@ watch(incompatibilityWarningModal, (modal) => { } }) -setupAuthProvider(credentials, async (_redirectPath, flow, options) => { +const authProvider = setupAuthProvider(credentials, async (_redirectPath, flow, options) => { if (options?.showModal === false) { await signIn(flow) } else { @@ -967,6 +972,79 @@ setupAuthProvider(credentials, async (_redirectPath, flow, options) => { } }) +const userPreferences = setupAppUserPreferencesProvider(authProvider, notificationManager) +let userPreferencesSync = Promise.resolve() + +watch( + [userPreferences.preferences, stateInitialized], + ([preferences, initialized]) => { + if (!preferences || !initialized) return + + userPreferencesSync = userPreferencesSync + .then(async () => { + const settings = await getSettings() + const selectedTheme = preferences.appearance.auto ? 'system' : preferences.appearance.theme + const locale = preferences.localization.locale + const behavior = preferences.behavior + let settingsChanged = false + + if (appTheme.syncAcrossDevices && appTheme.preferred !== selectedTheme) { + appTheme.preferred = selectedTheme + } + if (i18n.global.locale.value !== locale) { + i18n.global.locale.value = locale + } + + if (appTheme.syncAcrossDevices && settings.theme !== selectedTheme) { + settings.theme = selectedTheme + settingsChanged = true + } + if (settings.locale !== locale) { + settings.locale = locale + settingsChanged = true + } + + if (behavior && appSettings.syncBehaviorAcrossDevices) { + const behaviorFeatureFlags = { + worlds_in_home: behavior.show_jump_in, + show_instance_play_time: behavior.show_play_time, + skip_unknown_pack_warning: !behavior.warn_on_unknown_modpacks, + skip_non_essential_warnings: behavior.skip_non_essential_warnings, + } + + appSettings.toggleSidebar = behavior.hide_right_sidebar + appSettings.hideNametagSkinsPage = behavior.hide_nametag + Object.assign(appSettings.featureFlags, behaviorFeatureFlags) + + if (settings.hide_on_process_start !== behavior.minimize_app) { + settings.hide_on_process_start = behavior.minimize_app + settingsChanged = true + } + if (settings.toggle_sidebar !== behavior.hide_right_sidebar) { + settings.toggle_sidebar = behavior.hide_right_sidebar + settingsChanged = true + } + if (settings.hide_nametag_skins_page !== behavior.hide_nametag) { + settings.hide_nametag_skins_page = behavior.hide_nametag + settingsChanged = true + } + for (const [flag, value] of Object.entries(behaviorFeatureFlags)) { + if (settings.feature_flags[flag] !== value) { + settings.feature_flags[flag] = value + settingsChanged = true + } + } + } + + if (settingsChanged) { + await setSettings(settings) + } + }) + .catch(handleError) + }, + { immediate: true }, +) + async function validateSession(sessionToken) { try { const response = await tauriFetch(`${config.labrinthBaseUrl}/v2/user`, { @@ -1642,7 +1720,7 @@ provideAppUpdateDownloadProgress(appUpdateDownload)
@@ -1867,7 +1945,7 @@ provideAppUpdateDownloadProgress(appUpdateDownload)
{{ route.fullPath }} diff --git a/apps/app-frontend/src/components/ui/AccountsCard.vue b/apps/app-frontend/src/components/ui/AccountsCard.vue index bcb144f450..6dacd0f6a2 100644 --- a/apps/app-frontend/src/components/ui/AccountsCard.vue +++ b/apps/app-frontend/src/components/ui/AccountsCard.vue @@ -112,6 +112,7 @@ import type { Ref } from 'vue' import { computed, ref } from 'vue' import { useAppEvent } from '@/composables/use-app-event' +import { handleSevereError } from '@/composables/use-error.js' import { trackEvent } from '@/helpers/analytics' import { get_default_user, @@ -123,7 +124,6 @@ import { import { getPlayerHeadUrl } from '@/helpers/rendering/batch-skin-renderer.ts' import type { Skin } from '@/helpers/skins' import { get_available_skins } from '@/helpers/skins' -import { handleSevereError } from '@/store/error.js' const { formatMessage } = useVIntl() const { handleError } = injectNotificationManager() diff --git a/apps/app-frontend/src/components/ui/ErrorModal.vue b/apps/app-frontend/src/components/ui/ErrorModal.vue index 475c1f3f76..efac61720c 100644 --- a/apps/app-frontend/src/components/ui/ErrorModal.vue +++ b/apps/app-frontend/src/components/ui/ErrorModal.vue @@ -20,11 +20,11 @@ import { computed, ref } from 'vue' import { ChatIcon } from '@/assets/icons' import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue' +import { handleSevereError } from '@/composables/use-error.js' import { trackEvent } from '@/helpers/analytics' import { login as login_flow, set_default_user } from '@/helpers/auth.js' import { install_existing_instance } from '@/helpers/install' import { cancel_directory_change } from '@/helpers/settings.ts' -import { handleSevereError } from '@/store/error.js' const { handleError } = injectNotificationManager() diff --git a/apps/app-frontend/src/components/ui/Instance.vue b/apps/app-frontend/src/components/ui/Instance.vue index 2bc32b67fc..9f8338df6d 100644 --- a/apps/app-frontend/src/components/ui/Instance.vue +++ b/apps/app-frontend/src/components/ui/Instance.vue @@ -20,12 +20,12 @@ import { computed, onMounted, ref } from 'vue' import { useRouter } from 'vue-router' import { useAppEvent } from '@/composables/use-app-event' +import { handleSevereError } from '@/composables/use-error.js' import { trackEvent } from '@/helpers/analytics' import { install_existing_instance, install_pack_to_existing_instance } from '@/helpers/install' import { getInstanceIconUrl, kill, run } from '@/helpers/instance' import { get_by_instance_id } from '@/helpers/process' import { showInstanceInFolder } from '@/helpers/utils.js' -import { handleSevereError } from '@/store/error.js' const { handleError } = injectNotificationManager() const { formatMessage } = useVIntl() diff --git a/apps/app-frontend/src/components/ui/WindowControls.vue b/apps/app-frontend/src/components/ui/WindowControls.vue index 81797ba5fa..a10ccffff5 100644 --- a/apps/app-frontend/src/components/ui/WindowControls.vue +++ b/apps/app-frontend/src/components/ui/WindowControls.vue @@ -39,17 +39,17 @@ import { getCurrentWindow } from '@tauri-apps/api/window' import { saveWindowState, StateFlags } from '@tauri-apps/plugin-window-state' import { computed, onMounted, onUnmounted, ref } from 'vue' +import { useAppSettings } from '@/composables/use-app-settings.ts' import { get as getSettings } from '@/helpers/settings.ts' import { getOS } from '@/helpers/utils.js' -import { useTheming } from '@/store/state' -const themeStore = useTheming() +const appSettings = useAppSettings() const nativeDecorations = ref(true) const isMaximized = ref(false) const os = ref('') -const alwaysShowAppControls = computed(() => themeStore.getFeatureFlag('always_show_app_controls')) +const alwaysShowAppControls = computed(() => appSettings.getFeatureFlag('always_show_app_controls')) const showControls = computed( () => diff --git a/apps/app-frontend/src/components/ui/friends/FriendsList.vue b/apps/app-frontend/src/components/ui/friends/FriendsList.vue index 705b3c16a1..7294781e87 100644 --- a/apps/app-frontend/src/components/ui/friends/FriendsList.vue +++ b/apps/app-frontend/src/components/ui/friends/FriendsList.vue @@ -15,17 +15,17 @@ import { computed, ref } from 'vue' import FriendsSection from '@/components/ui/friends/FriendsSection.vue' import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue' +import { useAppSettings } from '@/composables/use-app-settings.ts' import { useFriends } from '@/composables/use-friends' import type { FriendWithUserData } from '@/helpers/friends.ts' import type { ModrinthCredentials } from '@/helpers/mr_auth' import { get as getSettings, set as setSettings } from '@/helpers/settings.ts' -import { useTheming } from '@/store/state' const { formatMessage } = useVIntl() const { handleError } = injectNotificationManager() const formatRelativeTime = useRelativeTime() -const themeStore = useTheming() +const appSettings = useAppSettings() const props = defineProps<{ credentials: ModrinthCredentials | null @@ -39,11 +39,11 @@ type FriendsSectionCollapsedFlag = | 'friends_pending_collapsed' function isFriendsSectionCollapsed(flag: FriendsSectionCollapsedFlag) { - return themeStore.getFeatureFlag(flag) + return appSettings.getFeatureFlag(flag) } function setFriendsSectionCollapsed(flag: FriendsSectionCollapsedFlag, collapsed: boolean) { - themeStore.featureFlags[flag] = collapsed + appSettings.featureFlags[flag] = collapsed getSettings() .then((settings) => { settings.feature_flags[flag] = collapsed diff --git a/apps/app-frontend/src/components/ui/install_flow/UnknownPackWarningModal.vue b/apps/app-frontend/src/components/ui/install_flow/UnknownPackWarningModal.vue index f1b929e050..b1063c805d 100644 --- a/apps/app-frontend/src/components/ui/install_flow/UnknownPackWarningModal.vue +++ b/apps/app-frontend/src/components/ui/install_flow/UnknownPackWarningModal.vue @@ -13,11 +13,10 @@ import { UnknownFileWarningModal } from '@modrinth/ui' import { ref, useTemplateRef } from 'vue' +import { type FeatureFlag, useAppSettings } from '@/composables/use-app-settings.ts' import { get as getSettings, set as setSettings } from '@/helpers/settings' -import { useTheming } from '@/store/state' -import type { FeatureFlag } from '@/store/theme.ts' -const themeStore = useTheming() +const appSettings = useAppSettings() const skipUnknownPackWarningFeatureFlag = 'skip_unknown_pack_warning' as FeatureFlag const modal = useTemplateRef('modal') @@ -34,7 +33,7 @@ function show( fileName.value = selectedFileName externalFilesInModpack.value = selectedExternalFiles - if (themeStore.getFeatureFlag(skipUnknownPackWarningFeatureFlag)) { + if (appSettings.getFeatureFlag(skipUnknownPackWarningFeatureFlag)) { void createInstance() return } @@ -50,7 +49,7 @@ function reset() { async function proceed(dontShowAgain: boolean) { if (dontShowAgain) { - themeStore.featureFlags[skipUnknownPackWarningFeatureFlag] = true + appSettings.featureFlags[skipUnknownPackWarningFeatureFlag] = true const settings = await getSettings() settings.feature_flags[skipUnknownPackWarningFeatureFlag] = true await setSettings(settings) diff --git a/apps/app-frontend/src/components/ui/library/instance-group/instance-card.vue b/apps/app-frontend/src/components/ui/library/instance-group/instance-card.vue index ff01d3d98e..e8499b9bfb 100644 --- a/apps/app-frontend/src/components/ui/library/instance-group/instance-card.vue +++ b/apps/app-frontend/src/components/ui/library/instance-group/instance-card.vue @@ -9,13 +9,13 @@ import { useRouter } from 'vue-router' import InstanceCardView from '@/components/ui/library/instance-group/instance-card-view.vue' import { getLibraryInstanceSelectionKey, useLibrary } from '@/components/ui/library/use-library' import { useAppEvent } from '@/composables/use-app-event' +import { handleSevereError } from '@/composables/use-error.js' import { trackEvent } from '@/helpers/analytics' import { install_existing_instance, install_pack_to_existing_instance } from '@/helpers/install' import { kill, run } from '@/helpers/instance' import { get_by_instance_id } from '@/helpers/process' import type { GameInstance } from '@/helpers/types' import { showInstanceInFolder } from '@/helpers/utils.js' -import { handleSevereError } from '@/store/error.js' type ProcessEvent = 'installing' | 'launched' | 'finished' diff --git a/apps/app-frontend/src/components/ui/minecraft-auth-error-modal/MinecraftAuthErrorModal.vue b/apps/app-frontend/src/components/ui/minecraft-auth-error-modal/MinecraftAuthErrorModal.vue index 9f29f9a945..8d34f9e1f8 100644 --- a/apps/app-frontend/src/components/ui/minecraft-auth-error-modal/MinecraftAuthErrorModal.vue +++ b/apps/app-frontend/src/components/ui/minecraft-auth-error-modal/MinecraftAuthErrorModal.vue @@ -10,8 +10,8 @@ import { import { Admonition, Button, ButtonLink, Collapsible, IconButton, NewModal } from '@modrinth/ui' import { computed, ref } from 'vue' +import { handleSevereError } from '@/composables/use-error.js' import { login as login_flow, set_default_user } from '@/helpers/auth.js' -import { handleSevereError } from '@/store/error.js' import { findMinecraftAuthError, type MinecraftAuthError } from './minecraft-auth-errors' diff --git a/apps/app-frontend/src/components/ui/minecraft-required-modal/MinecraftRequiredModal.vue b/apps/app-frontend/src/components/ui/minecraft-required-modal/MinecraftRequiredModal.vue index c22b268e2a..73293f5bed 100644 --- a/apps/app-frontend/src/components/ui/minecraft-required-modal/MinecraftRequiredModal.vue +++ b/apps/app-frontend/src/components/ui/minecraft-required-modal/MinecraftRequiredModal.vue @@ -63,9 +63,9 @@ import { inject, type Ref, ref } from 'vue' import steveImage from '@/assets/steve-look-up-left.webp' import type AccountsCard from '@/components/ui/AccountsCard.vue' +import { handleSevereError } from '@/composables/use-error.js' import { trackEvent } from '@/helpers/analytics' import { login as loginFlow, set_default_user } from '@/helpers/auth.js' -import { handleSevereError } from '@/store/error.js' const { formatMessage } = useVIntl() const accountsCard = inject('accountsCard') as Ref | null> diff --git a/apps/app-frontend/src/components/ui/modal/AppSettingsModal.vue b/apps/app-frontend/src/components/ui/modal/AppSettingsModal.vue index 90f4b25499..97258665d3 100644 --- a/apps/app-frontend/src/components/ui/modal/AppSettingsModal.vue +++ b/apps/app-frontend/src/components/ui/modal/AppSettingsModal.vue @@ -36,16 +36,16 @@ import LanguageSettings from '@/components/ui/settings/display/LanguageSettings. import DefaultInstanceSettings from '@/components/ui/settings/instances/DefaultInstanceSettings.vue' import JavaSettings from '@/components/ui/settings/instances/JavaSettings.vue' import ResourceManagementSettings from '@/components/ui/settings/instances/ResourceManagementSettings.vue' +import { useAppSettings } from '@/composables/use-app-settings.ts' import { get, set } from '@/helpers/settings.ts' import { appSettingsModalContextKey, type UnsavedChangesController, } from '@/providers/app-settings-modal' import { injectAppUpdateDownloadProgress } from '@/providers/download-progress.ts' -import { useTheming } from '@/store/state' // TODO: Apply COMPONENT_STRUCTURE.md here and extract out common setting option components -const themeStore = useTheming() +const appSettings = useAppSettings() const { formatMessage } = useVIntl() @@ -157,7 +157,9 @@ const tabs = [ }, ] -const availableTabs = computed(() => tabs.filter((tab) => !tab.developerOnly || themeStore.devMode)) +const availableTabs = computed(() => + tabs.filter((tab) => !tab.developerOnly || appSettings.devMode), +) const modal = ref | null>(null) const unsavedChangesPopup = ref<{ nudge: () => void } | null>(null) @@ -170,10 +172,19 @@ const modifiedUnsavedChangesState = computed( () => unsavedChangesController.value?.getModified() ?? emptyUnsavedChangesState, ) const savingUnsavedChanges = computed(() => unsavedChangesController.value?.isSaving() ?? false) -const hasUnsavedChanges = computed(() => unsavedChangesController.value?.hasChanges() ?? false) +const hasUnsavedChanges = computed( + () => + (unsavedChangesController.value?.hasChanges() ?? false) || + (unsavedChangesController.value?.isSaving() ?? false), +) function canLeaveCurrentTab(): boolean { - if (!unsavedChangesController.value?.hasChanges()) return true + if ( + !unsavedChangesController.value?.hasChanges() && + !unsavedChangesController.value?.isSaving() + ) { + return true + } unsavedChangesPopup.value?.nudge() return false } @@ -233,8 +244,8 @@ function devModeCount() { if (devModeCounter.value > 5) { const selectedTab = modal.value ? availableTabs.value[modal.value.selectedTab] : undefined - themeStore.devMode = !themeStore.devMode - settings.value.developer_mode = !!themeStore.devMode + appSettings.devMode = !appSettings.devMode + settings.value.developer_mode = !!appSettings.devMode devModeCounter.value = 0 if (modal.value) { @@ -298,7 +309,7 @@ const messages = defineMessages({
-

+

{{ formatMessage(developerModeEnabled) }}

@@ -306,8 +317,8 @@ const messages = defineMessages({ :aria-label="formatMessage(messages.developerModeButtonLabel)" class="p-0 m-0 bg-transparent border-none cursor-pointer button-animation" :class="{ - 'text-brand': themeStore.devMode, - 'text-secondary': !themeStore.devMode, + 'text-brand': appSettings.devMode, + 'text-secondary': !appSettings.devMode, }" @click="devModeCount" > diff --git a/apps/app-frontend/src/components/ui/settings/account/SocialSettings.vue b/apps/app-frontend/src/components/ui/settings/account/SocialSettings.vue index 15bb622c17..f74eae6598 100644 --- a/apps/app-frontend/src/components/ui/settings/account/SocialSettings.vue +++ b/apps/app-frontend/src/components/ui/settings/account/SocialSettings.vue @@ -1,5 +1,6 @@