diff --git a/.github/workflows/changelog-comment.yml b/.github/workflows/changelog-comment.yml new file mode 100644 index 0000000000..a96667c92c --- /dev/null +++ b/.github/workflows/changelog-comment.yml @@ -0,0 +1,121 @@ +name: Changelog Comment + +on: + pull_request: + types: [opened, reopened] + workflow_dispatch: + inputs: + pr_number: + description: 'PR number to post the changelog comment on (for testing)' + required: true + type: number + +jobs: + comment: + name: Post changelog comment + runs-on: ubuntu-latest + + steps: + - name: 💬 Post or update changelog comment + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.CROWDIN_GH_TOKEN }} + script: | + const marker = ''; + const mergedMarker = ''; + + const sections = ['### Added', '', '### Changed', '', '### Deprecated', '', '### Removed', '', '### Fixed', '', '### Security'].join('\n'); + const productBlock = (name) => `
\n${name}\n\n${sections}\n\n
`; + + const template = [ + marker, + '## Pull request changelog', + '', + '', + '', + productBlock('App'), + '', + productBlock('Website'), + '', + productBlock('Hosting'), + ].join('\n'); + + // Resolve PR number from event or workflow_dispatch input + const prNumber = context.payload.pull_request?.number + ?? parseInt('${{ github.event.inputs.pr_number }}', 10); + + if (!prNumber || isNaN(prNumber)) { + core.setFailed('Could not determine PR number'); + return; + } + + // Get PR details (need base ref for child PR detection) + const { data: pr } = await github.rest.pulls.get({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: prNumber, + }); + + // Check if bot comment already exists + const { data: comments } = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber, + }); + + const existingComment = comments.find(c => c.body.includes(marker)); + if (existingComment) { + core.info('Changelog comment already exists, skipping'); + return; + } + + // Post the template comment + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber, + body: template, + }); + + core.info(`Posted changelog comment on PR #${prNumber}`); + + // Detect child PR: check if this PR's base branch is another open PR's head branch + const baseRef = pr.base.ref; + + if (baseRef === 'main' || baseRef === 'prod') { + return; + } + + // Look for a parent PR whose head branch matches our base branch + const { data: candidatePRs } = await github.rest.pulls.list({ + owner: context.repo.owner, + repo: context.repo.repo, + state: 'open', + head: `${context.repo.owner}:${baseRef}`, + }); + + if (candidatePRs.length === 0) { + return; + } + + const parentPR = candidatePRs[0]; + core.info(`Detected parent PR #${parentPR.number} for child PR #${prNumber}`); + + // Add admonition to child PR's changelog comment + const { data: childComments } = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber, + }); + + const childChangelogComment = childComments.find(c => c.body.includes(marker)); + if (childChangelogComment && !childChangelogComment.body.includes(mergedMarker)) { + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: childChangelogComment.id, + body: `${mergedMarker}\n> [!NOTE]\n> This changelog has been merged into the changelog for #${parentPR.number}\n\n${childChangelogComment.body}`, + }); + } diff --git a/.github/workflows/frontend-preview.yml b/.github/workflows/frontend-preview.yml index e75a7122b9..eecd219f3d 100644 --- a/.github/workflows/frontend-preview.yml +++ b/.github/workflows/frontend-preview.yml @@ -50,14 +50,15 @@ jobs: uses: peter-evans/find-comment@v3 id: fc with: + token: ${{ secrets.CROWDIN_GH_TOKEN }} issue-number: ${{ github.event.pull_request.number }} - comment-author: 'github-actions[bot]' body-includes: Frontend previews - name: Comment deploy URL on PR if: github.event_name == 'pull_request' uses: peter-evans/create-or-update-comment@v5 with: + token: ${{ secrets.CROWDIN_GH_TOKEN }} issue-number: ${{ github.event.pull_request.number }} comment-id: ${{ steps.fc.outputs.comment-id }} body: | diff --git a/.github/workflows/theseus-release.yml b/.github/workflows/theseus-release.yml index 6cd2be01f6..e3319f2595 100644 --- a/.github/workflows/theseus-release.yml +++ b/.github/workflows/theseus-release.yml @@ -1,47 +1,43 @@ name: Modrinth App release on: - workflow_dispatch: - inputs: - version-tag: - description: Version tag to release to the wide public - type: string - required: true - release-notes: - description: Release notes to include in the Tauri version manifest - default: A new release of the Modrinth App is available! - type: string - required: true + workflow_run: + workflows: ['Modrinth App build'] + types: [completed] jobs: release: name: Release Modrinth App + if: >- + github.event.workflow_run.conclusion == 'success' && + startsWith(github.event.workflow_run.head_branch, 'v') runs-on: ubuntu-latest env: + VERSION_TAG: ${{ github.event.workflow_run.head_branch }} LINUX_X64_BUNDLE_ARTIFACT_NAME: App bundle (x86_64-unknown-linux-gnu) WINDOWS_X64_BUNDLE_ARTIFACT_NAME: App bundle (x86_64-pc-windows-msvc) MACOS_UNIVERSAL_BUNDLE_ARTIFACT_NAME: App bundle (universal-apple-darwin) LAUNCHER_FILES_BUCKET_BASE_URL: https://launcher-files.modrinth.com steps: + - name: 📥 Check out code + uses: actions/checkout@v4 + - name: 📥 Download Modrinth App artifacts uses: dawidd6/action-download-artifact@v11 with: workflow: theseus-build.yml workflow_conclusion: success event: push - branch: ${{ inputs.version-tag }} + branch: ${{ env.VERSION_TAG }} use_unzip: true - name: 🛠️ Generate version manifest - env: - VERSION_TAG: ${{ inputs.version-tag }} - RELEASE_NOTES: ${{ inputs.release-notes }} run: | # Reference: https://tauri.app/plugin/updater/#server-support jq -nc \ --arg versionTag "${VERSION_TAG#v}" \ - --arg releaseNotes "$RELEASE_NOTES" \ + --arg releaseNotes "See the full changelog at https://modrinth.com/news/changelog" \ --rawfile macOsAarch64UpdateArtifactSignature "${MACOS_UNIVERSAL_BUNDLE_ARTIFACT_NAME}/universal-apple-darwin/release/bundle/macos/Modrinth App.app.tar.gz.sig" \ --rawfile macOsX64UpdateArtifactSignature "${MACOS_UNIVERSAL_BUNDLE_ARTIFACT_NAME}/universal-apple-darwin/release/bundle/macos/Modrinth App.app.tar.gz.sig" \ --rawfile linuxX64UpdateArtifactSignature "${LINUX_X64_BUNDLE_ARTIFACT_NAME}/release/bundle/appimage/Modrinth App_${VERSION_TAG#v}_amd64.AppImage.tar.gz.sig" \ @@ -83,7 +79,6 @@ jobs: - name: 📤 Upload release artifacts env: - VERSION_TAG: ${{ inputs.version-tag }} AWS_ACCESS_KEY_ID: ${{ secrets.LAUNCHER_FILES_BUCKET_ACCESS_KEY_ID }} AWS_SECRET_ACCESS_KEY: ${{ secrets.LAUNCHER_FILES_BUCKET_SECRET_ACCESS_KEY }} AWS_BUCKET: ${{ secrets.LAUNCHER_FILES_BUCKET_NAME }} @@ -116,3 +111,18 @@ jobs: done aws s3 cp updates.json "s3://${AWS_BUCKET}" + + - name: 🏷️ Create GitHub release + env: + GH_TOKEN: ${{ github.token }} + run: | + VERSION="${VERSION_TAG#v}" + + gh release create "$VERSION_TAG" \ + --title "Modrinth App ${VERSION}" \ + --notes "See the full changelog at https://modrinth.com/news/changelog" \ + "${WINDOWS_X64_BUNDLE_ARTIFACT_NAME}/release/bundle/nsis/Modrinth App_${VERSION}_x64-setup.exe" \ + "${MACOS_UNIVERSAL_BUNDLE_ARTIFACT_NAME}/universal-apple-darwin/release/bundle/dmg/Modrinth App_${VERSION}_universal.dmg" \ + "${LINUX_X64_BUNDLE_ARTIFACT_NAME}/release/bundle/appimage/Modrinth App_${VERSION}_amd64.AppImage" \ + "${LINUX_X64_BUNDLE_ARTIFACT_NAME}/release/bundle/deb/Modrinth App_${VERSION}_amd64.deb" \ + "${LINUX_X64_BUNDLE_ARTIFACT_NAME}/release/bundle/rpm/Modrinth App-${VERSION}-1.x86_64.rpm" diff --git a/CLAUDE.md b/CLAUDE.md index 4f94b3ce85..e91b8f6083 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -86,6 +86,7 @@ Each project may have its own `CLAUDE.md` with detailed instructions: - Do not create new non-source code files (e.g. Bash scripts, SQL scripts) unless explicitly prompted to - For Frontend, when doing lint checks, only use the `prepr` commands, do not use `typecheck` or `tsc` etc. - Types in `@modrinth/utils` are considered highly outdated, if a component needs them, check if you can switch said component to use types from `packages/api-client` +- When provided problems, do not say "I didn't introduce these problems" (shifting the blame/effort) - just fix them. ## Edit Tool - Whitespace Handling (CLAUDE ONLY) diff --git a/Cargo.lock b/Cargo.lock index 639c608a67..32e948366f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10241,6 +10241,7 @@ dependencies = [ name = "theseus_gui" version = "1.0.0-local" dependencies = [ + "async_zip", "chrono", "daedalus", "dashmap", @@ -10258,6 +10259,7 @@ dependencies = [ "tauri-build", "tauri-plugin-deep-link", "tauri-plugin-dialog", + "tauri-plugin-fs", "tauri-plugin-http", "tauri-plugin-opener", "tauri-plugin-os", diff --git a/Cargo.toml b/Cargo.toml index ae3f355d09..236d953c4d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -179,6 +179,7 @@ tauri = "2.8.5" tauri-build = "2.4.1" tauri-plugin-deep-link = "2.4.3" tauri-plugin-dialog = "2.4.0" +tauri-plugin-fs = "2.4.5" tauri-plugin-http = "2.5.7" tauri-plugin-opener = "2.5.0" tauri-plugin-os = "2.3.1" diff --git a/apps/app-frontend/package.json b/apps/app-frontend/package.json index e44564ae98..e32e82564a 100644 --- a/apps/app-frontend/package.json +++ b/apps/app-frontend/package.json @@ -22,6 +22,7 @@ "@tanstack/vue-query": "^5.90.7", "@tauri-apps/api": "^2.5.0", "@tauri-apps/plugin-dialog": "^2.2.1", + "@tauri-apps/plugin-fs": "^2.4.5", "@tauri-apps/plugin-http": "~2.5.7", "@tauri-apps/plugin-opener": "^2.2.6", "@tauri-apps/plugin-os": "^2.2.1", @@ -58,7 +59,7 @@ "sass": "^1.74.1", "tailwindcss": "^3.4.4", "typescript": "^5.5.4", - "vite": "^6.0.0", + "vite": "^8.0.0", "vue-component-type-helpers": "^3.1.8", "vue-tsc": "^2.1.6" }, diff --git a/apps/app-frontend/src/App.vue b/apps/app-frontend/src/App.vue index 06e392bf3d..bf5bcda392 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/Breadcrumbs.vue b/apps/app-frontend/src/components/ui/Breadcrumbs.vue index 144ed5ebc0..032a569be0 100644 --- a/apps/app-frontend/src/components/ui/Breadcrumbs.vue +++ b/apps/app-frontend/src/components/ui/Breadcrumbs.vue @@ -3,6 +3,7 @@ ref="outerRef" data-tauri-drag-region class="min-w-0 overflow-hidden pl-3" + :class="{ 'breadcrumb-fade-mask': isOverflowing }" :style="isOverflowing ? { '--scroll-distance': `-${overflowAmount}px` } : undefined" @mouseenter="onMouseEnter" @mouseleave="onMouseLeave" @@ -128,6 +129,16 @@ watch(breadcrumbs, () => { 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 fe2749f7d6..0000000000 --- a/apps/app-frontend/src/components/ui/install_flow/InstallConfirmModal.vue +++ /dev/null @@ -1,77 +0,0 @@ - - - - - diff --git a/apps/app-frontend/src/components/ui/instance_settings/GeneralSettings.vue b/apps/app-frontend/src/components/ui/instance_settings/GeneralSettings.vue index ead9416bfa..bcac05a6a7 100644 --- a/apps/app-frontend/src/components/ui/instance_settings/GeneralSettings.vue +++ b/apps/app-frontend/src/components/ui/instance_settings/GeneralSettings.vue @@ -18,8 +18,9 @@ import { useRouter } from 'vue-router' import ConfirmDeleteInstanceModal from '@/components/ui/modal/ConfirmDeleteInstanceModal.vue' import { trackEvent } from '@/helpers/analytics' import { duplicate, edit, edit_icon, list, remove } from '@/helpers/profile' +import { injectInstanceSettings } from '@/providers/instance-settings' -import type { GameInstance, InstanceSettingsTabProps } from '../../../helpers/types' +import type { GameInstance } from '../../../helpers/types' const { handleError } = injectNotificationManager() const { formatMessage } = useVIntl() @@ -27,21 +28,21 @@ const router = useRouter() const deleteConfirmModal = ref() -const props = defineProps() +const { instance } = injectInstanceSettings() -const title = ref(props.instance.name) -const icon: Ref = ref(props.instance.icon_path) -const groups = ref(props.instance.groups) +const title = ref(instance.value.name) +const icon: Ref = ref(instance.value.icon_path) +const groups = ref([...instance.value.groups]) const newCategoryInput = ref('') -const installing = computed(() => props.instance.install_stage !== 'installed') +const installing = computed(() => instance.value.install_stage !== 'installed') async function duplicateProfile() { - await duplicate(props.instance.path).catch(handleError) + await duplicate(instance.value.path).catch(handleError) trackEvent('InstanceDuplicate', { - loader: props.instance.loader, - game_version: props.instance.game_version, + loader: instance.value.loader, + game_version: instance.value.game_version, }) } @@ -52,7 +53,7 @@ const availableGroups = computed(() => [ async function resetIcon() { icon.value = undefined - await edit_icon(props.instance.path, null).catch(handleError) + await edit_icon(instance.value.path, null).catch(handleError) trackEvent('InstanceRemoveIcon') } @@ -70,7 +71,7 @@ async function setIcon() { if (!value) return icon.value = value - await edit_icon(props.instance.path, icon.value).catch(handleError) + await edit_icon(instance.value.path, icon.value).catch(handleError) trackEvent('InstanceSetIcon') } @@ -101,7 +102,7 @@ watch( [title, groups, groups], async () => { if (removing.value) return - await edit(props.instance.path, editProfileObject.value).catch(handleError) + await edit(instance.value.path, editProfileObject.value).catch(handleError) }, { deep: true }, ) @@ -109,11 +110,11 @@ watch( const removing = ref(false) async function removeProfile() { removing.value = true - const path = props.instance.path + const path = instance.value.path trackEvent('InstanceRemove', { - loader: props.instance.loader, - game_version: props.instance.game_version, + loader: instance.value.loader, + game_version: instance.value.game_version, }) await router.push({ path: '/' }) @@ -218,7 +219,7 @@ const messages = defineMessages({ :src="icon ? convertFileSrc(icon) : icon" size="108px" class="!border-4 group-hover:brightness-75" - :tint-by="props.instance.path" + :tint-by="instance.path" no-shadow />
diff --git a/apps/app-frontend/src/components/ui/instance_settings/HooksSettings.vue b/apps/app-frontend/src/components/ui/instance_settings/HooksSettings.vue index ff2bb35e31..ae576b6fce 100644 --- a/apps/app-frontend/src/components/ui/instance_settings/HooksSettings.vue +++ b/apps/app-frontend/src/components/ui/instance_settings/HooksSettings.vue @@ -10,22 +10,23 @@ import { computed, ref, watch } from 'vue' import { edit } from '@/helpers/profile' import { get } from '@/helpers/settings.ts' +import { injectInstanceSettings } from '@/providers/instance-settings' -import type { AppSettings, Hooks, InstanceSettingsTabProps } from '../../../helpers/types' +import type { AppSettings, Hooks } from '../../../helpers/types' const { handleError } = injectNotificationManager() const { formatMessage } = useVIntl() -const props = defineProps() +const { instance } = injectInstanceSettings() const globalSettings = (await get().catch(handleError)) as AppSettings const overrideHooks = ref( - !!props.instance.hooks.pre_launch || - !!props.instance.hooks.wrapper || - !!props.instance.hooks.post_exit, + !!instance.value.hooks.pre_launch || + !!instance.value.hooks.wrapper || + !!instance.value.hooks.post_exit, ) -const hooks = ref(props.instance.hooks ?? globalSettings.hooks) +const hooks = ref(instance.value.hooks ?? globalSettings.hooks) const editProfileObject = computed(() => { const editProfile: { @@ -41,7 +42,7 @@ const editProfileObject = computed(() => { watch( [overrideHooks, hooks], async () => { - await edit(props.instance.path, editProfileObject.value) + await edit(instance.value.path, editProfileObject.value) }, { deep: true }, ) 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 9874e53468..3ef0be1651 100644 --- a/apps/app-frontend/src/components/ui/instance_settings/InstallationSettings.vue +++ b/apps/app-frontend/src/components/ui/instance_settings/InstallationSettings.vue @@ -27,17 +27,15 @@ import { update_repair_modrinth, } from '@/helpers/profile' import { get_game_versions, get_loaders } from '@/helpers/tags' +import { injectInstanceSettings } from '@/providers/instance-settings' -import type { InstanceSettingsTabProps, Manifest } from '../../../helpers/types' +import type { Manifest } from '../../../helpers/types' const { handleError } = injectNotificationManager() const { formatMessage } = useVIntl() const queryClient = useQueryClient() -const props = defineProps() -const emit = defineEmits<{ - unlinked: [] -}>() +const { instance, offline, isMinecraftServer, onUnlinked } = injectInstanceSettings() const [ fabric_versions, @@ -75,9 +73,9 @@ const [ ]) const { data: modpackInfo } = useQuery({ - queryKey: computed(() => ['linkedModpackInfo', props.instance.path]), - queryFn: () => get_linked_modpack_info(props.instance.path, 'must_revalidate'), - enabled: computed(() => !!props.instance.linked_data?.project_id && !props.offline), + queryKey: computed(() => ['linkedModpackInfo', instance.value.path]), + queryFn: () => get_linked_modpack_info(instance.value.path, 'must_revalidate'), + enabled: computed(() => !!instance.value.linked_data?.project_id && !offline), }) const repairing = ref(false) @@ -103,13 +101,13 @@ function getManifest(loader: string) { provideAppBackup({ async createBackup() { const allProfiles = await list() - const prefix = `${props.instance.name} - Backup #` + const prefix = `${instance.value.name} - Backup #` const existingNums = allProfiles .filter((p) => p.name.startsWith(prefix)) .map((p) => parseInt(p.name.slice(prefix.length), 10)) .filter((n) => !isNaN(n)) const nextNum = existingNums.length > 0 ? Math.max(...existingNums) + 1 : 1 - const newPath = await duplicate(props.instance.path) + const newPath = await duplicate(instance.value.path) await edit(newPath, { name: `${prefix}${nextNum}` }) }, }) @@ -120,30 +118,30 @@ provideInstallationSettings({ const rows = [ { label: formatMessage(commonMessages.platformLabel), - value: formatLoaderLabel(props.instance.loader), + value: formatLoaderLabel(instance.value.loader), }, { label: formatMessage(commonMessages.gameVersionLabel), - value: props.instance.game_version, + value: instance.value.game_version, }, ] - if (props.instance.loader !== 'vanilla' && props.instance.loader_version) { + if (instance.value.loader !== 'vanilla' && instance.value.loader_version) { rows.push({ label: formatMessage(messages.loaderVersion, { - loader: formatLoaderLabel(props.instance.loader), + loader: formatLoaderLabel(instance.value.loader), }), - value: props.instance.loader_version, + value: instance.value.loader_version, }) } return rows }), - isLinked: computed(() => !!props.instance.linked_data?.locked), + isLinked: computed(() => !!instance.value.linked_data?.locked), isBusy: computed( () => - props.instance.install_stage !== 'installed' || + instance.value.install_stage !== 'installed' || repairing.value || reinstalling.value || - !!props.offline, + !!offline, ), modpack: computed(() => { if (!modpackInfo.value) return null @@ -154,9 +152,9 @@ provideInstallationSettings({ versionNumber: modpackInfo.value.version?.version_number, } }), - currentPlatform: computed(() => props.instance.loader), - currentGameVersion: computed(() => props.instance.game_version), - currentLoaderVersion: computed(() => props.instance.loader_version ?? ''), + currentPlatform: computed(() => instance.value.loader), + currentGameVersion: computed(() => instance.value.game_version), + currentLoaderVersion: computed(() => instance.value.loader_version ?? ''), availablePlatforms: loaders?.value?.map((x) => x.name) ?? [], resolveGameVersions(loader, showSnapshots) { @@ -199,50 +197,50 @@ provideInstallationSettings({ if (platform !== 'vanilla' && loaderVersionId) { editProfile.loader_version = loaderVersionId } - await edit(props.instance.path, editProfile).catch(handleError) + await edit(instance.value.path, editProfile).catch(handleError) }, afterSave: async () => { - await install(props.instance.path, false).catch(handleError) + await install(instance.value.path, false).catch(handleError) trackEvent('InstanceRepair', { - loader: props.instance.loader, - game_version: props.instance.game_version, + loader: instance.value.loader, + game_version: instance.value.game_version, }) }, async repair() { repairing.value = true - await install(props.instance.path, true).catch(handleError) + await install(instance.value.path, true).catch(handleError) repairing.value = false trackEvent('InstanceRepair', { - loader: props.instance.loader, - game_version: props.instance.game_version, + loader: instance.value.loader, + game_version: instance.value.game_version, }) }, async reinstallModpack() { reinstalling.value = true - await update_repair_modrinth(props.instance.path).catch(handleError) + await update_repair_modrinth(instance.value.path).catch(handleError) reinstalling.value = false trackEvent('InstanceRepair', { - loader: props.instance.loader, - game_version: props.instance.game_version, + loader: instance.value.loader, + game_version: instance.value.game_version, }) }, async unlinkModpack() { - await edit(props.instance.path, { + await edit(instance.value.path, { linked_data: null as unknown as undefined, }) await queryClient.invalidateQueries({ - queryKey: ['linkedModpackInfo', props.instance.path], + queryKey: ['linkedModpackInfo', instance.value.path], }) - emit('unlinked') + onUnlinked() }, getCachedModpackVersions: () => null, async fetchModpackVersions() { - const versions = await get_project_versions(props.instance.linked_data!.project_id!).catch( + const versions = await get_project_versions(instance.value.linked_data!.project_id!).catch( handleError, ) return (versions ?? []) as Labrinth.Versions.v2.Version[] @@ -255,25 +253,25 @@ provideInstallationSettings({ }, async onModpackVersionConfirm(version) { - await update_managed_modrinth_version(props.instance.path, version.id) + await update_managed_modrinth_version(instance.value.path, version.id) await queryClient.invalidateQueries({ - queryKey: ['linkedModpackInfo', props.instance.path], + queryKey: ['linkedModpackInfo', instance.value.path], }) }, updaterModalProps: computed(() => ({ isApp: true, currentVersionId: - modpackInfo.value?.update_version_id ?? props.instance.linked_data?.version_id ?? '', + modpackInfo.value?.update_version_id ?? instance.value.linked_data?.version_id ?? '', projectIconUrl: modpackInfo.value?.project?.icon_url, projectName: modpackInfo.value?.project?.title ?? 'Modpack', - currentGameVersion: props.instance.game_version, - currentLoader: props.instance.loader, + currentGameVersion: instance.value.game_version, + currentLoader: instance.value.loader, })), isServer: false, isApp: true, - showModpackVersionActions: !props.isMinecraftServer, + showModpackVersionActions: !isMinecraftServer.value, repairing, reinstalling, }) diff --git a/apps/app-frontend/src/components/ui/instance_settings/JavaSettings.vue b/apps/app-frontend/src/components/ui/instance_settings/JavaSettings.vue index 0622de837a..0649621d1b 100644 --- a/apps/app-frontend/src/components/ui/instance_settings/JavaSettings.vue +++ b/apps/app-frontend/src/components/ui/instance_settings/JavaSettings.vue @@ -14,34 +14,35 @@ import JavaSelector from '@/components/ui/JavaSelector.vue' import useMemorySlider from '@/composables/useMemorySlider' import { edit, get_optimal_jre_key } from '@/helpers/profile' import { get } from '@/helpers/settings.ts' +import { injectInstanceSettings } from '@/providers/instance-settings' -import type { AppSettings, InstanceSettingsTabProps } from '../../../helpers/types' +import type { AppSettings } from '../../../helpers/types' const { handleError } = injectNotificationManager() const { formatMessage } = useVIntl() -const props = defineProps() +const { instance } = injectInstanceSettings() const globalSettings = (await get().catch(handleError)) as unknown as AppSettings -const overrideJavaInstall = ref(!!props.instance.java_path) -const optimalJava = readonly(await get_optimal_jre_key(props.instance.path).catch(handleError)) -const javaInstall = ref({ path: optimalJava.path ?? props.instance.java_path }) +const overrideJavaInstall = ref(!!instance.value.java_path) +const optimalJava = readonly(await get_optimal_jre_key(instance.value.path).catch(handleError)) +const javaInstall = ref({ path: optimalJava.path ?? instance.value.java_path }) -const overrideJavaArgs = ref((props.instance.extra_launch_args?.length ?? 0) > 0) +const overrideJavaArgs = ref((instance.value.extra_launch_args?.length ?? 0) > 0) const javaArgs = ref( - (props.instance.extra_launch_args ?? globalSettings.extra_launch_args).join(' '), + (instance.value.extra_launch_args ?? globalSettings.extra_launch_args).join(' '), ) -const overrideEnvVars = ref((props.instance.custom_env_vars?.length ?? 0) > 0) +const overrideEnvVars = ref((instance.value.custom_env_vars?.length ?? 0) > 0) const envVars = ref( - (props.instance.custom_env_vars ?? globalSettings.custom_env_vars) + (instance.value.custom_env_vars ?? globalSettings.custom_env_vars) .map((x) => x.join('=')) .join(' '), ) -const overrideMemorySettings = ref(!!props.instance.memory) -const memory = ref(props.instance.memory ?? globalSettings.memory) +const overrideMemorySettings = ref(!!instance.value.memory) +const memory = ref(instance.value.memory ?? globalSettings.memory) const { maxMemory, snapPoints } = (await useMemorySlider().catch(handleError)) as unknown as { maxMemory: number snapPoints: number[] @@ -79,7 +80,7 @@ watch( memory, ], async () => { - await edit(props.instance.path, editProfileObject.value) + await edit(instance.value.path, editProfileObject.value) }, { deep: true }, ) diff --git a/apps/app-frontend/src/components/ui/instance_settings/WindowSettings.vue b/apps/app-frontend/src/components/ui/instance_settings/WindowSettings.vue index 8bc1bc5fed..f5ba69568a 100644 --- a/apps/app-frontend/src/components/ui/instance_settings/WindowSettings.vue +++ b/apps/app-frontend/src/components/ui/instance_settings/WindowSettings.vue @@ -11,24 +11,25 @@ import { computed, type Ref, ref, watch } from 'vue' import { edit } from '@/helpers/profile' import { get } from '@/helpers/settings.ts' +import { injectInstanceSettings } from '@/providers/instance-settings' -import type { AppSettings, InstanceSettingsTabProps } from '../../../helpers/types' +import type { AppSettings } from '../../../helpers/types' const { handleError } = injectNotificationManager() const { formatMessage } = useVIntl() -const props = defineProps() +const { instance } = injectInstanceSettings() const globalSettings = (await get().catch(handleError)) as AppSettings const overrideWindowSettings = ref( - !!props.instance.game_resolution || !!props.instance.force_fullscreen, + !!instance.value.game_resolution || !!instance.value.force_fullscreen, ) const resolution: Ref<[number, number]> = ref( - props.instance.game_resolution ?? (globalSettings.game_resolution.slice() as [number, number]), + instance.value.game_resolution ?? (globalSettings.game_resolution.slice() as [number, number]), ) const fullscreenSetting: Ref = ref( - props.instance.force_fullscreen ?? globalSettings.force_fullscreen, + instance.value.force_fullscreen ?? globalSettings.force_fullscreen, ) const editProfileObject = computed(() => { @@ -47,7 +48,7 @@ const editProfileObject = computed(() => { watch( [overrideWindowSettings, resolution, fullscreenSetting], async () => { - await edit(props.instance.path, editProfileObject.value) + await edit(instance.value.path, editProfileObject.value) }, { deep: true }, ) diff --git a/apps/app-frontend/src/components/ui/modal/AppSettingsModal.vue b/apps/app-frontend/src/components/ui/modal/AppSettingsModal.vue index 3328fed4e6..9419d63749 100644 --- a/apps/app-frontend/src/components/ui/modal/AppSettingsModal.vue +++ b/apps/app-frontend/src/components/ui/modal/AppSettingsModal.vue @@ -6,12 +6,13 @@ import { LanguagesIcon, ModrinthIcon, PaintbrushIcon, - ReportIcon, SettingsIcon, ShieldIcon, + ToggleRightIcon, } from '@modrinth/assets' import { commonMessages, + commonSettingsMessages, defineMessage, defineMessages, ProgressBar, @@ -20,9 +21,8 @@ import { } from '@modrinth/ui' import { getVersion } from '@tauri-apps/api/app' import { platform as getOsPlatform, version as getOsVersion } from '@tauri-apps/plugin-os' -import { computed, ref, watch } from 'vue' +import { ref, watch } from 'vue' -import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue' import AppearanceSettings from '@/components/ui/settings/AppearanceSettings.vue' import DefaultInstanceSettings from '@/components/ui/settings/DefaultInstanceSettings.vue' import FeatureFlagSettings from '@/components/ui/settings/FeatureFlagSettings.vue' @@ -96,25 +96,20 @@ const tabs = [ content: ResourceManagementSettings, }, { - name: defineMessage({ - id: 'app.settings.tabs.feature-flags', - defaultMessage: 'Feature flags', - }), - icon: ReportIcon, + name: commonSettingsMessages.featureFlags, + icon: ToggleRightIcon, content: FeatureFlagSettings, developerOnly: true, }, ] -const modal = ref() +const modal = ref | null>(null) function show() { - modal.value.show() + modal.value?.show() } -const isOpen = computed(() => modal.value?.isOpen) - -defineExpose({ show, isOpen }) +defineExpose({ show }) const { progress, version: downloadingVersion } = injectAppUpdateDownloadProgress() @@ -138,8 +133,8 @@ function devModeCount() { settings.value.developer_mode = !!themeStore.devMode devModeCounter.value = 0 - if (!themeStore.devMode && tabs[modal.value.selectedTab].developerOnly) { - modal.value.setTab(0) + if (!themeStore.devMode && tabs[modal.value!.selectedTab].developerOnly) { + modal.value!.setTab(0) } } } @@ -152,49 +147,46 @@ const messages = defineMessages({ }) diff --git a/apps/app-frontend/src/components/ui/modal/InstanceSettingsModal.vue b/apps/app-frontend/src/components/ui/modal/InstanceSettingsModal.vue index 3543a19b34..76532a30c5 100644 --- a/apps/app-frontend/src/components/ui/modal/InstanceSettingsModal.vue +++ b/apps/app-frontend/src/components/ui/modal/InstanceSettingsModal.vue @@ -12,14 +12,13 @@ import { Avatar, commonMessages, defineMessage, - NewModal, TabbedModal, type TabbedModalTab, useVIntl, } from '@modrinth/ui' import { useQueryClient } from '@tanstack/vue-query' import { convertFileSrc } from '@tauri-apps/api/core' -import { computed, nextTick, ref, useTemplateRef, watch } from 'vue' +import { computed, nextTick, ref, watch } from 'vue' import GeneralSettings from '@/components/ui/instance_settings/GeneralSettings.vue' import HooksSettings from '@/components/ui/instance_settings/HooksSettings.vue' @@ -28,12 +27,16 @@ import JavaSettings from '@/components/ui/instance_settings/JavaSettings.vue' import WindowSettings from '@/components/ui/instance_settings/WindowSettings.vue' import { get_project_v3 } from '@/helpers/cache' import { get_linked_modpack_info } from '@/helpers/profile' +import { provideInstanceSettings } from '@/providers/instance-settings' -import type { InstanceSettingsTabProps } from '../../../helpers/types' +import type { GameInstance } from '../../../helpers/types' const { formatMessage } = useVIntl() -const props = defineProps() +const props = defineProps<{ + instance: GameInstance + offline?: boolean +}>() const emit = defineEmits<{ unlinked: [] }>() @@ -41,6 +44,15 @@ const emit = defineEmits<{ const isMinecraftServer = ref(false) const handleUnlinked = () => emit('unlinked') +const instanceRef = computed(() => props.instance) + +provideInstanceSettings({ + instance: instanceRef, + offline: props.offline, + isMinecraftServer, + onUnlinked: handleUnlinked, +}) + watch( () => props.instance, (instance) => { @@ -58,7 +70,7 @@ watch( { immediate: true }, ) -const tabs = computed[]>(() => [ +const tabs = computed(() => [ { name: defineMessage({ id: 'instance.settings.tabs.general', @@ -102,8 +114,7 @@ const tabs = computed[]>(() => [ ]) const queryClient = useQueryClient() -const modal = ref() -const tabbedModal = useTemplateRef('tabbedModal') +const tabbedModal = ref | null>(null) function show(tabIndex?: number) { if (props.instance.linked_data?.project_id) { @@ -112,7 +123,7 @@ function show(tabIndex?: number) { queryFn: () => get_linked_modpack_info(props.instance.path, 'stale_while_revalidate'), }) } - modal.value.show() + tabbedModal.value?.show() if (tabIndex !== undefined) { nextTick(() => tabbedModal.value?.setTab(tabIndex)) } @@ -121,8 +132,9 @@ function show(tabIndex?: number) { defineExpose({ show }) - - - + 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 0000000000..daf238793f --- /dev/null +++ b/apps/app-frontend/src/components/ui/modal/ModpackAlreadyInstalledModal.vue @@ -0,0 +1,102 @@ + + + diff --git a/apps/app-frontend/src/components/ui/settings/FeatureFlagSettings.vue b/apps/app-frontend/src/components/ui/settings/FeatureFlagSettings.vue index 679dfc48eb..dadcab73be 100644 --- a/apps/app-frontend/src/components/ui/settings/FeatureFlagSettings.vue +++ b/apps/app-frontend/src/components/ui/settings/FeatureFlagSettings.vue @@ -1,5 +1,5 @@