diff --git a/Cargo.lock b/Cargo.lock index 045a26a5e6f..43dae65372f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -13762,6 +13762,7 @@ dependencies = [ "infer 0.22.0", "itertools 0.15.0", "json5", + "libc", "minecraft-meta-manifest", "modrinth-content-management", "notify", diff --git a/Cargo.toml b/Cargo.toml index a6a0fe00e35..24154a871e9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -138,6 +138,7 @@ lettre = { version = "0.11.23", default-features = false, features = [ "tokio1", "tokio1-rustls", ] } +libc = "0.2.191" lingua = "=1.8.0" linkify = "0.11.0" lz4_flex = { version = "0.14.0", default-features = false, features = [ diff --git a/apps/app-frontend/src/components/ui/SplashScreen.vue b/apps/app-frontend/src/components/ui/SplashScreen.vue index c63421f80f6..1d79183bff5 100644 --- a/apps/app-frontend/src/components/ui/SplashScreen.vue +++ b/apps/app-frontend/src/components/ui/SplashScreen.vue @@ -67,8 +67,8 @@ - - {{ message }} + + {{ message }}
@@ -78,8 +78,8 @@ diff --git a/apps/app-frontend/src/features/library/shared-instances/shared-instance-modals.vue b/apps/app-frontend/src/features/library/shared-instances/shared-instance-modals.vue index 9962666b3e3..3e459d0d37a 100644 --- a/apps/app-frontend/src/features/library/shared-instances/shared-instance-modals.vue +++ b/apps/app-frontend/src/features/library/shared-instances/shared-instance-modals.vue @@ -57,6 +57,7 @@ async function install( async () => { if (loadingIds.value.has(sharedInstanceId)) return setLoading(sharedInstanceId, true) + let jobId: string try { const job = await install_shared_instance( sharedInstanceId, @@ -67,12 +68,16 @@ async function install( instance.icon_path ?? null, instance.icon_path ? (instance.icon_config ?? null) : null, ) + jobId = job.job_id await queryClient.invalidateQueries({ queryKey: ['instances'] }) options.onInstalled?.() - await wait_for_install_job(appEvents, job.job_id).catch(() => {}) - } finally { + } catch (error) { setLoading(sharedInstanceId, false) + throw error } + void wait_for_install_job(appEvents, jobId) + .catch(() => {}) + .finally(() => setLoading(sharedInstanceId, false)) }, manager ? { id: manager.id, username: manager.username, avatarUrl: manager.avatar_url ?? null } diff --git a/apps/app-frontend/src/features/settings/system/resource-management-settings/app-directory/change-directory-modal.vue b/apps/app-frontend/src/features/settings/system/resource-management-settings/app-directory/change-directory-modal.vue new file mode 100644 index 00000000000..2dbae5984e9 --- /dev/null +++ b/apps/app-frontend/src/features/settings/system/resource-management-settings/app-directory/change-directory-modal.vue @@ -0,0 +1,228 @@ + + + diff --git a/apps/app-frontend/src/features/settings/system/resource-management-settings/app-directory/index.vue b/apps/app-frontend/src/features/settings/system/resource-management-settings/app-directory/index.vue new file mode 100644 index 00000000000..90f8096a493 --- /dev/null +++ b/apps/app-frontend/src/features/settings/system/resource-management-settings/app-directory/index.vue @@ -0,0 +1,198 @@ + + + diff --git a/apps/app-frontend/src/features/settings/system/resource-management-settings/app-directory/paths.ts b/apps/app-frontend/src/features/settings/system/resource-management-settings/app-directory/paths.ts new file mode 100644 index 00000000000..a6ac2be1684 --- /dev/null +++ b/apps/app-frontend/src/features/settings/system/resource-management-settings/app-directory/paths.ts @@ -0,0 +1,21 @@ +export function normalizePath(path: string, caseInsensitive: boolean): string { + const normalized = path.replace(/\\/g, '/').replace(/\/+$/, '') + return caseInsensitive ? normalized.toLowerCase() : normalized +} + +export function isNestedPath(first: string, second: string): boolean { + return first.startsWith(`${second}/`) || second.startsWith(`${first}/`) +} + +const DRIVE_ROOTS = [ + /^$/, + /^[a-z]:$/i, + /^\/Volumes\/[^/]+$/, + /^\/mnt\/[^/]+$/, + /^(\/run)?\/media\/[^/]+\/[^/]+$/, +] + +export function isDriveRoot(path: string): boolean { + const normalized = normalizePath(path, false) + return DRIVE_ROOTS.some((root) => root.test(normalized)) +} diff --git a/apps/app-frontend/src/features/settings/system/resource-management-settings/app-directory/use-app-directory.ts b/apps/app-frontend/src/features/settings/system/resource-management-settings/app-directory/use-app-directory.ts new file mode 100644 index 00000000000..0e46f1aa6a1 --- /dev/null +++ b/apps/app-frontend/src/features/settings/system/resource-management-settings/app-directory/use-app-directory.ts @@ -0,0 +1,81 @@ +import { injectNotificationManager } from '@modrinth/ui' +import { useMutation, useQuery, useQueryClient } from '@tanstack/vue-query' +import { computed, watch } from 'vue' + +import { + appSettingsKeys, + appSettingsQueryOptions, + cancel_directory_change, + get, + set, +} from '@/helpers/settings' +import { restartApp } from '@/helpers/utils.js' +import { getDefaultAppDirectory } from '@/platform/app-lib/settings/commands' + +export function useAppDirectory() { + const { handleError } = injectNotificationManager() + const queryClient = useQueryClient() + const settingsQuery = useQuery(appSettingsQueryOptions()) + const defaultDirQuery = useQuery({ + queryKey: ['app-directory', 'default'], + networkMode: 'always', + queryFn: getDefaultAppDirectory, + staleTime: Infinity, + }) + + watch([settingsQuery.error, defaultDirQuery.error], ([settingsError, defaultDirError]) => { + const error = settingsError ?? defaultDirError + if (error) handleError(error) + }) + + const defaultDir = computed(() => defaultDirQuery.data.value ?? null) + + const currentDir = computed(() => { + const settings = settingsQuery.data.value + return settings?.prev_custom_dir ?? settings?.custom_dir ?? null + }) + + const pendingDir = computed(() => { + const settings = settingsQuery.data.value + if (!settings?.prev_custom_dir || !settings.custom_dir) return null + return settings.custom_dir === settings.prev_custom_dir ? null : settings.custom_dir + }) + + const settingsMutationOptions = { + networkMode: 'always', + mutationKey: appSettingsKeys.update, + scope: { id: 'app-settings' }, + onMutate: () => queryClient.cancelQueries({ queryKey: appSettingsKeys.all }), + onError: handleError, + onSettled: () => queryClient.invalidateQueries({ queryKey: appSettingsKeys.all }), + } as const + + const changeMutation = useMutation({ + ...settingsMutationOptions, + mutationFn: async ({ directory, restartNow }: { directory: string; restartNow: boolean }) => { + await set({ ...(await get()), custom_dir: directory }) + if (restartNow) await restartApp() + }, + }) + + const cancelMutation = useMutation({ + ...settingsMutationOptions, + mutationFn: () => cancel_directory_change(), + }) + + const busy = computed(() => changeMutation.isPending.value || cancelMutation.isPending.value) + + function changeDirectory(directory: string, restartNow: boolean) { + changeMutation.mutate({ directory, restartNow }) + } + + function cancelMove() { + cancelMutation.mutate() + } + + function restartNow() { + restartApp().catch(handleError) + } + + return { currentDir, defaultDir, pendingDir, busy, changeDirectory, cancelMove, restartNow } +} diff --git a/apps/app-frontend/src/features/settings/system/resource-management-settings/app-directory/use-copy-path.ts b/apps/app-frontend/src/features/settings/system/resource-management-settings/app-directory/use-copy-path.ts new file mode 100644 index 00000000000..061a72e5d92 --- /dev/null +++ b/apps/app-frontend/src/features/settings/system/resource-management-settings/app-directory/use-copy-path.ts @@ -0,0 +1,38 @@ +import { defineMessages, injectNotificationManager, useVIntl } from '@modrinth/ui' + +const messages = defineMessages({ + copiedTitle: { + id: 'app.settings.resource-management.app-directory.path-copied-title', + defaultMessage: 'Path copied', + }, + copiedText: { + id: 'app.settings.resource-management.app-directory.path-copied-text', + defaultMessage: 'The app directory path has been copied to your clipboard.', + }, + copyFailedTitle: { + id: 'app.settings.resource-management.app-directory.path-copy-failed-title', + defaultMessage: 'Failed to copy path', + }, +}) + +export function useCopyPath() { + const { formatMessage } = useVIntl() + const { addNotification } = injectNotificationManager() + + return async function copyPath(path: string) { + try { + await navigator.clipboard.writeText(path) + addNotification({ + type: 'success', + title: formatMessage(messages.copiedTitle), + text: formatMessage(messages.copiedText), + }) + } catch (error) { + addNotification({ + type: 'error', + title: formatMessage(messages.copyFailedTitle), + text: error instanceof Error ? error.message : String(error), + }) + } + } +} diff --git a/apps/app-frontend/src/features/settings/system/resource-management-settings/index.vue b/apps/app-frontend/src/features/settings/system/resource-management-settings/index.vue index eb12af28eb1..ae1fefeed1a 100644 --- a/apps/app-frontend/src/features/settings/system/resource-management-settings/index.vue +++ b/apps/app-frontend/src/features/settings/system/resource-management-settings/index.vue @@ -1,34 +1,19 @@