diff --git a/apps/app-frontend/src/App.vue b/apps/app-frontend/src/App.vue index a4bc69aaf..63fbe35ed 100644 --- a/apps/app-frontend/src/App.vue +++ b/apps/app-frontend/src/App.vue @@ -68,11 +68,11 @@ import ErrorModal from '@/components/ui/ErrorModal.vue' import FriendsList from '@/components/ui/friends/FriendsList.vue' import AddServerToInstanceModal from '@/components/ui/install_flow/AddServerToInstanceModal.vue' import IncompatibilityWarningModal from '@/components/ui/install_flow/IncompatibilityWarningModal.vue' -import InstallConfirmModal from '@/components/ui/install_flow/InstallConfirmModal.vue' import MinecraftAuthErrorModal from '@/components/ui/minecraft-auth-error-modal/MinecraftAuthErrorModal.vue' import AppSettingsModal from '@/components/ui/modal/AppSettingsModal.vue' import AuthGrantFlowWaitModal from '@/components/ui/modal/AuthGrantFlowWaitModal.vue' import InstallToPlayModal from '@/components/ui/modal/InstallToPlayModal.vue' +import ModpackAlreadyInstalledModal from '@/components/ui/modal/ModpackAlreadyInstalledModal.vue' import UpdateToPlayModal from '@/components/ui/modal/UpdateToPlayModal.vue' import NavButton from '@/components/ui/NavButton.vue' import PromotionWrapper from '@/components/ui/PromotionWrapper.vue' @@ -151,6 +151,9 @@ const { handleBrowseModpacks, searchModpacks, getProjectVersions, + setModpackAlreadyInstalledModal, + handleModpackDuplicateCreateAnyway, + handleModpackDuplicateGoToInstance, } = setupProviders(notificationManager) const news = ref([]) @@ -424,7 +427,9 @@ const { handleNavigate: handleContentInstallNavigate, handleCancel: handleContentInstallCancel, setContentInstallModal, - setInstallConfirmModal: setContentInstallConfirmModal, + setModpackAlreadyInstalledModal: setContentInstallModpackAlreadyInstalledModal, + handleModpackDuplicateCreateAnyway: handleContentInstallModpackDuplicateCreateAnyway, + handleModpackDuplicateGoToInstance: handleContentInstallModpackDuplicateGoToInstance, setIncompatibilityWarningModal: setContentIncompatibilityWarningModal, } = contentInstall @@ -438,8 +443,9 @@ const { } = serverInstall const modInstallModal = ref() +const modpackAlreadyInstalledModal = ref() +const contentInstallModpackAlreadyInstalledModal = ref() const addServerToInstanceModal = ref() -const installConfirmModal = ref() const incompatibilityWarningModal = ref() const installToPlayModal = ref() const updateToPlayModal = ref() @@ -519,8 +525,9 @@ onMounted(() => { error.setMinecraftAuthErrorModal(minecraftAuthErrorModal.value) setContentIncompatibilityWarningModal(incompatibilityWarningModal.value) - setContentInstallConfirmModal(installConfirmModal.value) setContentInstallModal(modInstallModal.value) + setContentInstallModpackAlreadyInstalledModal(contentInstallModpackAlreadyInstalledModal.value) + setModpackAlreadyInstalledModal(modpackAlreadyInstalledModal.value) setServerAddServerToInstanceModal(addServerToInstanceModal.value) setServerInstallToPlayModal(installToPlayModal.value) setServerUpdateToPlayModal(updateToPlayModal.value) @@ -1295,9 +1302,18 @@ provideAppUpdateDownloadProgress(appUpdateDownload) @navigate="handleContentInstallNavigate" @cancel="handleContentInstallCancel" /> + - + diff --git a/apps/app-frontend/src/components/ui/install_flow/InstallConfirmModal.vue b/apps/app-frontend/src/components/ui/install_flow/InstallConfirmModal.vue deleted file mode 100644 index fe2749f7d..000000000 --- a/apps/app-frontend/src/components/ui/install_flow/InstallConfirmModal.vue +++ /dev/null @@ -1,77 +0,0 @@ - - - - - - You already have this modpack installed. Are you sure you want to install it again? - - $refs.confirmModal.hide()">Cancel - {{ installing ? 'Installing' : 'Install' }} - - - - - - diff --git a/apps/app-frontend/src/components/ui/modal/ModpackAlreadyInstalledModal.vue b/apps/app-frontend/src/components/ui/modal/ModpackAlreadyInstalledModal.vue new file mode 100644 index 000000000..6b16e8022 --- /dev/null +++ b/apps/app-frontend/src/components/ui/modal/ModpackAlreadyInstalledModal.vue @@ -0,0 +1,84 @@ + + + + {{ formatMessage(messages.admonitionBody, { instanceName }) }} + + + + + + + + {{ formatMessage(messages.goToInstance) }} + + + + + + {{ formatMessage(messages.createAnyway) }} + + + + + + + + diff --git a/apps/app-frontend/src/helpers/types.d.ts b/apps/app-frontend/src/helpers/types.d.ts index de90ff404..46fe5406a 100644 --- a/apps/app-frontend/src/helpers/types.d.ts +++ b/apps/app-frontend/src/helpers/types.d.ts @@ -1,6 +1,6 @@ import type { ModrinthId } from '@modrinth/utils' -type GameInstance = { +export type GameInstance = { path: string install_stage: InstallStage @@ -46,7 +46,7 @@ type LinkedData = { locked: boolean } -type InstanceLoader = 'vanilla' | 'forge' | 'fabric' | 'quilt' | 'neoforge' +export type InstanceLoader = 'vanilla' | 'forge' | 'fabric' | 'quilt' | 'neoforge' type ContentFile = { metadata?: { diff --git a/apps/app-frontend/src/locales/en-US/index.json b/apps/app-frontend/src/locales/en-US/index.json index 2ed90eae5..8d9bbdcde 100644 --- a/apps/app-frontend/src/locales/en-US/index.json +++ b/apps/app-frontend/src/locales/en-US/index.json @@ -41,6 +41,21 @@ "app.instance.confirm-delete.header": { "message": "Delete instance" }, + "app.instance.modpack-already-installed.admonition-body": { + "message": "This modpack is already installed in the \"{instanceName}\" instance." + }, + "app.instance.modpack-already-installed.admonition-header": { + "message": "Duplicate modpack" + }, + "app.instance.modpack-already-installed.create-anyway": { + "message": "Create anyway" + }, + "app.instance.modpack-already-installed.go-to-instance": { + "message": "Go to instance" + }, + "app.instance.modpack-already-installed.header": { + "message": "Modpack already installed" + }, "app.instance.mods.content-type-project": { "message": "project" }, diff --git a/apps/app-frontend/src/pages/instance/Mods.vue b/apps/app-frontend/src/pages/instance/Mods.vue index c3244a3ea..d08ef2532 100644 --- a/apps/app-frontend/src/pages/instance/Mods.vue +++ b/apps/app-frontend/src/pages/instance/Mods.vue @@ -20,6 +20,11 @@ @@ -471,7 +476,7 @@ async function handleModpackContentToggle(item: ContentItem) { } async function handleModpackContentBulkToggle(items: ContentItem[]) { - await Promise.all(items.map((item) => toggleDisableMod(item))) + await Promise.all(items.map((item) => _toggleDisableMod(item))) } async function handleModpackContent() { @@ -814,13 +819,12 @@ provideContentManager({ isPackLocked, isBusy: isInstanceBusy, isBulkOperating, - getItemId: (item) => item.file_path ?? item.file_name, contentTypeLabel: ref(formatMessage(messages.contentTypeProject)), toggleEnabled: toggleDisableMod, bulkEnableItems: (items) => - Promise.all(items.map((item) => toggleDisableMod(item))).then(() => {}), + Promise.all(items.map((item) => _toggleDisableMod(item))).then(() => {}), bulkDisableItems: (items) => - Promise.all(items.map((item) => toggleDisableMod(item))).then(() => {}), + Promise.all(items.map((item) => _toggleDisableMod(item))).then(() => {}), deleteItem: removeMod, bulkDeleteItems: (items) => Promise.all(items.map((item) => removeMod(item))).then(() => {}), refresh: () => initProjects('must_revalidate'), @@ -838,7 +842,7 @@ provideContentManager({ dismissContentHint, shareItems: handleShareItems, mapToTableItem: (item) => ({ - id: item.file_path ?? item.file_name, + id: item.id, project: item.project ?? { id: item.file_name, slug: null, diff --git a/apps/app-frontend/src/providers/content-install.ts b/apps/app-frontend/src/providers/content-install.ts index 463eafc25..3ec65e1cc 100644 --- a/apps/app-frontend/src/providers/content-install.ts +++ b/apps/app-frontend/src/providers/content-install.ts @@ -26,6 +26,7 @@ import { remove_project, } from '@/helpers/profile.js' import { get_game_versions } from '@/helpers/tags' +import type { GameInstance, InstanceLoader } from '@/helpers/types' import { findPreferredVersion, installVersionDependencies, @@ -37,13 +38,8 @@ interface ModalRef { hide: () => void } -interface InstallConfirmModalRef { - show: ( - project: Labrinth.Projects.v2.Project, - version: string, - callback: (versionId?: string) => void, - createInstanceCallback: (profile: string) => void, - ) => void +interface ModpackAlreadyInstalledModalRef { + show: (instanceName: string, instancePath: string) => void } interface IncompatibilityWarningModalRef { @@ -92,7 +88,9 @@ export interface ContentInstallContext { handleNavigate: (instance: ContentInstallInstance) => void handleCancel: () => void setContentInstallModal: (ref: ModalRef) => void - setInstallConfirmModal: (ref: InstallConfirmModalRef) => void + setModpackAlreadyInstalledModal: (ref: ModpackAlreadyInstalledModalRef) => void + handleModpackDuplicateCreateAnyway: () => Promise + handleModpackDuplicateGoToInstance: (instancePath: string) => void setIncompatibilityWarningModal: (ref: IncompatibilityWarningModalRef) => void install: ( projectId: string, @@ -140,12 +138,13 @@ export function createContentInstall(opts: { ) { const primaryFile = version?.files?.find((f) => f.primary) ?? version?.files?.[0] const placeholder: ContentItem = { + id: `__installing_${project.id}`, file_name: `__installing_${project.id}`, project: { id: project.id, - slug: project.slug ?? null, + slug: project.slug ?? '', title: project.title, - icon_url: project.icon_url ?? null, + icon_url: project.icon_url ?? undefined, }, version: version ? { @@ -183,18 +182,26 @@ export function createContentInstall(opts: { } let modalRef: ModalRef | null = null - let installConfirmModalRef: InstallConfirmModalRef | null = null + let modpackAlreadyInstalledModalRef: ModpackAlreadyInstalledModalRef | null = null let incompatibilityWarningModalRef: IncompatibilityWarningModalRef | null = null let currentProject: Labrinth.Projects.v2.Project | null = null let currentVersions: Labrinth.Versions.v2.Version[] = [] let currentCallback: (versionId?: string) => void = () => {} let profileMap: Record = {} + let pendingModpackInstall: { + project: Labrinth.Projects.v2.Project + version: string + source: string + callback: (versionId?: string) => void + createInstanceCallback: (profile: string) => void + } | null = null + async function showModInstallModal( project: Labrinth.Projects.v2.Project, versions: Labrinth.Versions.v2.Version[], onInstall: (versionId?: string) => void, - hints?: { preferredLoader?: string; preferredGameVersion?: string }, + hints?: { preferredLoader?: string; preferredGameVersion?: string; showProjectInfo?: boolean }, ) { currentProject = project currentVersions = versions @@ -379,10 +386,10 @@ export function createContentInstall(opts: { trackEvent('ProjectInstall', { loader: profile.loader, game_version: profile.game_version, - id: currentProject.id, + id: currentProject!.id, version_id: version.id, - project_type: currentProject.project_type, - title: currentProject.title, + project_type: currentProject!.project_type, + title: currentProject!.title, source: 'ProjectInstallModal', }) currentCallback(version.id) @@ -433,10 +440,10 @@ export function createContentInstall(opts: { trackEvent('ProjectInstall', { loader: data.loader, game_version: data.gameVersion, - id: currentProject.id, + id: currentProject!.id, version_id: version.id, - project_type: currentProject.project_type, - title: currentProject.title, + project_type: currentProject!.project_type, + title: currentProject!.title, source: 'ProjectInstallModal', }) @@ -470,28 +477,28 @@ export function createContentInstall(opts: { if (project.project_type === 'modpack') { const version = versionId ?? project.versions[project.versions.length - 1] const packs = await list() + const existingPack = packs.find((pack) => pack.linked_data?.project_id === project.id) - if ( - packs.length === 0 || - !packs.find((pack) => pack.linked_data?.project_id === project.id) - ) { - await packInstall( - project.id, - version, - project.title, - project.icon_url, - createInstanceCallback, - ) - trackEvent('PackInstall', { - id: project.id, - version_id: version, - title: project.title, - source, - }) - callback(version) - } else { - installConfirmModalRef?.show(project, version, callback, createInstanceCallback) + if (existingPack) { + pendingModpackInstall = { project, version, source, callback, createInstanceCallback } + modpackAlreadyInstalledModalRef?.show(existingPack.name, existingPack.path) + return } + + await packInstall( + project.id, + version, + project.title, + project.icon_url, + createInstanceCallback, + ) + trackEvent('PackInstall', { + id: project.id, + version_id: version, + title: project.title, + source, + }) + callback(version) } else if (instancePath) { const [instanceOrNull, instanceProjects, versions] = await Promise.all([ get(instancePath), @@ -577,8 +584,31 @@ export function createContentInstall(opts: { setContentInstallModal(ref: ModalRef) { modalRef = ref }, - setInstallConfirmModal(ref: InstallConfirmModalRef) { - installConfirmModalRef = ref + setModpackAlreadyInstalledModal(ref: ModpackAlreadyInstalledModalRef) { + modpackAlreadyInstalledModalRef = ref + }, + async handleModpackDuplicateCreateAnyway() { + if (!pendingModpackInstall) return + const { project, version, source, callback, createInstanceCallback } = pendingModpackInstall + pendingModpackInstall = null + await packInstall( + project.id, + version, + project.title, + project.icon_url, + createInstanceCallback, + ) + trackEvent('PackInstall', { + id: project.id, + version_id: version, + title: project.title, + source, + }) + callback(version) + }, + handleModpackDuplicateGoToInstance(instancePath: string) { + pendingModpackInstall = null + opts.router.push(`/instance/${encodeURIComponent(instancePath)}/`) }, setIncompatibilityWarningModal(ref: IncompatibilityWarningModalRef) { incompatibilityWarningModalRef = ref diff --git a/apps/app-frontend/src/providers/setup/creation-modal.ts b/apps/app-frontend/src/providers/setup/creation-modal.ts index 8d4b1b3ae..4020fe8b3 100644 --- a/apps/app-frontend/src/providers/setup/creation-modal.ts +++ b/apps/app-frontend/src/providers/setup/creation-modal.ts @@ -1,18 +1,33 @@ -import type { AbstractWebNotificationManager, CreationFlowContextValue } from '@modrinth/ui' -import { provide, useTemplateRef } from 'vue' +import type { + AbstractWebNotificationManager, + CreationFlowContextValue, + CreationFlowModal, +} from '@modrinth/ui' +import { provide, ref, useTemplateRef } from 'vue' +import type { ComponentExposed } from 'vue-component-type-helpers' import { useRouter } from 'vue-router' +import type ModpackAlreadyInstalledModal from '@/components/ui/modal/ModpackAlreadyInstalledModal.vue' import { trackEvent } from '@/helpers/analytics' import { get_project_versions, get_search_results } from '@/helpers/cache.js' import { import_instance } from '@/helpers/import.js' import { create_profile_and_install, create_profile_and_install_from_file } from '@/helpers/pack' import { create, list } from '@/helpers/profile.js' +import type { InstanceLoader } from '@/helpers/types' export function setupCreationModal(notificationManager: AbstractWebNotificationManager) { const { handleError } = notificationManager const router = useRouter() - const installationModal = useTemplateRef('installationModal') + const installationModal = + useTemplateRef>('installationModal') + const modpackAlreadyInstalledModal = ref>() + + function setModpackAlreadyInstalledModal( + modal: InstanceType, + ) { + modpackAlreadyInstalledModal.value = modal + } async function fetchExistingInstanceNames(): Promise { const instances = await list().catch(handleError) @@ -23,10 +38,34 @@ export function setupCreationModal(notificationManager: AbstractWebNotificationM installationModal.value?.show() }) - async function handleCreate(config: CreationFlowContextValue) { - installationModal.value?.hide() + async function proceedWithModpackCreation( + projectId: string, + versionId: string, + name: string, + iconUrl?: string, + ) { + await create_profile_and_install(projectId, versionId, name, iconUrl).catch(handleError) + trackEvent('InstanceCreate', { source: 'CreationModalModpack' }) + } + async function handleCreate(config: CreationFlowContextValue) { try { + if (config.modpackSelection.value) { + const { projectId, versionId, name, iconUrl } = config.modpackSelection.value + + const instances = await list().catch(handleError) + const existingInstance = instances?.find((i) => i.linked_data?.project_id === projectId) + + if (existingInstance) { + pendingModpackCreation.value = { projectId, versionId, name, iconUrl } + installationModal.value?.hide() + modpackAlreadyInstalledModal.value?.show(existingInstance.name, existingInstance.path) + return + } + } + + installationModal.value?.hide() + if (config.isImportMode.value) { for (const [launcherName, instanceSet] of Object.entries( config.importSelectedInstances.value, @@ -43,8 +82,7 @@ export function setupCreationModal(notificationManager: AbstractWebNotificationM if (config.modpackSelection.value) { const { projectId, versionId, name, iconUrl } = config.modpackSelection.value - await create_profile_and_install(projectId, versionId, name, iconUrl).catch(handleError) - trackEvent('InstanceCreate', { source: 'CreationModalModpack' }) + await proceedWithModpackCreation(projectId, versionId, name, iconUrl) return } @@ -66,26 +104,40 @@ export function setupCreationModal(notificationManager: AbstractWebNotificationM await create( name, - config.selectedGameVersion.value, - loader, + config.selectedGameVersion.value!, + loader as InstanceLoader, loaderVersion, iconPath, false, ).catch(handleError) trackEvent('InstanceCreate', { - profile_name: name, - game_version: config.selectedGameVersion.value, - loader, - loader_version: loaderVersion, - has_icon: !!iconPath, source: 'CreationModal', }) } catch (err) { - handleError(err) + handleError(err as Error) } } + const pendingModpackCreation = ref<{ + projectId: string + versionId: string + name: string + iconUrl?: string + } | null>(null) + + async function handleModpackDuplicateCreateAnyway() { + if (!pendingModpackCreation.value) return + const { projectId, versionId, name, iconUrl } = pendingModpackCreation.value + pendingModpackCreation.value = null + await proceedWithModpackCreation(projectId, versionId, name, iconUrl) + } + + function handleModpackDuplicateGoToInstance(instancePath: string) { + pendingModpackCreation.value = null + router.push(`/instance/${encodeURIComponent(instancePath)}/`) + } + function handleBrowseModpacks() { installationModal.value?.hide() router.push('/browse/modpack') @@ -113,5 +165,8 @@ export function setupCreationModal(notificationManager: AbstractWebNotificationM handleBrowseModpacks, searchModpacks, getProjectVersions, + setModpackAlreadyInstalledModal, + handleModpackDuplicateCreateAnyway, + handleModpackDuplicateGoToInstance, } } diff --git a/apps/frontend/package.json b/apps/frontend/package.json index 08370f641..1c023e0ce 100644 --- a/apps/frontend/package.json +++ b/apps/frontend/package.json @@ -75,6 +75,7 @@ "semver": "^7.5.4", "three": "^0.172.0", "vue-confetti-explosion": "^1.0.2", + "vue-router": "*", "vue-typed-virtual-list": "^1.0.10", "vue3-ace-editor": "^2.2.4", "vue3-apexcharts": "^1.5.2", diff --git a/packages/app-lib/src/state/instances/content.rs b/packages/app-lib/src/state/instances/content.rs index 826ba5844..ffe7be073 100644 --- a/packages/app-lib/src/state/instances/content.rs +++ b/packages/app-lib/src/state/instances/content.rs @@ -35,8 +35,9 @@ pub struct ContentItem { pub file_name: String, /// Relative path to the file within the profile pub file_path: String, - /// SHA1 hash of the file - pub hash: String, + /// Stable frontend identifier (SHA1 hash of file content, survives renames). + /// Not a project or version ID. + pub id: String, /// File size in bytes pub size: u64, /// Whether the file is enabled (not .disabled) @@ -542,7 +543,7 @@ async fn profile_files_to_content_items( ContentItem { file_name: file.file_name.clone(), file_path: path.clone(), - hash: file.hash.clone(), + id: file.hash.clone(), size: file.size, enabled: !file.file_name.ends_with(".disabled"), project_type: file.project_type, @@ -726,7 +727,7 @@ pub async fn dependencies_to_content_items( ) }), file_path: String::new(), - hash: String::new(), + id: String::new(), size: version .and_then(|v| v.files.first()) .map(|f| f.size as u64) diff --git a/packages/assets/generated-icons.ts b/packages/assets/generated-icons.ts index 9905214c3..f1f921902 100644 --- a/packages/assets/generated-icons.ts +++ b/packages/assets/generated-icons.ts @@ -19,6 +19,7 @@ import _ArrowLeftRightIcon from './icons/arrow-left-right.svg?component' import _ArrowUpIcon from './icons/arrow-up.svg?component' import _ArrowUpDownIcon from './icons/arrow-up-down.svg?component' import _ArrowUpRightIcon from './icons/arrow-up-right.svg?component' +import _ArrowUpZAIcon from './icons/arrow-up-z-a.svg?component' import _AsteriskIcon from './icons/asterisk.svg?component' import _BadgeCheckIcon from './icons/badge-check.svg?component' import _BadgeDollarSignIcon from './icons/badge-dollar-sign.svg?component' @@ -404,6 +405,7 @@ export const ArrowLeftRightIcon = _ArrowLeftRightIcon export const ArrowUpIcon = _ArrowUpIcon export const ArrowUpDownIcon = _ArrowUpDownIcon export const ArrowUpRightIcon = _ArrowUpRightIcon +export const ArrowUpZAIcon = _ArrowUpZAIcon export const AsteriskIcon = _AsteriskIcon export const BadgeCheckIcon = _BadgeCheckIcon export const BadgeDollarSignIcon = _BadgeDollarSignIcon diff --git a/packages/assets/icons/arrow-up-z-a.svg b/packages/assets/icons/arrow-up-z-a.svg new file mode 100644 index 000000000..0e68ae581 --- /dev/null +++ b/packages/assets/icons/arrow-up-z-a.svg @@ -0,0 +1,19 @@ + + + + + + + + diff --git a/packages/ui/src/components/base/FloatingActionBar.vue b/packages/ui/src/components/base/FloatingActionBar.vue index a9d872c1b..0fb04c271 100644 --- a/packages/ui/src/components/base/FloatingActionBar.vue +++ b/packages/ui/src/components/base/FloatingActionBar.vue @@ -1,11 +1,48 @@ @@ -27,9 +65,11 @@ onUnmounted(() => { aria-live="polite" > @@ -81,4 +121,12 @@ onUnmounted(() => { .intercom-lightweight-app-launcher { z-index: 9 !important; } + +.bar-compact .bar-label { + display: none; +} + +.bar-compact .cq-show-icon { + display: block; +} diff --git a/packages/ui/src/components/flows/creation-flow-modal/components/FinalConfigStage.vue b/packages/ui/src/components/flows/creation-flow-modal/components/FinalConfigStage.vue index 00ace6bc8..2638cf986 100644 --- a/packages/ui/src/components/flows/creation-flow-modal/components/FinalConfigStage.vue +++ b/packages/ui/src/components/flows/creation-flow-modal/components/FinalConfigStage.vue @@ -100,15 +100,17 @@ diff --git a/packages/ui/src/components/flows/creation-flow-modal/stages/final-config-stage.ts b/packages/ui/src/components/flows/creation-flow-modal/stages/final-config-stage.ts index d2ee28b0d..41294299f 100644 --- a/packages/ui/src/components/flows/creation-flow-modal/stages/final-config-stage.ts +++ b/packages/ui/src/components/flows/creation-flow-modal/stages/final-config-stage.ts @@ -44,7 +44,7 @@ export const stageConfig: StageConfigInput = { icon: isFinish ? PlusIcon : RightArrowIcon, iconPosition: isFinish ? ('before' as const) : ('after' as const), color: isReset ? ('red' as const) : isFinish ? ('brand' as const) : undefined, - disabled: isForwardBlocked(ctx), + disabled: isForwardBlocked(ctx) || ctx.isBackingUp.value, loading: isFinish && ctx.loading.value, onClick: () => { if (isFinish) { diff --git a/packages/ui/src/components/servers/backups/BackupProgressAdmonition.vue b/packages/ui/src/components/servers/backups/BackupProgressAdmonition.vue index d1110a901..3dc327303 100644 --- a/packages/ui/src/components/servers/backups/BackupProgressAdmonition.vue +++ b/packages/ui/src/components/servers/backups/BackupProgressAdmonition.vue @@ -109,7 +109,7 @@ const description = computed(() => { const messages = defineMessages({ fallbackName: { id: 'servers.backups.admonition.fallback-name', - defaultMessage: 'your backup', + defaultMessage: 'Your backup', }, backupQueuedTitle: { id: 'servers.backups.admonition.backup-queued.title', diff --git a/packages/ui/src/components/servers/backups/BackupProgressAdmonitions.vue b/packages/ui/src/components/servers/backups/BackupProgressAdmonitions.vue index b6ae21906..1e56e0671 100644 --- a/packages/ui/src/components/servers/backups/BackupProgressAdmonitions.vue +++ b/packages/ui/src/components/servers/backups/BackupProgressAdmonitions.vue @@ -84,10 +84,10 @@ const admonitions = computed(() => { // 1. Active WS entries (real-time progress from backupsState) for (const [id, entry] of backupsState.entries()) { const backup = findBackup(id) + seenIds.add(id) if (entry.create && entry.create.state === 'ongoing') { const key = `${id}:create` if (!dismissedIds.has(key)) { - seenIds.add(id) result.push({ key, backupId: id, @@ -102,7 +102,6 @@ const admonitions = computed(() => { if (entry.restore && entry.restore.state === 'ongoing') { const key = `${id}:restore` if (!dismissedIds.has(key)) { - seenIds.add(id) result.push({ key, backupId: id, diff --git a/packages/ui/src/components/servers/files/explorer/TeleportOverflowMenu.vue b/packages/ui/src/components/servers/files/explorer/TeleportOverflowMenu.vue index 369bc2f48..d0acb339b 100644 --- a/packages/ui/src/components/servers/files/explorer/TeleportOverflowMenu.vue +++ b/packages/ui/src/components/servers/files/explorer/TeleportOverflowMenu.vue @@ -178,10 +178,10 @@ const calculateMenuPosition = () => { top = Math.max(margin, window.innerHeight - menuHeight - margin) } - if (triggerRect.left + menuWidth + margin <= window.innerWidth) { - left = triggerRect.left - } else if (triggerRect.right - menuWidth - margin >= 0) { + if (triggerRect.right - menuWidth >= margin) { left = triggerRect.right - menuWidth + } else if (triggerRect.left + menuWidth + margin <= window.innerWidth) { + left = triggerRect.left } else { left = Math.max(margin, window.innerWidth - menuWidth - margin) } diff --git a/packages/ui/src/layouts/shared/content-tab/components/ContentCardItem.vue b/packages/ui/src/layouts/shared/content-tab/components/ContentCardItem.vue index 082d811bf..d3e0ed5f5 100644 --- a/packages/ui/src/layouts/shared/content-tab/components/ContentCardItem.vue +++ b/packages/ui/src/layouts/shared/content-tab/components/ContentCardItem.vue @@ -87,22 +87,23 @@ const deleteHovered = ref(false) :class="{ 'opacity-50': disabled }" > - + emit('update:enabled', val as boolean)" /> diff --git a/packages/ui/src/layouts/shared/content-tab/components/ContentSelectionBar.vue b/packages/ui/src/layouts/shared/content-tab/components/ContentSelectionBar.vue index 1e731024a..2f5359338 100644 --- a/packages/ui/src/layouts/shared/content-tab/components/ContentSelectionBar.vue +++ b/packages/ui/src/layouts/shared/content-tab/components/ContentSelectionBar.vue @@ -1,5 +1,5 @@
You already have this modpack installed. Are you sure you want to install it again?