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 979ca33a3f..e30bead729 100644
--- a/apps/app-frontend/src/components/ui/instance_settings/GeneralSettings.vue
+++ b/apps/app-frontend/src/components/ui/instance_settings/GeneralSettings.vue
@@ -4,12 +4,14 @@ import {
Avatar,
ButtonStyled,
Checkbox,
+ Chips,
defineMessages,
injectNotificationManager,
OverflowMenu,
StyledInput,
useVIntl,
} from '@modrinth/ui'
+import { useQueryClient } from '@tanstack/vue-query'
import { convertFileSrc } from '@tauri-apps/api/core'
import { open } from '@tauri-apps/plugin-dialog'
import { computed, type Ref, ref, watch } from 'vue'
@@ -25,14 +27,22 @@ import type { GameInstance } from '../../../helpers/types'
const { handleError } = injectNotificationManager()
const { formatMessage } = useVIntl()
const router = useRouter()
+const queryClient = useQueryClient()
const deleteConfirmModal = ref()
const { instance } = injectInstanceSettings()
+type ReleaseChannel = GameInstance['preferred_update_channel']
+const releaseChannelOptions: ReleaseChannel[] = ['release', 'beta', 'alpha']
const title = ref(instance.value.name)
const icon: Ref = ref(instance.value.icon_path)
const groups = ref([...instance.value.groups])
+const savingReleaseChannel = ref(false)
+const selectedReleaseChannel = ref(instance.value.preferred_update_channel)
+const releaseChannelDisabledItems = computed(() =>
+ savingReleaseChannel.value ? [...releaseChannelOptions] : [],
+)
const newCategoryInput = ref('')
@@ -51,6 +61,52 @@ const availableGroups = computed(() => [
...new Set([...allInstances.value.flatMap((instance) => instance.groups), ...groups.value]),
])
+function formatReleaseChannelLabel(channel: ReleaseChannel) {
+ switch (channel) {
+ case 'release':
+ return formatMessage(messages.updateChannelRelease)
+ case 'beta':
+ return formatMessage(messages.updateChannelBeta)
+ case 'alpha':
+ return formatMessage(messages.updateChannelAlpha)
+ }
+}
+
+function formatReleaseChannelDescription(channel: ReleaseChannel) {
+ switch (channel) {
+ case 'release':
+ return formatMessage(messages.updateChannelReleaseDescription)
+ case 'beta':
+ return formatMessage(messages.updateChannelBetaDescription)
+ case 'alpha':
+ return formatMessage(messages.updateChannelAlphaDescription)
+ }
+}
+
+watch(
+ () => [instance.value.path, instance.value.preferred_update_channel] as const,
+ () => {
+ if (!savingReleaseChannel.value) {
+ selectedReleaseChannel.value = instance.value.preferred_update_channel
+ }
+ },
+)
+
+watch(selectedReleaseChannel, async (channel, previousChannel) => {
+ const previousReleaseChannel = previousChannel ?? instance.value.preferred_update_channel
+ if (channel === instance.value.preferred_update_channel) return
+
+ savingReleaseChannel.value = true
+ const profilePath = instance.value.path
+ await edit(profilePath, { preferred_update_channel: channel })
+ .then(() => queryClient.invalidateQueries({ queryKey: ['linkedModpackInfo', profilePath] }))
+ .catch((error) => {
+ selectedReleaseChannel.value = previousReleaseChannel
+ handleError(error)
+ })
+ savingReleaseChannel.value = false
+})
+
async function resetIcon() {
icon.value = undefined
await edit_icon(instance.value.path, null).catch(handleError)
@@ -175,6 +231,38 @@ const messages = defineMessages({
id: 'instance.settings.tabs.general.duplicate-button',
defaultMessage: 'Duplicate',
},
+ updateChannel: {
+ id: 'instance.settings.tabs.general.update-channel',
+ defaultMessage: 'Update channel',
+ },
+ updateChannelReleaseDescription: {
+ id: 'instance.settings.tabs.general.update-channel.release.description',
+ defaultMessage: 'Only release versions will be shown as available updates.',
+ },
+ updateChannelBetaDescription: {
+ id: 'instance.settings.tabs.general.update-channel.beta.description',
+ defaultMessage: 'Release and beta versions will be shown as available updates.',
+ },
+ updateChannelAlphaDescription: {
+ id: 'instance.settings.tabs.general.update-channel.alpha.description',
+ defaultMessage: 'Release, beta, and alpha versions will be shown as available updates.',
+ },
+ updateChannelRelease: {
+ id: 'instance.settings.tabs.general.update-channel.release',
+ defaultMessage: 'Release',
+ },
+ updateChannelBeta: {
+ id: 'instance.settings.tabs.general.update-channel.beta',
+ defaultMessage: 'Beta',
+ },
+ updateChannelAlpha: {
+ id: 'instance.settings.tabs.general.update-channel.alpha',
+ defaultMessage: 'Alpha',
+ },
+ selectUpdateChannelAriaLabel: {
+ id: 'instance.settings.tabs.general.update-channel.select',
+ defaultMessage: 'Select update channel',
+ },
deleteInstance: {
id: 'instance.settings.tabs.general.delete',
defaultMessage: 'Delete instance',
@@ -304,6 +392,23 @@ const messages = defineMessages({
+
+
+ {{ formatMessage(messages.updateChannel) }}
+
+
+
+ {{ formatReleaseChannelDescription(selectedReleaseChannel) }}
+
+
+
{{ formatMessage(messages.deleteInstance) }}
diff --git a/apps/app-frontend/src/helpers/types.d.ts b/apps/app-frontend/src/helpers/types.d.ts
index 7c5f1d25e7..143997702f 100644
--- a/apps/app-frontend/src/helpers/types.d.ts
+++ b/apps/app-frontend/src/helpers/types.d.ts
@@ -14,6 +14,7 @@ export type GameInstance = {
groups: string[]
linked_data?: LinkedData
+ preferred_update_channel: ReleaseChannel
created: Date
modified: Date
@@ -46,6 +47,8 @@ type LinkedData = {
locked: boolean
}
+type ReleaseChannel = 'release' | 'beta' | 'alpha'
+
export type InstanceLoader = 'vanilla' | 'forge' | 'fabric' | 'quilt' | 'neoforge'
type ContentFile = {
diff --git a/apps/app-frontend/src/locales/en-US/index.json b/apps/app-frontend/src/locales/en-US/index.json
index 5d909a5777..f0345af27b 100644
--- a/apps/app-frontend/src/locales/en-US/index.json
+++ b/apps/app-frontend/src/locales/en-US/index.json
@@ -704,6 +704,30 @@
"instance.settings.tabs.general.name": {
"message": "Name"
},
+ "instance.settings.tabs.general.update-channel": {
+ "message": "Update channel"
+ },
+ "instance.settings.tabs.general.update-channel.alpha": {
+ "message": "Alpha"
+ },
+ "instance.settings.tabs.general.update-channel.alpha.description": {
+ "message": "Release, beta, and alpha versions will be shown as available updates."
+ },
+ "instance.settings.tabs.general.update-channel.beta": {
+ "message": "Beta"
+ },
+ "instance.settings.tabs.general.update-channel.beta.description": {
+ "message": "Release and beta versions will be shown as available updates."
+ },
+ "instance.settings.tabs.general.update-channel.release": {
+ "message": "Release"
+ },
+ "instance.settings.tabs.general.update-channel.release.description": {
+ "message": "Only release versions will be shown as available updates."
+ },
+ "instance.settings.tabs.general.update-channel.select": {
+ "message": "Select update channel"
+ },
"instance.settings.tabs.hooks": {
"message": "Launch hooks"
},
diff --git a/apps/app-frontend/src/pages/instance/Mods.vue b/apps/app-frontend/src/pages/instance/Mods.vue
index f64101f56a..d3a8004e07 100644
--- a/apps/app-frontend/src/pages/instance/Mods.vue
+++ b/apps/app-frontend/src/pages/instance/Mods.vue
@@ -1194,6 +1194,15 @@ watch(
},
)
+watch(
+ () => props.instance?.preferred_update_channel,
+ async (newValue, oldValue) => {
+ if (newValue !== oldValue) {
+ await initProjects('must_revalidate')
+ }
+ },
+)
+
onUnmounted(() => {
isUnmounted = true
removeBeforeEach()
diff --git a/apps/app/src/api/profile.rs b/apps/app/src/api/profile.rs
index d33d3a3f2b..e35a3675c1 100644
--- a/apps/app/src/api/profile.rs
+++ b/apps/app/src/api/profile.rs
@@ -385,6 +385,7 @@ pub struct EditProfile {
with = "serde_with::rust::double_option"
)]
pub linked_data: Option