diff --git a/Cargo.lock b/Cargo.lock index 028da5aaf8..ad23da4124 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1628,6 +1628,31 @@ dependencies = [ "serde_with", ] +[[package]] +name = "bon" +version = "3.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a602c73c7b0148ec6d12af6fd5cc7a46e2eacc8878271a999abac56eed12f561" +dependencies = [ + "bon-macros", + "rustversion", +] + +[[package]] +name = "bon-macros" +version = "3.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dee98b0db6a962de883bf5d20362dee4d7ca0d12fe39a7c6c73c844e1cd7c1f" +dependencies = [ + "darling 0.23.0", + "ident_case", + "prettyplease", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.106", +] + [[package]] name = "borsh" version = "1.5.7" @@ -10849,6 +10874,7 @@ dependencies = [ "async-walkdir", "async_zip", "base64 0.22.1", + "bon", "bytemuck", "bytes", "chardetng", diff --git a/Cargo.toml b/Cargo.toml index a7a6d055b3..301bed803f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -51,6 +51,7 @@ aws-sdk-s3 = { version = "=1.122.0", default-features = false, features = [ ] } base64 = "0.22.1" bitflags = "2.9.4" +bon = "3.9.3" bytemuck = "1.24.0" bytes = "1.10.1" censor = "0.3.0" diff --git a/apps/app-frontend/src/components/ui/instance_settings/InstallationSettings.vue b/apps/app-frontend/src/components/ui/instance_settings/InstallationSettings.vue index 6d908eab2b..fa1a4d0312 100644 --- a/apps/app-frontend/src/components/ui/instance_settings/InstallationSettings.vue +++ b/apps/app-frontend/src/components/ui/instance_settings/InstallationSettings.vue @@ -269,16 +269,31 @@ provideInstallationSettings({ debug('resolveLoaderVersions: no manifest', { loader, gameVersion }) return [] } - if (loader === 'fabric' || loader === 'quilt') { - const result = manifest.gameVersions[0]?.loaders ?? [] - debug('resolveLoaderVersions: fabric/quilt result', { + const entry = manifest.gameVersions?.find((item) => item.id === gameVersion) + if (entry?.versionGroup) { + const result = + manifest.versionGroups?.find((group) => group.id === entry.versionGroup)?.loaders ?? [] + debug('resolveLoaderVersions: version group result', { + loader, + gameVersion, + versionGroup: entry.versionGroup, + count: result.length, + }) + return result + } + const placeholder = manifest.gameVersions?.find((item) => item.id === '${modrinth.gameVersion}') + if (placeholder) { + const result = manifest.gameVersions?.some((item) => item.id === gameVersion) + ? placeholder.loaders + : [] + debug('resolveLoaderVersions: placeholder result', { loader, gameVersion, count: result.length, }) return result } - const result = manifest.gameVersions?.find((item) => item.id === gameVersion)?.loaders ?? [] + const result = entry?.loaders ?? [] debug('resolveLoaderVersions: result', { loader, gameVersion, count: result.length }) return result }, diff --git a/apps/app-frontend/src/composables/browse/install-job-notifications.ts b/apps/app-frontend/src/composables/browse/install-job-notifications.ts index 2346f77002..8d7eaa832d 100644 --- a/apps/app-frontend/src/composables/browse/install-job-notifications.ts +++ b/apps/app-frontend/src/composables/browse/install-job-notifications.ts @@ -1,4 +1,4 @@ -import { UpdatedIcon } from '@modrinth/assets' +import { CheckIcon, CopyIcon, UpdatedIcon } from '@modrinth/assets' import { defineMessages, type PopupNotificationButton, @@ -15,9 +15,11 @@ import { install_job_dismiss, install_job_list, install_job_retry, + install_job_support_details, installJobInstanceId, type InstallJobSnapshot, type InstallJobStatus, + type InstallPhaseId, type InstallProgress, } from '@/helpers/install' import { get_many as getInstances } from '@/helpers/instance' @@ -31,6 +33,14 @@ const messages = defineMessages({ id: 'app.action-bar.install.retry', defaultMessage: 'Retry', }, + copyDetails: { + id: 'app.action-bar.install.copy-details', + defaultMessage: 'Copy details', + }, + copied: { + id: 'app.action-bar.install.copied-details', + defaultMessage: 'Copied', + }, dismiss: { id: 'app.action-bar.install.dismiss', defaultMessage: 'Dismiss', @@ -39,22 +49,6 @@ const messages = defineMessages({ id: 'app.action-bar.install.open-instance', defaultMessage: 'Open instance', }, - installFailed: { - id: 'app.action-bar.install.failed', - defaultMessage: 'Install failed', - }, - installFailedAppClosed: { - id: 'app.action-bar.install.failed-app-closed', - defaultMessage: 'Installation failed due to app closing.', - }, - installFailedNetwork: { - id: 'app.action-bar.install.failed-network', - defaultMessage: 'Installation failed due to a network error.', - }, - installFailedUnknown: { - id: 'app.action-bar.install.failed-unknown', - defaultMessage: 'Installation failed due to an unknown error.', - }, unknownInstance: { id: 'app.action-bar.install.unknown-instance', defaultMessage: 'Unknown instance', @@ -64,7 +58,7 @@ const messages = defineMessages({ const phaseMessages = defineMessages({ preparing_instance: { id: 'app.install.phase.preparing_instance', - defaultMessage: 'Preparing instance', + defaultMessage: 'Queued to install', }, resolving_pack: { id: 'app.install.phase.resolving_pack', @@ -139,7 +133,97 @@ const javaStepMessages = defineMessages({ }, }) +const failureSummaryMessages = defineMessages({ + canceled: { + id: 'app.action-bar.install.summary.canceled', + defaultMessage: 'Canceled', + }, + appClosed: { + id: 'app.action-bar.install.summary.app-closing', + defaultMessage: 'Canceled due to app closing', + }, + downloadFailed: { + id: 'app.action-bar.install.summary.download-failed', + defaultMessage: "Download couldn't finish", + }, + modrinthUnreachable: { + id: 'app.action-bar.install.summary.modrinth-unreachable', + defaultMessage: "Couldn't reach Modrinth", + }, + packDownloadFailed: { + id: 'app.action-bar.install.summary.pack-download-failed', + defaultMessage: "Couldn't download pack", + }, + badModpackFile: { + id: 'app.action-bar.install.summary.bad-modpack-file', + defaultMessage: "Couldn't read modpack", + }, + invalidModpack: { + id: 'app.action-bar.install.summary.invalid-modpack', + defaultMessage: 'Modpack data invalid', + }, + contentDownloadFailed: { + id: 'app.action-bar.install.summary.content-download-failed', + defaultMessage: "Couldn't download files", + }, + corruptDownload: { + id: 'app.action-bar.install.summary.corrupt-download', + defaultMessage: 'Downloaded file is corrupt', + }, + invalidModpackFiles: { + id: 'app.action-bar.install.summary.invalid-modpack-files', + defaultMessage: 'Modpack files have invalid metadata', + }, + noWritePermission: { + id: 'app.action-bar.install.summary.no-write-permission', + defaultMessage: 'No permission to write', + }, + couldNotSaveFiles: { + id: 'app.action-bar.install.summary.could-not-save-files', + defaultMessage: "Couldn't save files", + }, + invalidFilePath: { + id: 'app.action-bar.install.summary.invalid-file-path', + defaultMessage: 'File path is invalid', + }, + instanceNotFound: { + id: 'app.action-bar.install.summary.instance-not-found', + defaultMessage: "Instance couldn't be found", + }, + cleanupIncomplete: { + id: 'app.action-bar.install.summary.cleanup-incomplete', + defaultMessage: "Cleanup didn't finish", + }, + javaSetupFailed: { + id: 'app.action-bar.install.summary.java-setup-failed', + defaultMessage: "Java setup couldn't finish", + }, + minecraftSetupFailed: { + id: 'app.action-bar.install.summary.minecraft-setup-failed', + defaultMessage: 'Minecraft setup failed', + }, + loaderSetupFailed: { + id: 'app.action-bar.install.summary.loader-setup-failed', + defaultMessage: 'Loader setup failed', + }, + localDataError: { + id: 'app.action-bar.install.summary.local-data-error', + defaultMessage: "Couldn't update local data", + }, + unexpectedError: { + id: 'app.action-bar.install.summary.unexpected-error', + defaultMessage: 'Something went wrong', + }, +}) + const visibleJobStatuses = new Set(['queued', 'running', 'failed', 'interrupted']) +const copyDetailsStallMs = 30_000 + +interface ProgressSnapshot { + signature: string + changedAt: number + timeout: number | null +} function getDisplayIconUrl(icon: string | null | undefined): string | null { if (!icon) return null @@ -156,10 +240,13 @@ export async function useInstallJobNotifications(opts: { const jobs = ref([]) const iconUrls = ref>({}) const instanceNames = ref>({}) + const copiedJobIds = ref>(new Set()) const jobOrder = new Map() let refreshRequest = 0 let metadataRequest = 0 let nextJobOrder = 0 + const copiedResetTimeouts = new Map() + const progressSnapshots = new Map() function getTitle(job: InstallJobSnapshot): string { if (job.display?.title) return job.display.title @@ -174,13 +261,7 @@ export async function useInstallJobNotifications(opts: { function getText(job: InstallJobSnapshot): string { if (job.status === 'failed' || job.status === 'interrupted') { - if (job.error?.code === 'interrupted') { - return formatMessage(messages.installFailedAppClosed) - } - if (job.error?.code === 'network_error') { - return formatMessage(messages.installFailedNetwork) - } - return formatMessage(messages.installFailedUnknown) + return getFailureSummary(job) } if (job.phase === 'preparing_java' && job.details.type === 'java') { return formatMessage(javaStepMessages[job.details.step], { @@ -190,6 +271,104 @@ export async function useInstallJobNotifications(opts: { return formatMessage(phaseMessages[job.phase]) } + function getFailureSummary(job: InstallJobSnapshot): string { + const code = job.error?.code + const phase = job.error?.phase ?? job.phase + + if (code === 'app_closed' || (job.status === 'interrupted' && code === 'interrupted')) { + return formatMessage(failureSummaryMessages.appClosed) + } + if (code === 'canceled') { + return formatMessage(failureSummaryMessages.canceled) + } + if (job.rollback_error || code === 'rollback_error') { + return formatMessage(failureSummaryMessages.cleanupIncomplete) + } + if (hasPermissionError(job)) { + return formatMessage(failureSummaryMessages.noWritePermission) + } + + switch (code) { + case 'network_error': + return formatMessage( + phase === 'downloading_pack_file' + ? failureSummaryMessages.packDownloadFailed + : failureSummaryMessages.downloadFailed, + ) + case 'api_error': + return formatMessage(failureSummaryMessages.modrinthUnreachable) + case 'pack_error': + return formatMessage( + phase === 'downloading_pack_file' + ? failureSummaryMessages.packDownloadFailed + : failureSummaryMessages.invalidModpack, + ) + case 'archive_error': + return formatMessage(failureSummaryMessages.badModpackFile) + case 'parse_error': + return formatMessage(failureSummaryMessages.invalidModpack) + case 'content_error': + return formatMessage(failureSummaryMessages.invalidModpackFiles) + case 'hash_error': + return formatMessage(failureSummaryMessages.corruptDownload) + case 'filesystem_error': + return formatMessage(failureSummaryMessages.couldNotSaveFiles) + case 'path_error': + return formatMessage(failureSummaryMessages.invalidFilePath) + case 'instance_error': + return formatMessage(failureSummaryMessages.instanceNotFound) + case 'java_error': + return formatMessage(failureSummaryMessages.javaSetupFailed) + case 'loader_error': + case 'processor_error': + return formatMessage(failureSummaryMessages.loaderSetupFailed) + case 'database_error': + return formatMessage(failureSummaryMessages.localDataError) + case 'launcher_error': + case 'metadata_error': + return getFailureSummaryForPhase(phase) + default: + return getFailureSummaryForPhase(phase) + } + } + + function getFailureSummaryForPhase(phase: InstallPhaseId): string { + switch (phase) { + case 'downloading_pack_file': + return formatMessage(failureSummaryMessages.packDownloadFailed) + case 'resolving_pack': + case 'reading_pack_manifest': + return formatMessage(failureSummaryMessages.invalidModpack) + case 'downloading_content': + return formatMessage(failureSummaryMessages.contentDownloadFailed) + case 'extracting_overrides': + return formatMessage(failureSummaryMessages.couldNotSaveFiles) + case 'resolving_minecraft': + case 'downloading_minecraft': + return formatMessage(failureSummaryMessages.minecraftSetupFailed) + case 'resolving_loader': + case 'running_loader_processors': + return formatMessage(failureSummaryMessages.loaderSetupFailed) + case 'preparing_java': + return formatMessage(failureSummaryMessages.javaSetupFailed) + case 'preparing_instance': + return formatMessage(failureSummaryMessages.instanceNotFound) + case 'rolling_back': + return formatMessage(failureSummaryMessages.cleanupIncomplete) + default: + return formatMessage(failureSummaryMessages.unexpectedError) + } + } + + function hasPermissionError(job: InstallJobSnapshot): boolean { + const message = job.error?.message.toLowerCase() ?? '' + return ( + message.includes('permission denied') || + message.includes('access is denied') || + message.includes('operation not permitted') + ) + } + function getProgressType(job: InstallJobSnapshot): PopupNotificationProgressType | undefined { if (!getEffectiveProgress(job)) return undefined if ( @@ -235,11 +414,165 @@ export async function useInstallJobNotifications(opts: { return job.status === 'failed' || job.status === 'interrupted' } - function getTerminalButtons(job: InstallJobSnapshot): PopupNotificationButton[] | undefined { - if (!isTerminalJob(job)) return undefined + function canShowStalledProgressDetails(job: InstallJobSnapshot): boolean { + return ( + job.status === 'running' && + job.phase !== 'preparing_instance' && + job.phase !== 'finalizing' && + job.phase !== 'rolling_back' + ) + } + function getJobSortRank(job: InstallJobSnapshot): number { + if (isTerminalJob(job)) return 0 + if (job.status === 'queued' || job.phase === 'preparing_instance') return 2 + return 1 + } + + function progressSignature(job: InstallJobSnapshot): string { + const progress = job.progress + const secondary = progress?.secondary return [ - { + job.status, + job.phase, + JSON.stringify(job.details), + progress?.current ?? '', + progress?.total ?? '', + secondary?.current ?? '', + secondary?.total ?? '', + ].join(':') + } + + function clearCopied(jobId: string) { + if (!copiedJobIds.value.has(jobId)) { + return + } + + const timeout = copiedResetTimeouts.get(jobId) + if (timeout != null) { + window.clearTimeout(timeout) + copiedResetTimeouts.delete(jobId) + } + + const nextCopiedJobIds = new Set(copiedJobIds.value) + nextCopiedJobIds.delete(jobId) + copiedJobIds.value = nextCopiedJobIds + } + + function clearProgressSnapshot(jobId: string) { + const snapshot = progressSnapshots.get(jobId) + if (snapshot?.timeout != null) { + window.clearTimeout(snapshot.timeout) + } + progressSnapshots.delete(jobId) + } + + function scheduleStaleProgressRefresh(jobId: string) { + const snapshot = progressSnapshots.get(jobId) + if (!snapshot) { + return + } + + snapshot.timeout = window.setTimeout(() => { + const snapshot = progressSnapshots.get(jobId) + if (!snapshot) { + return + } + + snapshot.timeout = null + opts.onChange() + }, copyDetailsStallMs) + } + + function syncProgressSnapshots(nextJobs: InstallJobSnapshot[]) { + const trackedJobIds = new Set() + const now = Date.now() + + for (const job of nextJobs) { + if (!canShowStalledProgressDetails(job)) { + continue + } + + trackedJobIds.add(job.job_id) + const signature = progressSignature(job) + const snapshot = progressSnapshots.get(job.job_id) + if (snapshot?.signature === signature) { + continue + } + + clearProgressSnapshot(job.job_id) + clearCopied(job.job_id) + progressSnapshots.set(job.job_id, { + signature, + changedAt: now, + timeout: null, + }) + scheduleStaleProgressRefresh(job.job_id) + } + + for (const jobId of progressSnapshots.keys()) { + if (!trackedJobIds.has(jobId)) { + clearProgressSnapshot(jobId) + } + } + } + + function hasStalledProgress(job: InstallJobSnapshot): boolean { + const snapshot = progressSnapshots.get(job.job_id) + return !!snapshot && Date.now() - snapshot.changedAt >= copyDetailsStallMs + } + + function shouldShowCopyDetails(job: InstallJobSnapshot): boolean { + return isTerminalJob(job) || (canShowStalledProgressDetails(job) && hasStalledProgress(job)) + } + + function isCopied(job: InstallJobSnapshot): boolean { + return copiedJobIds.value.has(job.job_id) + } + + function setCopied(job: InstallJobSnapshot) { + copiedJobIds.value = new Set([...copiedJobIds.value, job.job_id]) + const existingTimeout = copiedResetTimeouts.get(job.job_id) + if (existingTimeout != null) { + window.clearTimeout(existingTimeout) + } + copiedResetTimeouts.set( + job.job_id, + window.setTimeout(() => { + copiedResetTimeouts.delete(job.job_id) + if (!copiedJobIds.value.has(job.job_id)) { + return + } + const nextCopiedJobIds = new Set(copiedJobIds.value) + nextCopiedJobIds.delete(job.job_id) + copiedJobIds.value = nextCopiedJobIds + opts.onChange() + }, 1_000), + ) + opts.onChange() + } + + async function copyJobDetails(job: InstallJobSnapshot) { + const details = await install_job_support_details(job.job_id).catch((error) => { + opts.handleError(error) + return null + }) + if (!details) { + return + } + try { + await navigator.clipboard.writeText(details) + setCopied(job) + } catch (error) { + opts.handleError(error) + } + } + + function getButtons(job: InstallJobSnapshot): PopupNotificationButton[] { + const buttons: PopupNotificationButton[] = [] + + if (isTerminalJob(job)) { + buttons.push({ label: formatMessage(messages.retry), icon: UpdatedIcon, color: 'brand', @@ -248,8 +581,23 @@ export async function useInstallJobNotifications(opts: { await install_job_retry(job.job_id).catch(opts.handleError) await refresh() }, - }, - ] + }) + } + + if (shouldShowCopyDetails(job)) { + const copied = isCopied(job) + buttons.push({ + label: formatMessage(copied ? messages.copied : messages.copyDetails), + icon: copied ? CheckIcon : CopyIcon, + color: 'standard', + keepOpen: true, + action: async () => { + await copyJobDetails(job) + }, + }) + } + + return buttons } function setJobs(nextJobs: InstallJobSnapshot[]) { @@ -259,13 +607,15 @@ export async function useInstallJobNotifications(opts: { } } - jobs.value = nextJobs - .filter((job) => visibleJobStatuses.has(job.status)) - .sort( - (a, b) => - a.created.localeCompare(b.created) || - (jobOrder.get(a.job_id) ?? 0) - (jobOrder.get(b.job_id) ?? 0), - ) + const visibleJobs = nextJobs.filter((job) => visibleJobStatuses.has(job.status)) + syncProgressSnapshots(visibleJobs) + + jobs.value = visibleJobs.sort( + (a, b) => + getJobSortRank(a) - getJobSortRank(b) || + a.created.localeCompare(b.created) || + (jobOrder.get(a.job_id) ?? 0) - (jobOrder.get(b.job_id) ?? 0), + ) } const progressItems = computed(() => @@ -284,7 +634,7 @@ export async function useInstallJobNotifications(opts: { progressType: isTerminalJob(job) ? undefined : getProgressType(job), progressCurrent: isTerminalJob(job) ? undefined : progress?.current, progressTotal: isTerminalJob(job) ? undefined : progress?.total, - buttons: getTerminalButtons(job), + buttons: getButtons(job), onDismiss: isTerminalJob(job) ? async () => { await install_job_dismiss(job.job_id).catch(opts.handleError) @@ -382,6 +732,14 @@ export async function useInstallJobNotifications(opts: { progressItems, buttons, refresh, - dispose: () => unlisten(), + dispose: () => { + for (const timeout of copiedResetTimeouts.values()) { + window.clearTimeout(timeout) + } + for (const jobId of progressSnapshots.keys()) { + clearProgressSnapshot(jobId) + } + unlisten() + }, } } diff --git a/apps/app-frontend/src/helpers/install.ts b/apps/app-frontend/src/helpers/install.ts index 20ca0810a0..76fc45cfa4 100644 --- a/apps/app-frontend/src/helpers/install.ts +++ b/apps/app-frontend/src/helpers/install.ts @@ -85,6 +85,36 @@ export type InstallJavaStep = | 'extracting' | 'validating' +export interface InstallErrorView { + code: string + phase?: InstallPhaseId | null + message: string + api?: { + error: string + status?: number | null + method?: string | null + url?: string | null + route?: string | null + } | null + context?: { + operation: string + source_path?: string | null + target_path?: string | null + file_path?: string | null + entry_path?: string | null + urls?: string[] + expected_hash?: string | null + expected_size?: number | null + project_id?: string | null + version_id?: string | null + minecraft_version?: string | null + loader?: string | null + java_version?: number | null + os?: string | null + arch?: string | null + } | null +} + export interface InstallJobSnapshot { job_id: string instance_id?: string | null @@ -114,7 +144,8 @@ export interface InstallJobSnapshot { } | { type: 'import'; launcher_type: string; instance_folder: string } display?: { title: string; icon?: string | null } | null - error?: { code: string; message: string } | null + error?: InstallErrorView | null + rollback_error?: InstallErrorView | null created: string modified: string finished?: string | null @@ -197,6 +228,10 @@ export async function install_job_dismiss(jobId: string) { return await invoke('plugin:install|install_job_dismiss', { jobId }) } +export async function install_job_support_details(jobId: string) { + return await invoke('plugin:install|install_job_support_details', { jobId }) +} + export function installJobInstanceId(job: InstallJobSnapshot): string | null { return job.instance_id ?? job.target.instance_id ?? null } diff --git a/apps/app-frontend/src/helpers/types.d.ts b/apps/app-frontend/src/helpers/types.d.ts index 0bd0101461..761226eb91 100644 --- a/apps/app-frontend/src/helpers/types.d.ts +++ b/apps/app-frontend/src/helpers/types.d.ts @@ -133,11 +133,18 @@ type Hooks = { type Manifest = { gameVersions: ManifestGameVersion[] + versionGroups?: ManifestVersionGroup[] } type ManifestGameVersion = { id: string stable: boolean + versionGroup?: string + loaders: ManifestLoaderVersion[] +} + +type ManifestVersionGroup = { + id: string loaders: ManifestLoaderVersion[] } diff --git a/apps/app-frontend/src/locales/cs-CZ/index.json b/apps/app-frontend/src/locales/cs-CZ/index.json index 3996f59835..b5b00a972a 100644 --- a/apps/app-frontend/src/locales/cs-CZ/index.json +++ b/apps/app-frontend/src/locales/cs-CZ/index.json @@ -14,18 +14,6 @@ "app.action-bar.install.dismiss": { "message": "Zavřít" }, - "app.action-bar.install.failed": { - "message": "Instalace selhala" - }, - "app.action-bar.install.failed-app-closed": { - "message": "Instalace selhala kvůli zavření aplikace." - }, - "app.action-bar.install.failed-network": { - "message": "Instalace selhala kvůli chybě sítě." - }, - "app.action-bar.install.failed-unknown": { - "message": "Instalace selhala kvůli neznámé chybě." - }, "app.action-bar.install.open-instance": { "message": "Otevřít instanci" }, diff --git a/apps/app-frontend/src/locales/de-CH/index.json b/apps/app-frontend/src/locales/de-CH/index.json index 7b0b52ac5e..1d02989b17 100644 --- a/apps/app-frontend/src/locales/de-CH/index.json +++ b/apps/app-frontend/src/locales/de-CH/index.json @@ -14,18 +14,6 @@ "app.action-bar.install.dismiss": { "message": "Ignorieren" }, - "app.action-bar.install.failed": { - "message": "Installation fehlgeschlagen" - }, - "app.action-bar.install.failed-app-closed": { - "message": "Installation aufgrund des Schließens der App fehlgeschlagen." - }, - "app.action-bar.install.failed-network": { - "message": "Installation aufgrund eines Netzwerkfehlers fehlgeschlagen." - }, - "app.action-bar.install.failed-unknown": { - "message": "Installation aufgrund eines unbekannten Fehlers fehlgeschlagen." - }, "app.action-bar.install.open-instance": { "message": "Instanz öffnen" }, diff --git a/apps/app-frontend/src/locales/de-DE/index.json b/apps/app-frontend/src/locales/de-DE/index.json index ccbbe77b5f..1711ab355a 100644 --- a/apps/app-frontend/src/locales/de-DE/index.json +++ b/apps/app-frontend/src/locales/de-DE/index.json @@ -14,18 +14,6 @@ "app.action-bar.install.dismiss": { "message": "Ausblenden" }, - "app.action-bar.install.failed": { - "message": "Installation fehlgeschlagen" - }, - "app.action-bar.install.failed-app-closed": { - "message": "Installation aufgrund des Schließens der App fehlgeschlagen." - }, - "app.action-bar.install.failed-network": { - "message": "Installation aufgrund eines Netzwerkfehlers fehlgeschlagen." - }, - "app.action-bar.install.failed-unknown": { - "message": "Installation aufgrund eines unbekannten Fehlers fehlgeschlagen." - }, "app.action-bar.install.open-instance": { "message": "Instanz öffnen" }, diff --git a/apps/app-frontend/src/locales/en-US/index.json b/apps/app-frontend/src/locales/en-US/index.json index fea079df51..b6a092b1d4 100644 --- a/apps/app-frontend/src/locales/en-US/index.json +++ b/apps/app-frontend/src/locales/en-US/index.json @@ -11,27 +11,81 @@ "app.action-bar.hide-more-running-instances": { "message": "Hide more running instances" }, + "app.action-bar.install.copied-details": { + "message": "Copied" + }, + "app.action-bar.install.copy-details": { + "message": "Copy details" + }, "app.action-bar.install.dismiss": { "message": "Dismiss" }, - "app.action-bar.install.failed": { - "message": "Install failed" - }, - "app.action-bar.install.failed-app-closed": { - "message": "Installation failed due to app closing." - }, - "app.action-bar.install.failed-network": { - "message": "Installation failed due to a network error." - }, - "app.action-bar.install.failed-unknown": { - "message": "Installation failed due to an unknown error." - }, "app.action-bar.install.open-instance": { "message": "Open instance" }, "app.action-bar.install.retry": { "message": "Retry" }, + "app.action-bar.install.summary.app-closing": { + "message": "Canceled due to app closing" + }, + "app.action-bar.install.summary.bad-modpack-file": { + "message": "Couldn't read modpack" + }, + "app.action-bar.install.summary.canceled": { + "message": "Canceled" + }, + "app.action-bar.install.summary.cleanup-incomplete": { + "message": "Cleanup didn't finish" + }, + "app.action-bar.install.summary.content-download-failed": { + "message": "Couldn't download files" + }, + "app.action-bar.install.summary.corrupt-download": { + "message": "Downloaded file is corrupt" + }, + "app.action-bar.install.summary.could-not-save-files": { + "message": "Couldn't save files" + }, + "app.action-bar.install.summary.download-failed": { + "message": "Download couldn't finish" + }, + "app.action-bar.install.summary.instance-not-found": { + "message": "Instance couldn't be found" + }, + "app.action-bar.install.summary.invalid-file-path": { + "message": "File path is invalid" + }, + "app.action-bar.install.summary.invalid-modpack": { + "message": "Modpack data invalid" + }, + "app.action-bar.install.summary.invalid-modpack-files": { + "message": "Modpack files have invalid metadata" + }, + "app.action-bar.install.summary.java-setup-failed": { + "message": "Java setup couldn't finish" + }, + "app.action-bar.install.summary.loader-setup-failed": { + "message": "Loader setup failed" + }, + "app.action-bar.install.summary.local-data-error": { + "message": "Couldn't update local data" + }, + "app.action-bar.install.summary.minecraft-setup-failed": { + "message": "Minecraft setup failed" + }, + "app.action-bar.install.summary.modrinth-unreachable": { + "message": "Couldn't reach Modrinth" + }, + "app.action-bar.install.summary.no-write-permission": { + "message": "No permission to write" + }, + "app.action-bar.install.summary.pack-download-failed": { + "message": "Couldn't download pack" + }, + "app.action-bar.install.summary.unexpected-error": { + "message": "Something went wrong" + }, "app.action-bar.install.unknown-instance": { "message": "Unknown instance" }, @@ -231,7 +285,7 @@ "message": "Finalizing" }, "app.install.phase.preparing_instance": { - "message": "Preparing instance" + "message": "Queued to install" }, "app.install.phase.preparing_java": { "message": "Preparing Java" diff --git a/apps/app-frontend/src/locales/es-419/index.json b/apps/app-frontend/src/locales/es-419/index.json index 8b3bfc865c..af39db4297 100644 --- a/apps/app-frontend/src/locales/es-419/index.json +++ b/apps/app-frontend/src/locales/es-419/index.json @@ -14,18 +14,6 @@ "app.action-bar.install.dismiss": { "message": "Descartar" }, - "app.action-bar.install.failed": { - "message": "La instalación falló" - }, - "app.action-bar.install.failed-app-closed": { - "message": "La instalación falló porque la aplicación se cerró." - }, - "app.action-bar.install.failed-network": { - "message": "La instalación falló debido a un error de red." - }, - "app.action-bar.install.failed-unknown": { - "message": "La instalación falló debido a un error desconocido." - }, "app.action-bar.install.open-instance": { "message": "Abrir instancia" }, diff --git a/apps/app-frontend/src/locales/es-ES/index.json b/apps/app-frontend/src/locales/es-ES/index.json index 863c85f0fd..a1ff15b789 100644 --- a/apps/app-frontend/src/locales/es-ES/index.json +++ b/apps/app-frontend/src/locales/es-ES/index.json @@ -14,15 +14,6 @@ "app.action-bar.install.dismiss": { "message": "Descartar" }, - "app.action-bar.install.failed": { - "message": "La instalación falló" - }, - "app.action-bar.install.failed-app-closed": { - "message": "La instalación falló porque la aplicación se cerró." - }, - "app.action-bar.install.failed-network": { - "message": "La instalación falló debido a un error de red." - }, "app.action-bar.make-primary-instance": { "message": "Establecer como instancia principal" }, diff --git a/apps/app-frontend/src/locales/fi-FI/index.json b/apps/app-frontend/src/locales/fi-FI/index.json index 5f5020e903..dc3439adff 100644 --- a/apps/app-frontend/src/locales/fi-FI/index.json +++ b/apps/app-frontend/src/locales/fi-FI/index.json @@ -14,18 +14,6 @@ "app.action-bar.install.dismiss": { "message": "Hylkää" }, - "app.action-bar.install.failed": { - "message": "Asennus epäonnistui" - }, - "app.action-bar.install.failed-app-closed": { - "message": "Asennus epäonnistui sovelluksen sulkeutumisen vuoksi." - }, - "app.action-bar.install.failed-network": { - "message": "Asennus epäonnistui verkkovirheen vuoksi." - }, - "app.action-bar.install.failed-unknown": { - "message": "Asennus epäonnistui tuntemattoman virheen vuoksi." - }, "app.action-bar.install.open-instance": { "message": "Avaa instanssi" }, diff --git a/apps/app-frontend/src/locales/fr-FR/index.json b/apps/app-frontend/src/locales/fr-FR/index.json index 9461752870..11d89bcac8 100644 --- a/apps/app-frontend/src/locales/fr-FR/index.json +++ b/apps/app-frontend/src/locales/fr-FR/index.json @@ -14,18 +14,6 @@ "app.action-bar.install.dismiss": { "message": "Rejeter" }, - "app.action-bar.install.failed": { - "message": "Échec de l'installation" - }, - "app.action-bar.install.failed-app-closed": { - "message": "Échec de l'installation dû à la fermeture de l'app." - }, - "app.action-bar.install.failed-network": { - "message": "Échec de l'installation dû à une erreur réseau." - }, - "app.action-bar.install.failed-unknown": { - "message": "Échec de l'installation dû à une erreur inconnue." - }, "app.action-bar.install.open-instance": { "message": "Ouvrir l'instance" }, diff --git a/apps/app-frontend/src/locales/it-IT/index.json b/apps/app-frontend/src/locales/it-IT/index.json index 28adfc3dba..43021b2167 100644 --- a/apps/app-frontend/src/locales/it-IT/index.json +++ b/apps/app-frontend/src/locales/it-IT/index.json @@ -14,18 +14,6 @@ "app.action-bar.install.dismiss": { "message": "Ignora" }, - "app.action-bar.install.failed": { - "message": "Installazione fallita" - }, - "app.action-bar.install.failed-app-closed": { - "message": "Installazione fallita: l'app è stata chiusa." - }, - "app.action-bar.install.failed-network": { - "message": "Installazione fallita: errore di rete." - }, - "app.action-bar.install.failed-unknown": { - "message": "Installazione fallita: errore sconosciuto." - }, "app.action-bar.install.open-instance": { "message": "Apri istanza" }, diff --git a/apps/app-frontend/src/locales/pl-PL/index.json b/apps/app-frontend/src/locales/pl-PL/index.json index 0a52fa2be7..24d7824683 100644 --- a/apps/app-frontend/src/locales/pl-PL/index.json +++ b/apps/app-frontend/src/locales/pl-PL/index.json @@ -14,18 +14,6 @@ "app.action-bar.install.dismiss": { "message": "Odrzuć" }, - "app.action-bar.install.failed": { - "message": "Instalacja nie powiodła się" - }, - "app.action-bar.install.failed-app-closed": { - "message": "Instalacja nie powiodła się z powodu zamknięcia aplikacji." - }, - "app.action-bar.install.failed-network": { - "message": "Instalacja nie powiodła się z powodu problemów z połączeniem." - }, - "app.action-bar.install.failed-unknown": { - "message": "Instalacja nie powiodła się z nieznanych przyczyn." - }, "app.action-bar.install.open-instance": { "message": "Otwórz instancję" }, diff --git a/apps/app-frontend/src/locales/pt-BR/index.json b/apps/app-frontend/src/locales/pt-BR/index.json index 4ca721a2f6..c1ea5612fc 100644 --- a/apps/app-frontend/src/locales/pt-BR/index.json +++ b/apps/app-frontend/src/locales/pt-BR/index.json @@ -14,18 +14,6 @@ "app.action-bar.install.dismiss": { "message": "Dispensar" }, - "app.action-bar.install.failed": { - "message": "Instalação falha" - }, - "app.action-bar.install.failed-app-closed": { - "message": "A instalação falhou devido ao aplicativo ter fechado." - }, - "app.action-bar.install.failed-network": { - "message": "A instalação falhou devido a um erro na rede." - }, - "app.action-bar.install.failed-unknown": { - "message": "A instalação falhou devido a um erro desconhecido." - }, "app.action-bar.install.open-instance": { "message": "Abrir instância" }, diff --git a/apps/app-frontend/src/locales/ru-RU/index.json b/apps/app-frontend/src/locales/ru-RU/index.json index a722d690ee..8228ca52c5 100644 --- a/apps/app-frontend/src/locales/ru-RU/index.json +++ b/apps/app-frontend/src/locales/ru-RU/index.json @@ -14,18 +14,6 @@ "app.action-bar.install.dismiss": { "message": "Закрыть" }, - "app.action-bar.install.failed": { - "message": "Установка не удалась" - }, - "app.action-bar.install.failed-app-closed": { - "message": "Установка не удалась из-за закрытия приложения." - }, - "app.action-bar.install.failed-network": { - "message": "Установка не удалась из-за сетевой ошибки." - }, - "app.action-bar.install.failed-unknown": { - "message": "Установка не удалась из-за неизвестной ошибки." - }, "app.action-bar.install.open-instance": { "message": "Открыть сборку" }, diff --git a/apps/app-frontend/src/locales/sr-CS/index.json b/apps/app-frontend/src/locales/sr-CS/index.json index 69d8fcad39..a3a9c65270 100644 --- a/apps/app-frontend/src/locales/sr-CS/index.json +++ b/apps/app-frontend/src/locales/sr-CS/index.json @@ -1,10 +1,4 @@ { - "app.action-bar.install.failed": { - "message": "Neuspešna instalacija" - }, - "app.action-bar.install.failed-app-closed": { - "message": "Neuspešna instalacija zbog zatvaranja aplikacije." - }, "app.auth-servers.unreachable.body": { "message": "Minecraft serveri za autentifikaciju su možda trenutno nedostupni. Molimo vas da proverite vašu internet vezu i pokušajte ponovo kasnije." }, diff --git a/apps/app-frontend/src/locales/sv-SE/index.json b/apps/app-frontend/src/locales/sv-SE/index.json index 058ea2ba20..49a795670c 100644 --- a/apps/app-frontend/src/locales/sv-SE/index.json +++ b/apps/app-frontend/src/locales/sv-SE/index.json @@ -14,18 +14,6 @@ "app.action-bar.install.dismiss": { "message": "Avvisa" }, - "app.action-bar.install.failed": { - "message": "Installationen misslyckades" - }, - "app.action-bar.install.failed-app-closed": { - "message": "Installationen misslyckades för att appen stängdes." - }, - "app.action-bar.install.failed-network": { - "message": "Installationen misslyckades av ett nätverksfel." - }, - "app.action-bar.install.failed-unknown": { - "message": "Installationen misslyckades med ett okänt fel." - }, "app.action-bar.install.open-instance": { "message": "Öppna instans" }, diff --git a/apps/app-frontend/src/locales/tr-TR/index.json b/apps/app-frontend/src/locales/tr-TR/index.json index e39707996b..48e0b40a24 100644 --- a/apps/app-frontend/src/locales/tr-TR/index.json +++ b/apps/app-frontend/src/locales/tr-TR/index.json @@ -14,18 +14,6 @@ "app.action-bar.install.dismiss": { "message": "Reddet" }, - "app.action-bar.install.failed": { - "message": "Yükleme yapılamadı" - }, - "app.action-bar.install.failed-app-closed": { - "message": "Yükleme uygulama kapandığından yapılamadı." - }, - "app.action-bar.install.failed-network": { - "message": "Yükleme ağ hatasından dolayı yapılamadı." - }, - "app.action-bar.install.failed-unknown": { - "message": "Yükleme bilinmeyen bir hatadan yapılamadı." - }, "app.action-bar.install.open-instance": { "message": "Kurulumu aç" }, diff --git a/apps/app-frontend/src/locales/zh-CN/index.json b/apps/app-frontend/src/locales/zh-CN/index.json index 73c6437d42..c9965726fe 100644 --- a/apps/app-frontend/src/locales/zh-CN/index.json +++ b/apps/app-frontend/src/locales/zh-CN/index.json @@ -14,18 +14,6 @@ "app.action-bar.install.dismiss": { "message": "忽略" }, - "app.action-bar.install.failed": { - "message": "安装失败" - }, - "app.action-bar.install.failed-app-closed": { - "message": "由于应用关闭,安装失败。" - }, - "app.action-bar.install.failed-network": { - "message": "由于网络错误,安装失败。" - }, - "app.action-bar.install.failed-unknown": { - "message": "由于未知错误,安装失败。" - }, "app.action-bar.install.open-instance": { "message": "打开实例" }, diff --git a/apps/app-frontend/src/locales/zh-TW/index.json b/apps/app-frontend/src/locales/zh-TW/index.json index 10b15aca8f..f0e5896246 100644 --- a/apps/app-frontend/src/locales/zh-TW/index.json +++ b/apps/app-frontend/src/locales/zh-TW/index.json @@ -14,18 +14,6 @@ "app.action-bar.install.dismiss": { "message": "忽略" }, - "app.action-bar.install.failed": { - "message": "無法安裝" - }, - "app.action-bar.install.failed-app-closed": { - "message": "由於應用程式關閉,無法安裝。" - }, - "app.action-bar.install.failed-network": { - "message": "由於網路錯誤,無法安裝。" - }, - "app.action-bar.install.failed-unknown": { - "message": "由於未知錯誤,無法安裝。" - }, "app.action-bar.install.open-instance": { "message": "開啟實例" }, diff --git a/apps/app/build.rs b/apps/app/build.rs index 3bc241475d..8a8fe00c79 100644 --- a/apps/app/build.rs +++ b/apps/app/build.rs @@ -157,6 +157,7 @@ fn main() { "install_job_retry", "install_job_cancel", "install_job_dismiss", + "install_job_support_details", ]) .default_permission( DefaultPermissionRule::AllowAllCommands, diff --git a/apps/app/src/api/install.rs b/apps/app/src/api/install.rs index 393f4923c4..96d2f0481b 100644 --- a/apps/app/src/api/install.rs +++ b/apps/app/src/api/install.rs @@ -25,6 +25,7 @@ pub fn init() -> tauri::plugin::TauriPlugin { install_job_retry, install_job_cancel, install_job_dismiss, + install_job_support_details, ]) .build() } @@ -169,3 +170,8 @@ pub async fn install_job_cancel(job_id: Uuid) -> Result { pub async fn install_job_dismiss(job_id: Uuid) -> Result<()> { Ok(theseus::install::dismiss_job(job_id).await?) } + +#[tauri::command] +pub async fn install_job_support_details(job_id: Uuid) -> Result { + Ok(theseus::install::job_support_details(job_id).await?) +} diff --git a/apps/frontend/src/components/ui/ProjectDownloadModal/DownloadProject.vue b/apps/frontend/src/components/ui/ProjectDownloadModal/DownloadProject.vue index 0f610f51b7..976b2a681f 100644 --- a/apps/frontend/src/components/ui/ProjectDownloadModal/DownloadProject.vue +++ b/apps/frontend/src/components/ui/ProjectDownloadModal/DownloadProject.vue @@ -126,6 +126,9 @@ @download="emit('download')" /> +
+ {{ noCompatibleVersionsDescription }} +
diff --git a/apps/frontend/src/components/ui/moderation/ModerationTechRevCard.vue b/apps/frontend/src/components/ui/moderation/ModerationTechRevCard.vue index 3b34f4d010..5437df1c49 100644 --- a/apps/frontend/src/components/ui/moderation/ModerationTechRevCard.vue +++ b/apps/frontend/src/components/ui/moderation/ModerationTechRevCard.vue @@ -1,7 +1,9 @@