diff --git a/apps/app-frontend/src/App.vue b/apps/app-frontend/src/App.vue index 674ba88fe18..0844b05a1c1 100644 --- a/apps/app-frontend/src/App.vue +++ b/apps/app-frontend/src/App.vue @@ -105,6 +105,7 @@ import { createInstanceLaunchState, provideInstanceLaunchState, } from '@/features/instances/launch-state' +import { maxMemoryQueryOptions } from '@/features/java/queries' import SharedInstanceLockNotice from '@/features/library/shared-instances/shared-instance-lock-notice.vue' import SharedInstanceModals from '@/features/library/shared-instances/shared-instance-modals.vue' import { provideMinecraftAccounts } from '@/features/minecraft-accounts/context' @@ -131,7 +132,6 @@ import { run, set_global_synced_option, } from '@/helpers/instance' -import { maxMemoryQueryOptions } from '@/helpers/jre.js' import { get as getCreds, getAll as getAllCreds, diff --git a/apps/app-frontend/src/components/ui/JavaDetectionModal.vue b/apps/app-frontend/src/components/ui/JavaDetectionModal.vue deleted file mode 100644 index 87858c7f6ea..00000000000 --- a/apps/app-frontend/src/components/ui/JavaDetectionModal.vue +++ /dev/null @@ -1,130 +0,0 @@ - - diff --git a/apps/app-frontend/src/components/ui/JavaSelector.vue b/apps/app-frontend/src/components/ui/JavaSelector.vue deleted file mode 100644 index ff0631397e4..00000000000 --- a/apps/app-frontend/src/components/ui/JavaSelector.vue +++ /dev/null @@ -1,304 +0,0 @@ - - - - - diff --git a/apps/app-frontend/src/composables/useJavaTest.ts b/apps/app-frontend/src/composables/useJavaTest.ts deleted file mode 100644 index 448854454c6..00000000000 --- a/apps/app-frontend/src/composables/useJavaTest.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { ref } from 'vue' - -import { trackEvent } from '@/helpers/analytics' -import { get_jre, test_jre } from '@/helpers/jre.js' - -export default function useJavaTest() { - const testingJava = ref(false) - const javaTestResult = ref(null) - let testDebounceTimer: ReturnType | null = null - - async function runJavaTest(path: string, version: number | null, track = true) { - if (testDebounceTimer) { - clearTimeout(testDebounceTimer) - testDebounceTimer = null - } - if (!path) { - javaTestResult.value = null - return - } - testingJava.value = true - try { - javaTestResult.value = - version === null ? !!(await get_jre(path)) : await test_jre(path, version) - } catch { - javaTestResult.value = false - } - testingJava.value = false - - if (track) { - trackEvent('JavaTest', { path, success: javaTestResult.value }) - } - } - - function testJavaInstallationDebounced(path: string, version: number | null, delay = 600) { - if (testDebounceTimer) clearTimeout(testDebounceTimer) - if (!path) { - javaTestResult.value = null - return - } - testDebounceTimer = setTimeout(() => runJavaTest(path, version, false), delay) - } - - async function testJavaInstallation(path: string, version: number | null, track = false) { - await runJavaTest(path, version, track) - } - - return { - testingJava, - javaTestResult, - testJavaInstallationDebounced, - testJavaInstallation, - } -} diff --git a/apps/app-frontend/src/composables/useMemorySlider.js b/apps/app-frontend/src/composables/useMemorySlider.js deleted file mode 100644 index fb9d577659c..00000000000 --- a/apps/app-frontend/src/composables/useMemorySlider.js +++ /dev/null @@ -1,23 +0,0 @@ -import { useQuery } from '@tanstack/vue-query' -import { computed } from 'vue' - -import { maxMemoryQueryOptions } from '@/helpers/jre.js' - -export default function () { - const memoryQuery = useQuery(maxMemoryQueryOptions()) - const maxMemory = computed(() => Math.floor((memoryQuery.data.value ?? 0) / 1024)) - - const snapPoints = computed(() => { - let points = [] - let memory = 2048 - - while (memory <= maxMemory.value) { - points.push(memory) - memory *= 2 - } - - return points - }) - - return { maxMemory, snapPoints, memoryQuery } -} diff --git a/apps/app-frontend/src/features/instances/settings-modal/default-settings-intro.vue b/apps/app-frontend/src/features/instances/settings-modal/default-settings-intro.vue index c9e638e67ce..ef664c45ee7 100644 --- a/apps/app-frontend/src/features/instances/settings-modal/default-settings-intro.vue +++ b/apps/app-frontend/src/features/instances/settings-modal/default-settings-intro.vue @@ -30,7 +30,9 @@ function showDefaultSettings() { -
+

{{ formatMessage(messages.sharedSettingsDescription) }}

diff --git a/apps/app-frontend/src/features/instances/settings-modal/use-instance-settings-draft.ts b/apps/app-frontend/src/features/instances/settings-modal/use-instance-settings-draft.ts new file mode 100644 index 00000000000..20aacbf57c7 --- /dev/null +++ b/apps/app-frontend/src/features/instances/settings-modal/use-instance-settings-draft.ts @@ -0,0 +1,64 @@ +import { injectNotificationManager } from '@modrinth/ui' +import { onBeforeUnmount, onMounted, type Ref, ref, watch } from 'vue' + +import { injectInstanceSettings } from './instance-settings-context' + +function clone(value: T): T { + return JSON.parse(JSON.stringify(value)) +} + +function isEqual(a: unknown, b: unknown): boolean { + return JSON.stringify(a) === JSON.stringify(b) +} + +export function useInstanceSettingsDraft>( + initial: () => T, + persist: (value: T) => Promise, +): Ref { + const { handleError } = injectNotificationManager() + const { registerUnsavedChangesController } = injectInstanceSettings() + + const original = ref(clone(initial())) as Ref + const draft = ref(clone(initial())) as Ref + const saving = ref(false) + + watch(initial, (next) => { + if (saving.value || isEqual(next, original.value)) return + if (!isEqual(draft.value, original.value)) return + original.value = clone(next) + draft.value = clone(next) + }) + + async function save() { + if (saving.value) return + saving.value = true + const value = clone(draft.value) + try { + await persist(value) + original.value = value + } catch (error) { + handleError(error as Error) + } finally { + saving.value = false + } + } + + onMounted(() => { + registerUnsavedChangesController({ + hasChanges: () => !isEqual(original.value, draft.value), + getOriginal: () => original.value, + getModified: () => draft.value, + isSaving: () => saving.value, + reset: () => { + draft.value = clone(original.value) + }, + save, + }) + }) + + onBeforeUnmount(() => { + registerUnsavedChangesController(null) + }) + + return draft +} diff --git a/apps/app-frontend/src/features/instances/settings-modal/window-settings.vue b/apps/app-frontend/src/features/instances/settings-modal/window-settings.vue index 0c65199214e..2f7aeb998a5 100644 --- a/apps/app-frontend/src/features/instances/settings-modal/window-settings.vue +++ b/apps/app-frontend/src/features/instances/settings-modal/window-settings.vue @@ -1,68 +1,65 @@ + + diff --git a/apps/app-frontend/src/features/java/java-selector/index.vue b/apps/app-frontend/src/features/java/java-selector/index.vue new file mode 100644 index 00000000000..ed61e642a0a --- /dev/null +++ b/apps/app-frontend/src/features/java/java-selector/index.vue @@ -0,0 +1,315 @@ + + + diff --git a/apps/app-frontend/src/features/java/memory-slider.vue b/apps/app-frontend/src/features/java/memory-slider.vue new file mode 100644 index 00000000000..357829d9375 --- /dev/null +++ b/apps/app-frontend/src/features/java/memory-slider.vue @@ -0,0 +1,56 @@ + + + diff --git a/apps/app-frontend/src/features/java/queries.ts b/apps/app-frontend/src/features/java/queries.ts new file mode 100644 index 00000000000..71c3a326ef6 --- /dev/null +++ b/apps/app-frontend/src/features/java/queries.ts @@ -0,0 +1,52 @@ +import { queryOptions } from '@tanstack/vue-query' + +import { + findFilteredJres, + getJavaVersions, + getJre, + getMaxMemory, +} from '@/platform/app-lib/jre/commands' + +export const javaKeys = { + versions: ['system', 'java-versions'] as const, + jre: (path: string) => ['system', 'jre', path] as const, + detected: (version: number | null) => ['system', 'detected-jres', version] as const, + maxMemory: ['system', 'memory', 'total'] as const, +} + +export function javaVersionsQueryOptions() { + return queryOptions({ + queryKey: javaKeys.versions, + networkMode: 'always', + queryFn: getJavaVersions, + staleTime: 0, + }) +} + +export function jreQueryOptions(path: string) { + return queryOptions({ + queryKey: javaKeys.jre(path), + networkMode: 'always', + queryFn: () => getJre(path).catch(() => null), + staleTime: Infinity, + }) +} + +export function detectedJresQueryOptions(version: number | null) { + return queryOptions({ + queryKey: javaKeys.detected(version), + networkMode: 'always', + queryFn: () => findFilteredJres(version), + staleTime: 0, + }) +} + +export function maxMemoryQueryOptions() { + return queryOptions({ + queryKey: javaKeys.maxMemory, + networkMode: 'always', + queryFn: getMaxMemory, + staleTime: Infinity, + gcTime: Infinity, + }) +} diff --git a/apps/app-frontend/src/features/settings/app-settings-modal/context.ts b/apps/app-frontend/src/features/settings/app-settings-modal/context.ts index 0efb19f8e22..b1be0386ecd 100644 --- a/apps/app-frontend/src/features/settings/app-settings-modal/context.ts +++ b/apps/app-frontend/src/features/settings/app-settings-modal/context.ts @@ -1,13 +1,6 @@ import { createContext } from '@modrinth/ui' -export type UnsavedChangesController = { - hasChanges: () => boolean - getOriginal: () => Record - getModified: () => Record - isSaving: () => boolean - reset: () => void - save: () => void | Promise -} +import type { UnsavedChangesController } from '@/shared/unsaved-changes/types' export interface AppSettingsModalContext { close: () => boolean diff --git a/apps/app-frontend/src/features/settings/app-settings-modal/index.vue b/apps/app-frontend/src/features/settings/app-settings-modal/index.vue index 567bc7d7247..a669ece1b69 100644 --- a/apps/app-frontend/src/features/settings/app-settings-modal/index.vue +++ b/apps/app-frontend/src/features/settings/app-settings-modal/index.vue @@ -50,8 +50,9 @@ import ResourceManagementSettings from '@/features/settings/system/resource-mana import TroubleshootingSettings from '@/features/settings/system/troubleshooting-settings.vue' import { appSettingsKeys, appSettingsQueryOptions, set } from '@/helpers/settings.ts' import { injectAppUpdateDownloadProgress } from '@/providers/download-progress.ts' +import { useUnsavedChangesHost } from '@/shared/unsaved-changes/use-unsaved-changes-host' -import { provideAppSettingsModal, type UnsavedChangesController } from './context' +import { provideAppSettingsModal } from './context' // TODO: Apply COMPONENT_STRUCTURE.md here and extract out common setting option components const appSettings = useAppSettings() @@ -213,54 +214,27 @@ const availableTabs = computed(() => ) const modal = ref | null>(null) -const unsavedChangesPopup = ref<{ nudge: () => void } | null>(null) -const unsavedChangesController = ref(null) -const emptyUnsavedChangesState: Record = {} -const originalUnsavedChangesState = computed( - () => unsavedChangesController.value?.getOriginal() ?? emptyUnsavedChangesState, -) -const modifiedUnsavedChangesState = computed( - () => unsavedChangesController.value?.getModified() ?? emptyUnsavedChangesState, -) -const savingUnsavedChanges = computed(() => unsavedChangesController.value?.isSaving() ?? false) -const hasUnsavedChanges = computed( - () => - (unsavedChangesController.value?.hasChanges() ?? false) || - (unsavedChangesController.value?.isSaving() ?? false), -) - -function canLeaveCurrentTab(): boolean { - if ( - !unsavedChangesController.value?.hasChanges() && - !unsavedChangesController.value?.isSaving() - ) { - return true - } - unsavedChangesPopup.value?.nudge() - return false -} +const { + popup: unsavedChangesPopup, + original: originalUnsavedChangesState, + modified: modifiedUnsavedChangesState, + saving: savingUnsavedChanges, + hasUnsavedChanges, + canLeave: canLeaveCurrentTab, + register: registerUnsavedChangesController, + reset: resetUnsavedChanges, + save: saveUnsavedChanges, +} = useUnsavedChangesHost() function close(): boolean { return modal.value?.hide() ?? false } -function registerUnsavedChangesController(controller: UnsavedChangesController | null): void { - unsavedChangesController.value = controller -} - provideAppSettingsModal({ close, registerUnsavedChangesController, }) -function resetUnsavedChanges(): void { - unsavedChangesController.value?.reset() -} - -function saveUnsavedChanges(): void { - void unsavedChangesController.value?.save() -} - function show() { modal.value?.show() } @@ -385,13 +359,11 @@ const messages = defineMessages({