You've already forked AstralRinth
feat: release channels instance setting (#6252)
* feat: rough release channels impl draft * feat: move to bottom + lint * fix: invalidate content queries on channel change * fix: change to chips * fix: lint * fix: copy
This commit is contained in:
@@ -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<string | undefined> = ref(instance.value.icon_path)
|
||||
const groups = ref([...instance.value.groups])
|
||||
const savingReleaseChannel = ref(false)
|
||||
const selectedReleaseChannel = ref<ReleaseChannel>(instance.value.preferred_update_channel)
|
||||
const releaseChannelDisabledItems = computed<ReleaseChannel[]>(() =>
|
||||
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({
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2.5 mt-6">
|
||||
<h2 class="m-0 text-lg font-semibold text-contrast block">
|
||||
{{ formatMessage(messages.updateChannel) }}
|
||||
</h2>
|
||||
<Chips
|
||||
v-model="selectedReleaseChannel"
|
||||
:items="releaseChannelOptions"
|
||||
:format-label="formatReleaseChannelLabel"
|
||||
:capitalize="false"
|
||||
:disabled-items="releaseChannelDisabledItems"
|
||||
:aria-label="formatMessage(messages.selectUpdateChannelAriaLabel)"
|
||||
/>
|
||||
<p class="m-0">
|
||||
{{ formatReleaseChannelDescription(selectedReleaseChannel) }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2.5 mt-6">
|
||||
<h2 id="delete-instance-label" class="m-0 text-lg font-semibold text-contrast block">
|
||||
{{ formatMessage(messages.deleteInstance) }}
|
||||
|
||||
+3
@@ -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 = {
|
||||
|
||||
@@ -713,6 +713,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"
|
||||
},
|
||||
|
||||
@@ -1227,6 +1227,15 @@ watch(
|
||||
},
|
||||
)
|
||||
|
||||
watch(
|
||||
() => props.instance?.preferred_update_channel,
|
||||
async (newValue, oldValue) => {
|
||||
if (newValue !== oldValue) {
|
||||
await initProjects('must_revalidate')
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
onUnmounted(() => {
|
||||
isUnmounted = true
|
||||
removeBeforeEach()
|
||||
|
||||
@@ -385,6 +385,7 @@ pub struct EditProfile {
|
||||
with = "serde_with::rust::double_option"
|
||||
)]
|
||||
pub linked_data: Option<Option<LinkedData>>,
|
||||
pub preferred_update_channel: Option<ReleaseChannel>,
|
||||
|
||||
#[serde(
|
||||
default,
|
||||
@@ -449,6 +450,11 @@ pub async fn profile_edit(path: &str, edit_profile: EditProfile) -> Result<()> {
|
||||
if let Some(linked_data) = edit_profile.linked_data.clone() {
|
||||
prof.linked_data = linked_data;
|
||||
}
|
||||
if let Some(preferred_update_channel) =
|
||||
edit_profile.preferred_update_channel
|
||||
{
|
||||
prof.preferred_update_channel = preferred_update_channel;
|
||||
}
|
||||
if let Some(groups) = edit_profile.groups.clone() {
|
||||
prof.groups = groups;
|
||||
}
|
||||
|
||||
Generated
+12
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n INSERT INTO profiles (\n path, install_stage, name, icon_path,\n game_version, mod_loader, mod_loader_version,\n groups,\n linked_project_id, linked_version_id, locked, preferred_update_channel,\n created, modified, last_played,\n submitted_time_played, recent_time_played,\n override_java_path, override_extra_launch_args, override_custom_env_vars,\n override_mc_memory_max, override_mc_force_fullscreen, override_mc_game_resolution_x, override_mc_game_resolution_y,\n override_hook_pre_launch, override_hook_wrapper, override_hook_post_exit,\n protocol_version, launcher_feature_version\n )\n VALUES (\n $1, $2, $3, $4,\n $5, $6, $7,\n jsonb($8),\n $9, $10, $11, $12,\n $13, $14, $15,\n $16, $17,\n $18, jsonb($19), jsonb($20),\n $21, $22, $23, $24,\n $25, $26, $27,\n $28, $29\n )\n ON CONFLICT (path) DO UPDATE SET\n install_stage = $2,\n name = $3,\n icon_path = $4,\n\n game_version = $5,\n mod_loader = $6,\n mod_loader_version = $7,\n\n groups = jsonb($8),\n\n linked_project_id = $9,\n linked_version_id = $10,\n locked = $11,\n preferred_update_channel = $12,\n\n created = $13,\n modified = $14,\n last_played = $15,\n\n submitted_time_played = $16,\n recent_time_played = $17,\n\n override_java_path = $18,\n override_extra_launch_args = jsonb($19),\n override_custom_env_vars = jsonb($20),\n override_mc_memory_max = $21,\n override_mc_force_fullscreen = $22,\n override_mc_game_resolution_x = $23,\n override_mc_game_resolution_y = $24,\n\n override_hook_pre_launch = $25,\n override_hook_wrapper = $26,\n override_hook_post_exit = $27,\n\n protocol_version = $28,\n launcher_feature_version = $29\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 29
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "22202dacf6b5bade90c909cb4a663a1388ef538e4b80680bce4b668b182b4f42"
|
||||
}
|
||||
Generated
-12
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n INSERT INTO profiles (\n path, install_stage, name, icon_path,\n game_version, mod_loader, mod_loader_version,\n groups,\n linked_project_id, linked_version_id, locked,\n created, modified, last_played,\n submitted_time_played, recent_time_played,\n override_java_path, override_extra_launch_args, override_custom_env_vars,\n override_mc_memory_max, override_mc_force_fullscreen, override_mc_game_resolution_x, override_mc_game_resolution_y,\n override_hook_pre_launch, override_hook_wrapper, override_hook_post_exit,\n protocol_version, launcher_feature_version\n )\n VALUES (\n $1, $2, $3, $4,\n $5, $6, $7,\n jsonb($8),\n $9, $10, $11,\n $12, $13, $14,\n $15, $16,\n $17, jsonb($18), jsonb($19),\n $20, $21, $22, $23,\n $24, $25, $26,\n $27, $28\n )\n ON CONFLICT (path) DO UPDATE SET\n install_stage = $2,\n name = $3,\n icon_path = $4,\n\n game_version = $5,\n mod_loader = $6,\n mod_loader_version = $7,\n\n groups = jsonb($8),\n\n linked_project_id = $9,\n linked_version_id = $10,\n locked = $11,\n\n created = $12,\n modified = $13,\n last_played = $14,\n\n submitted_time_played = $15,\n recent_time_played = $16,\n\n override_java_path = $17,\n override_extra_launch_args = jsonb($18),\n override_custom_env_vars = jsonb($19),\n override_mc_memory_max = $20,\n override_mc_force_fullscreen = $21,\n override_mc_game_resolution_x = $22,\n override_mc_game_resolution_y = $23,\n\n override_hook_pre_launch = $24,\n override_hook_wrapper = $25,\n override_hook_post_exit = $26,\n\n protocol_version = $27,\n launcher_feature_version = $28\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 28
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "27283e20fc86c941c7d6d09259d8f4ec2e248f751f98140f77bea4f9d5971ef1"
|
||||
}
|
||||
+29
-23
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n SELECT\n path, install_stage, launcher_feature_version, name, icon_path,\n game_version, protocol_version, mod_loader, mod_loader_version,\n json(groups) as \"groups!: serde_json::Value\",\n linked_project_id, linked_version_id, locked,\n created, modified, last_played,\n submitted_time_played, recent_time_played,\n override_java_path,\n json(override_extra_launch_args) as \"override_extra_launch_args!: serde_json::Value\", json(override_custom_env_vars) as \"override_custom_env_vars!: serde_json::Value\",\n override_mc_memory_max, override_mc_force_fullscreen, override_mc_game_resolution_x, override_mc_game_resolution_y,\n override_hook_pre_launch, override_hook_wrapper, override_hook_post_exit\n FROM profiles\n WHERE path IN (SELECT value FROM json_each($1))",
|
||||
"query": "\n SELECT\n path, install_stage, launcher_feature_version, name, icon_path,\n game_version, protocol_version, mod_loader, mod_loader_version,\n json(groups) as \"groups!: serde_json::Value\",\n linked_project_id, linked_version_id, locked, preferred_update_channel,\n created, modified, last_played,\n submitted_time_played, recent_time_played,\n override_java_path,\n json(override_extra_launch_args) as \"override_extra_launch_args!: serde_json::Value\", json(override_custom_env_vars) as \"override_custom_env_vars!: serde_json::Value\",\n override_mc_memory_max, override_mc_force_fullscreen, override_mc_game_resolution_x, override_mc_game_resolution_y,\n override_hook_pre_launch, override_hook_wrapper, override_hook_post_exit\n FROM profiles\n WHERE 1=$1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -69,79 +69,84 @@
|
||||
"type_info": "Integer"
|
||||
},
|
||||
{
|
||||
"name": "created",
|
||||
"name": "preferred_update_channel",
|
||||
"ordinal": 13,
|
||||
"type_info": "Integer"
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "modified",
|
||||
"name": "created",
|
||||
"ordinal": 14,
|
||||
"type_info": "Integer"
|
||||
},
|
||||
{
|
||||
"name": "last_played",
|
||||
"name": "modified",
|
||||
"ordinal": 15,
|
||||
"type_info": "Integer"
|
||||
},
|
||||
{
|
||||
"name": "submitted_time_played",
|
||||
"name": "last_played",
|
||||
"ordinal": 16,
|
||||
"type_info": "Integer"
|
||||
},
|
||||
{
|
||||
"name": "recent_time_played",
|
||||
"name": "submitted_time_played",
|
||||
"ordinal": 17,
|
||||
"type_info": "Integer"
|
||||
},
|
||||
{
|
||||
"name": "override_java_path",
|
||||
"name": "recent_time_played",
|
||||
"ordinal": 18,
|
||||
"type_info": "Integer"
|
||||
},
|
||||
{
|
||||
"name": "override_java_path",
|
||||
"ordinal": 19,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "override_extra_launch_args!: serde_json::Value",
|
||||
"ordinal": 19,
|
||||
"type_info": "Null"
|
||||
},
|
||||
{
|
||||
"name": "override_custom_env_vars!: serde_json::Value",
|
||||
"ordinal": 20,
|
||||
"type_info": "Null"
|
||||
},
|
||||
{
|
||||
"name": "override_mc_memory_max",
|
||||
"name": "override_custom_env_vars!: serde_json::Value",
|
||||
"ordinal": 21,
|
||||
"type_info": "Integer"
|
||||
"type_info": "Null"
|
||||
},
|
||||
{
|
||||
"name": "override_mc_force_fullscreen",
|
||||
"name": "override_mc_memory_max",
|
||||
"ordinal": 22,
|
||||
"type_info": "Integer"
|
||||
},
|
||||
{
|
||||
"name": "override_mc_game_resolution_x",
|
||||
"name": "override_mc_force_fullscreen",
|
||||
"ordinal": 23,
|
||||
"type_info": "Integer"
|
||||
},
|
||||
{
|
||||
"name": "override_mc_game_resolution_y",
|
||||
"name": "override_mc_game_resolution_x",
|
||||
"ordinal": 24,
|
||||
"type_info": "Integer"
|
||||
},
|
||||
{
|
||||
"name": "override_hook_pre_launch",
|
||||
"name": "override_mc_game_resolution_y",
|
||||
"ordinal": 25,
|
||||
"type_info": "Text"
|
||||
"type_info": "Integer"
|
||||
},
|
||||
{
|
||||
"name": "override_hook_wrapper",
|
||||
"name": "override_hook_pre_launch",
|
||||
"ordinal": 26,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "override_hook_post_exit",
|
||||
"name": "override_hook_wrapper",
|
||||
"ordinal": 27,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "override_hook_post_exit",
|
||||
"ordinal": 28,
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
@@ -163,6 +168,7 @@
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
@@ -178,5 +184,5 @@
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "6a434cc55635b6e325e9e5f06d21b787af281db4402c8ff45fe6a77b5be6c929"
|
||||
"hash": "be21bfd7c36fd8c69dfffe0299eeb7e57590dc75b721dd823d00572f428d0bc5"
|
||||
}
|
||||
+29
-23
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n SELECT\n path, install_stage, launcher_feature_version, name, icon_path,\n game_version, protocol_version, mod_loader, mod_loader_version,\n json(groups) as \"groups!: serde_json::Value\",\n linked_project_id, linked_version_id, locked,\n created, modified, last_played,\n submitted_time_played, recent_time_played,\n override_java_path,\n json(override_extra_launch_args) as \"override_extra_launch_args!: serde_json::Value\", json(override_custom_env_vars) as \"override_custom_env_vars!: serde_json::Value\",\n override_mc_memory_max, override_mc_force_fullscreen, override_mc_game_resolution_x, override_mc_game_resolution_y,\n override_hook_pre_launch, override_hook_wrapper, override_hook_post_exit\n FROM profiles\n WHERE 1=$1",
|
||||
"query": "\n SELECT\n path, install_stage, launcher_feature_version, name, icon_path,\n game_version, protocol_version, mod_loader, mod_loader_version,\n json(groups) as \"groups!: serde_json::Value\",\n linked_project_id, linked_version_id, locked, preferred_update_channel,\n created, modified, last_played,\n submitted_time_played, recent_time_played,\n override_java_path,\n json(override_extra_launch_args) as \"override_extra_launch_args!: serde_json::Value\", json(override_custom_env_vars) as \"override_custom_env_vars!: serde_json::Value\",\n override_mc_memory_max, override_mc_force_fullscreen, override_mc_game_resolution_x, override_mc_game_resolution_y,\n override_hook_pre_launch, override_hook_wrapper, override_hook_post_exit\n FROM profiles\n WHERE path IN (SELECT value FROM json_each($1))",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -69,79 +69,84 @@
|
||||
"type_info": "Integer"
|
||||
},
|
||||
{
|
||||
"name": "created",
|
||||
"name": "preferred_update_channel",
|
||||
"ordinal": 13,
|
||||
"type_info": "Integer"
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "modified",
|
||||
"name": "created",
|
||||
"ordinal": 14,
|
||||
"type_info": "Integer"
|
||||
},
|
||||
{
|
||||
"name": "last_played",
|
||||
"name": "modified",
|
||||
"ordinal": 15,
|
||||
"type_info": "Integer"
|
||||
},
|
||||
{
|
||||
"name": "submitted_time_played",
|
||||
"name": "last_played",
|
||||
"ordinal": 16,
|
||||
"type_info": "Integer"
|
||||
},
|
||||
{
|
||||
"name": "recent_time_played",
|
||||
"name": "submitted_time_played",
|
||||
"ordinal": 17,
|
||||
"type_info": "Integer"
|
||||
},
|
||||
{
|
||||
"name": "override_java_path",
|
||||
"name": "recent_time_played",
|
||||
"ordinal": 18,
|
||||
"type_info": "Integer"
|
||||
},
|
||||
{
|
||||
"name": "override_java_path",
|
||||
"ordinal": 19,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "override_extra_launch_args!: serde_json::Value",
|
||||
"ordinal": 19,
|
||||
"type_info": "Null"
|
||||
},
|
||||
{
|
||||
"name": "override_custom_env_vars!: serde_json::Value",
|
||||
"ordinal": 20,
|
||||
"type_info": "Null"
|
||||
},
|
||||
{
|
||||
"name": "override_mc_memory_max",
|
||||
"name": "override_custom_env_vars!: serde_json::Value",
|
||||
"ordinal": 21,
|
||||
"type_info": "Integer"
|
||||
"type_info": "Null"
|
||||
},
|
||||
{
|
||||
"name": "override_mc_force_fullscreen",
|
||||
"name": "override_mc_memory_max",
|
||||
"ordinal": 22,
|
||||
"type_info": "Integer"
|
||||
},
|
||||
{
|
||||
"name": "override_mc_game_resolution_x",
|
||||
"name": "override_mc_force_fullscreen",
|
||||
"ordinal": 23,
|
||||
"type_info": "Integer"
|
||||
},
|
||||
{
|
||||
"name": "override_mc_game_resolution_y",
|
||||
"name": "override_mc_game_resolution_x",
|
||||
"ordinal": 24,
|
||||
"type_info": "Integer"
|
||||
},
|
||||
{
|
||||
"name": "override_hook_pre_launch",
|
||||
"name": "override_mc_game_resolution_y",
|
||||
"ordinal": 25,
|
||||
"type_info": "Text"
|
||||
"type_info": "Integer"
|
||||
},
|
||||
{
|
||||
"name": "override_hook_wrapper",
|
||||
"name": "override_hook_pre_launch",
|
||||
"ordinal": 26,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "override_hook_post_exit",
|
||||
"name": "override_hook_wrapper",
|
||||
"ordinal": 27,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "override_hook_post_exit",
|
||||
"ordinal": 28,
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
@@ -163,6 +168,7 @@
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
@@ -178,5 +184,5 @@
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "c108849d77c7627d6a11d5be34984938c41283e6091a1301fc3ca0b355ffcfa9"
|
||||
"hash": "de1887a95766303d16ddcad7fe7f34e0a64101ead329bcc88e55e31b32578a97"
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE profiles
|
||||
ADD COLUMN preferred_update_channel TEXT NOT NULL DEFAULT 'release';
|
||||
@@ -37,6 +37,7 @@ pub mod prelude {
|
||||
jre, metadata, minecraft_auth, mr_auth, pack, process,
|
||||
profile::{self, Profile, create},
|
||||
settings,
|
||||
state::ReleaseChannel,
|
||||
util::{
|
||||
io::{IOError, canonicalize},
|
||||
network::{is_network_metered, tcp_listen_any_loopback},
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
//! Theseus profile management interface
|
||||
use crate::launcher::get_loader_version_from_profile;
|
||||
use crate::settings::Hooks;
|
||||
use crate::state::{LauncherFeatureVersion, LinkedData, ProfileInstallStage};
|
||||
use crate::state::{
|
||||
LauncherFeatureVersion, LinkedData, ProfileInstallStage, ReleaseChannel,
|
||||
};
|
||||
use crate::util::io::{self, canonicalize};
|
||||
use crate::{ErrorKind, pack, profile};
|
||||
pub use crate::{State, state::Profile};
|
||||
@@ -83,6 +85,7 @@ pub async fn profile_create(
|
||||
loader_version: loader.map(|x| x.id),
|
||||
groups: Vec::new(),
|
||||
linked_data,
|
||||
preferred_update_channel: ReleaseChannel::Release,
|
||||
created: Utc::now(),
|
||||
modified: Utc::now(),
|
||||
last_played: None,
|
||||
|
||||
@@ -259,9 +259,77 @@ pub struct CachedFileUpdate {
|
||||
pub hash: String,
|
||||
pub game_version: String,
|
||||
pub loaders: Vec<String>,
|
||||
pub channel_policy: String,
|
||||
pub update_version_id: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum ReleaseChannel {
|
||||
Release,
|
||||
Beta,
|
||||
Alpha,
|
||||
}
|
||||
|
||||
impl ReleaseChannel {
|
||||
pub fn key(self) -> &'static str {
|
||||
match self {
|
||||
Self::Release => "release",
|
||||
Self::Beta => "beta",
|
||||
Self::Alpha => "alpha",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_key(key: &str) -> Self {
|
||||
match key {
|
||||
"alpha" => Self::Alpha,
|
||||
"all" => Self::Alpha,
|
||||
"beta" => Self::Beta,
|
||||
_ => Self::Release,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_version_type(version_type: &str) -> Self {
|
||||
match version_type {
|
||||
"alpha" => Self::Alpha,
|
||||
"beta" => Self::Beta,
|
||||
_ => Self::Release,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn least_stable(self, other: Self) -> Self {
|
||||
if self.instability_rank() >= other.instability_rank() {
|
||||
self
|
||||
} else {
|
||||
other
|
||||
}
|
||||
}
|
||||
|
||||
fn instability_rank(self) -> u8 {
|
||||
match self {
|
||||
Self::Release => 0,
|
||||
Self::Beta => 1,
|
||||
Self::Alpha => 2,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn version_type_fallbacks(self) -> Vec<Vec<&'static str>> {
|
||||
match self {
|
||||
Self::Release => {
|
||||
vec![vec!["release"], vec!["beta"], vec!["alpha"]]
|
||||
}
|
||||
Self::Beta => {
|
||||
vec![vec!["release", "beta"], vec!["alpha"]]
|
||||
}
|
||||
Self::Alpha => vec![vec!["release", "beta", "alpha"]],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn default_file_update_channel_policy() -> String {
|
||||
ReleaseChannel::Alpha.key().to_string()
|
||||
}
|
||||
|
||||
/// Migrates old cache entries that stored `"loader": "forge"` (singular string)
|
||||
/// to the current `"loaders": ["forge"]` (array) format.
|
||||
/// SEE: https://github.com/modrinth/code/issues/5562
|
||||
@@ -278,6 +346,8 @@ impl<'de> serde::Deserialize<'de> for CachedFileUpdate {
|
||||
loaders: Option<Vec<String>>,
|
||||
#[serde(default)]
|
||||
loader: Option<String>,
|
||||
#[serde(default = "default_file_update_channel_policy")]
|
||||
channel_policy: String,
|
||||
update_version_id: String,
|
||||
}
|
||||
|
||||
@@ -290,6 +360,7 @@ impl<'de> serde::Deserialize<'de> for CachedFileUpdate {
|
||||
hash: helper.hash,
|
||||
game_version: helper.game_version,
|
||||
loaders,
|
||||
channel_policy: helper.channel_policy,
|
||||
update_version_id: helper.update_version_id,
|
||||
})
|
||||
}
|
||||
@@ -599,9 +670,10 @@ impl CacheValue {
|
||||
}
|
||||
CacheValue::FileUpdate(hash) => {
|
||||
format!(
|
||||
"{}-{}-{}",
|
||||
"{}-{}-{}-{}",
|
||||
hash.hash,
|
||||
hash.loaders.join("+"),
|
||||
hash.channel_policy,
|
||||
hash.game_version
|
||||
)
|
||||
}
|
||||
@@ -1453,20 +1525,46 @@ impl CachedEntry {
|
||||
let mut vals = Vec::new();
|
||||
|
||||
// TODO: switch to update individual once back-end route exists
|
||||
let mut filtered_keys: Vec<((String, String), Vec<String>)> =
|
||||
Vec::new();
|
||||
let mut filtered_keys: Vec<(
|
||||
(String, String, String),
|
||||
Vec<String>,
|
||||
)> = Vec::new();
|
||||
keys.iter().for_each(|x| {
|
||||
let string = x.key().to_string();
|
||||
let key = string.splitn(3, '-').collect::<Vec<_>>();
|
||||
let key = string.splitn(4, '-').collect::<Vec<_>>();
|
||||
|
||||
if key.len() == 3 {
|
||||
let hash = key[0];
|
||||
let loaders_key = key[1];
|
||||
let game_version = key[2];
|
||||
let parsed_key = if key.len() == 4
|
||||
&& matches!(
|
||||
key[2],
|
||||
"release" | "beta" | "alpha" | "all"
|
||||
) {
|
||||
Some((key[0], key[1], key[2], key[3]))
|
||||
} else {
|
||||
let key = string.splitn(3, '-').collect::<Vec<_>>();
|
||||
if key.len() == 3 {
|
||||
Some((
|
||||
key[0],
|
||||
key[1],
|
||||
ReleaseChannel::Alpha.key(),
|
||||
key[2],
|
||||
))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
};
|
||||
|
||||
if let Some((
|
||||
hash,
|
||||
loaders_key,
|
||||
channel_policy_key,
|
||||
game_version,
|
||||
)) = parsed_key
|
||||
{
|
||||
if let Some(values) =
|
||||
filtered_keys.iter_mut().find(|x| {
|
||||
x.0.0 == loaders_key && x.0.1 == game_version
|
||||
x.0.0 == loaders_key
|
||||
&& x.0.1 == channel_policy_key
|
||||
&& x.0.2 == game_version
|
||||
})
|
||||
{
|
||||
values.1.push(hash.to_string());
|
||||
@@ -1474,6 +1572,7 @@ impl CachedEntry {
|
||||
filtered_keys.push((
|
||||
(
|
||||
loaders_key.to_string(),
|
||||
channel_policy_key.to_string(),
|
||||
game_version.to_string(),
|
||||
),
|
||||
vec![hash.to_string()],
|
||||
@@ -1489,19 +1588,56 @@ impl CachedEntry {
|
||||
|
||||
let variations =
|
||||
futures::future::try_join_all(filtered_keys.iter().map(
|
||||
|((loaders_key, game_version), hashes)| {
|
||||
fetch_json::<HashMap<String, Vec<Version>>>(
|
||||
Method::POST,
|
||||
concat!(env!("MODRINTH_API_URL"), "version_files/update_many"),
|
||||
None,
|
||||
Some(serde_json::json!({
|
||||
"algorithm": "sha1",
|
||||
"hashes": hashes,
|
||||
"loaders": loaders_key.split('+').collect::<Vec<_>>(),
|
||||
"game_versions": [game_version]
|
||||
})),
|
||||
fetch_semaphore,
|
||||
pool,
|
||||
|((loaders_key, channel_policy_key, game_version), hashes)| async move {
|
||||
let channel_policy =
|
||||
ReleaseChannel::from_key(channel_policy_key);
|
||||
let mut remaining_hashes = hashes.clone();
|
||||
let mut found_versions = HashMap::new();
|
||||
|
||||
for version_types in
|
||||
channel_policy.version_type_fallbacks()
|
||||
{
|
||||
if remaining_hashes.is_empty() {
|
||||
break;
|
||||
}
|
||||
|
||||
let variation = fetch_json::<
|
||||
HashMap<String, Vec<Version>>,
|
||||
>(
|
||||
Method::POST,
|
||||
concat!(
|
||||
env!("MODRINTH_API_URL"),
|
||||
"version_files/update_many"
|
||||
),
|
||||
None,
|
||||
Some(serde_json::json!({
|
||||
"algorithm": "sha1",
|
||||
"hashes": remaining_hashes.clone(),
|
||||
"loaders": loaders_key.split('+').collect::<Vec<_>>(),
|
||||
"game_versions": [game_version],
|
||||
"version_types": version_types
|
||||
})),
|
||||
fetch_semaphore,
|
||||
pool,
|
||||
)
|
||||
.await?;
|
||||
|
||||
for (hash, versions) in variation {
|
||||
found_versions.insert(hash, versions);
|
||||
}
|
||||
|
||||
remaining_hashes = hashes
|
||||
.iter()
|
||||
.filter(|hash| {
|
||||
!found_versions
|
||||
.contains_key(hash.as_str())
|
||||
})
|
||||
.cloned()
|
||||
.collect();
|
||||
}
|
||||
|
||||
Ok::<HashMap<String, Vec<Version>>, crate::Error>(
|
||||
found_versions,
|
||||
)
|
||||
},
|
||||
))
|
||||
@@ -1509,9 +1645,10 @@ impl CachedEntry {
|
||||
|
||||
for (index, mut variation) in variations.into_iter().enumerate()
|
||||
{
|
||||
let ((loaders_key, game_version), hashes) =
|
||||
&filtered_keys[index];
|
||||
|
||||
let (
|
||||
(loaders_key, channel_policy_key, game_version),
|
||||
hashes,
|
||||
) = &filtered_keys[index];
|
||||
for hash in hashes {
|
||||
let versions = variation.remove(hash);
|
||||
|
||||
@@ -1531,6 +1668,8 @@ impl CachedEntry {
|
||||
.split('+')
|
||||
.map(|x| x.to_string())
|
||||
.collect(),
|
||||
channel_policy: channel_policy_key
|
||||
.to_string(),
|
||||
update_version_id: version_id,
|
||||
})
|
||||
.get_entry(),
|
||||
@@ -1541,7 +1680,7 @@ impl CachedEntry {
|
||||
vals.push((
|
||||
CacheValueType::FileUpdate.get_empty_entry(
|
||||
format!(
|
||||
"{hash}-{loaders_key}-{game_version}"
|
||||
"{hash}-{loaders_key}-{channel_policy_key}-{game_version}"
|
||||
),
|
||||
),
|
||||
true,
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
|
||||
use crate::pack::install_from::{PackFileHash, PackFormat};
|
||||
use crate::state::profiles::{Profile, ProfileFile, ProjectType};
|
||||
use crate::state::{CacheBehaviour, CachedEntry};
|
||||
use crate::state::{CacheBehaviour, CachedEntry, ReleaseChannel};
|
||||
use crate::util::fetch::{
|
||||
DownloadMeta, DownloadReason, FetchSemaphore, fetch_mirrors, sha1_async,
|
||||
};
|
||||
@@ -225,8 +225,12 @@ pub async fn get_linked_modpack_info(
|
||||
};
|
||||
|
||||
// Check for updates
|
||||
let (has_update, update_version_id, update_version) =
|
||||
check_modpack_update(&linked_data.version_id, &version, all_versions);
|
||||
let (has_update, update_version_id, update_version) = check_modpack_update(
|
||||
&linked_data.version_id,
|
||||
&version,
|
||||
all_versions,
|
||||
profile.preferred_update_channel,
|
||||
);
|
||||
|
||||
Ok(Some(LinkedModpackInfo {
|
||||
project,
|
||||
@@ -244,24 +248,42 @@ fn check_modpack_update(
|
||||
installed_version_id: &str,
|
||||
installed_version: &Version,
|
||||
all_versions: Option<Vec<Version>>,
|
||||
preferred_update_channel: ReleaseChannel,
|
||||
) -> (bool, Option<String>, Option<Version>) {
|
||||
let Some(versions) = all_versions else {
|
||||
return (false, None, None);
|
||||
};
|
||||
|
||||
let mut newer_versions: Vec<&Version> = versions
|
||||
.iter()
|
||||
.filter(|v| {
|
||||
v.id != installed_version_id
|
||||
&& v.date_published > installed_version.date_published
|
||||
})
|
||||
.collect();
|
||||
let installed_channel =
|
||||
ReleaseChannel::from_version_type(&installed_version.version_type);
|
||||
let effective_channel =
|
||||
preferred_update_channel.least_stable(installed_channel);
|
||||
|
||||
// Sort by date_published descending (newest first)
|
||||
newer_versions.sort_by_key(|b| std::cmp::Reverse(b.date_published));
|
||||
for version_types in effective_channel.version_type_fallbacks() {
|
||||
if !versions
|
||||
.iter()
|
||||
.any(|v| version_types.contains(&v.version_type.as_str()))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(newest) = newer_versions.first() {
|
||||
return (true, Some(newest.id.clone()), Some((*newest).clone()));
|
||||
let mut newer_versions: Vec<&Version> = versions
|
||||
.iter()
|
||||
.filter(|v| {
|
||||
v.id != installed_version_id
|
||||
&& v.date_published > installed_version.date_published
|
||||
&& version_types.contains(&v.version_type.as_str())
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Sort by date_published descending (newest first)
|
||||
newer_versions.sort_by_key(|b| std::cmp::Reverse(b.date_published));
|
||||
|
||||
if let Some(newest) = newer_versions.first() {
|
||||
return (true, Some(newest.id.clone()), Some((*newest).clone()));
|
||||
}
|
||||
|
||||
return (false, None, None);
|
||||
}
|
||||
|
||||
(false, None, None)
|
||||
|
||||
@@ -7,7 +7,7 @@ use crate::state::{
|
||||
Credentials, DefaultPage, DependencyType, DeviceToken, DeviceTokenKey,
|
||||
DeviceTokenPair, FileType, Hooks, LauncherFeatureVersion, LinkedData,
|
||||
MemorySettings, ModrinthCredentials, Profile, ProfileInstallStage,
|
||||
TeamMember, Theme, VersionFile, WindowSize,
|
||||
ReleaseChannel, TeamMember, Theme, VersionFile, WindowSize,
|
||||
};
|
||||
use crate::util::fetch::{IoSemaphore, read_json};
|
||||
use chrono::{DateTime, Utc};
|
||||
@@ -248,6 +248,9 @@ where
|
||||
loaders: vec![
|
||||
mod_loader.as_str().to_string(),
|
||||
],
|
||||
channel_policy: ReleaseChannel::Alpha
|
||||
.key()
|
||||
.to_string(),
|
||||
update_version_id: update_version
|
||||
.id
|
||||
.clone(),
|
||||
@@ -333,6 +336,7 @@ where
|
||||
|
||||
None
|
||||
}),
|
||||
preferred_update_channel: ReleaseChannel::Release,
|
||||
created: profile.metadata.date_created,
|
||||
modified: profile.metadata.date_modified,
|
||||
last_played: profile.metadata.last_played,
|
||||
|
||||
@@ -2,7 +2,8 @@ use super::settings::{Hooks, MemorySettings, WindowSize};
|
||||
use crate::profile::get_full_path;
|
||||
use crate::state::server_join_log::JoinLogEntry;
|
||||
use crate::state::{
|
||||
CacheBehaviour, CachedEntry, CachedFile, CachedFileHash, cache_file_hash,
|
||||
CacheBehaviour, CachedEntry, CachedFile, CachedFileHash, ReleaseChannel,
|
||||
cache_file_hash,
|
||||
};
|
||||
use crate::util;
|
||||
use crate::util::fetch::{FetchSemaphore, IoSemaphore, write_cached_icon};
|
||||
@@ -39,6 +40,7 @@ pub struct Profile {
|
||||
pub groups: Vec<String>,
|
||||
|
||||
pub linked_data: Option<LinkedData>,
|
||||
pub preferred_update_channel: ReleaseChannel,
|
||||
|
||||
pub created: DateTime<Utc>,
|
||||
pub modified: DateTime<Utc>,
|
||||
@@ -295,6 +297,7 @@ struct ProfileQueryResult {
|
||||
linked_project_id: Option<String>,
|
||||
linked_version_id: Option<String>,
|
||||
locked: Option<i64>,
|
||||
preferred_update_channel: String,
|
||||
created: i64,
|
||||
modified: i64,
|
||||
last_played: Option<i64>,
|
||||
@@ -344,6 +347,9 @@ impl TryFrom<ProfileQueryResult> for Profile {
|
||||
} else {
|
||||
None
|
||||
},
|
||||
preferred_update_channel: ReleaseChannel::from_key(
|
||||
&x.preferred_update_channel,
|
||||
),
|
||||
created: Utc
|
||||
.timestamp_opt(x.created, 0)
|
||||
.single()
|
||||
@@ -394,7 +400,7 @@ macro_rules! select_profiles_with_predicate {
|
||||
path, install_stage, launcher_feature_version, name, icon_path,
|
||||
game_version, protocol_version, mod_loader, mod_loader_version,
|
||||
json(groups) as "groups!: serde_json::Value",
|
||||
linked_project_id, linked_version_id, locked,
|
||||
linked_project_id, linked_version_id, locked, preferred_update_channel,
|
||||
created, modified, last_played,
|
||||
submitted_time_played, recent_time_played,
|
||||
override_java_path,
|
||||
@@ -492,6 +498,7 @@ impl Profile {
|
||||
let linked_data_version_id =
|
||||
self.linked_data.as_ref().map(|x| x.version_id.clone());
|
||||
let linked_data_locked = self.linked_data.as_ref().map(|x| x.locked);
|
||||
let preferred_update_channel = self.preferred_update_channel.key();
|
||||
|
||||
let created = self.created.timestamp();
|
||||
let modified = self.modified.timestamp();
|
||||
@@ -514,7 +521,7 @@ impl Profile {
|
||||
path, install_stage, name, icon_path,
|
||||
game_version, mod_loader, mod_loader_version,
|
||||
groups,
|
||||
linked_project_id, linked_version_id, locked,
|
||||
linked_project_id, linked_version_id, locked, preferred_update_channel,
|
||||
created, modified, last_played,
|
||||
submitted_time_played, recent_time_played,
|
||||
override_java_path, override_extra_launch_args, override_custom_env_vars,
|
||||
@@ -526,13 +533,13 @@ impl Profile {
|
||||
$1, $2, $3, $4,
|
||||
$5, $6, $7,
|
||||
jsonb($8),
|
||||
$9, $10, $11,
|
||||
$12, $13, $14,
|
||||
$15, $16,
|
||||
$17, jsonb($18), jsonb($19),
|
||||
$20, $21, $22, $23,
|
||||
$24, $25, $26,
|
||||
$27, $28
|
||||
$9, $10, $11, $12,
|
||||
$13, $14, $15,
|
||||
$16, $17,
|
||||
$18, jsonb($19), jsonb($20),
|
||||
$21, $22, $23, $24,
|
||||
$25, $26, $27,
|
||||
$28, $29
|
||||
)
|
||||
ON CONFLICT (path) DO UPDATE SET
|
||||
install_stage = $2,
|
||||
@@ -548,28 +555,29 @@ impl Profile {
|
||||
linked_project_id = $9,
|
||||
linked_version_id = $10,
|
||||
locked = $11,
|
||||
preferred_update_channel = $12,
|
||||
|
||||
created = $12,
|
||||
modified = $13,
|
||||
last_played = $14,
|
||||
created = $13,
|
||||
modified = $14,
|
||||
last_played = $15,
|
||||
|
||||
submitted_time_played = $15,
|
||||
recent_time_played = $16,
|
||||
submitted_time_played = $16,
|
||||
recent_time_played = $17,
|
||||
|
||||
override_java_path = $17,
|
||||
override_extra_launch_args = jsonb($18),
|
||||
override_custom_env_vars = jsonb($19),
|
||||
override_mc_memory_max = $20,
|
||||
override_mc_force_fullscreen = $21,
|
||||
override_mc_game_resolution_x = $22,
|
||||
override_mc_game_resolution_y = $23,
|
||||
override_java_path = $18,
|
||||
override_extra_launch_args = jsonb($19),
|
||||
override_custom_env_vars = jsonb($20),
|
||||
override_mc_memory_max = $21,
|
||||
override_mc_force_fullscreen = $22,
|
||||
override_mc_game_resolution_x = $23,
|
||||
override_mc_game_resolution_y = $24,
|
||||
|
||||
override_hook_pre_launch = $24,
|
||||
override_hook_wrapper = $25,
|
||||
override_hook_post_exit = $26,
|
||||
override_hook_pre_launch = $25,
|
||||
override_hook_wrapper = $26,
|
||||
override_hook_post_exit = $27,
|
||||
|
||||
protocol_version = $27,
|
||||
launcher_feature_version = $28
|
||||
protocol_version = $28,
|
||||
launcher_feature_version = $29
|
||||
",
|
||||
self.path,
|
||||
install_stage,
|
||||
@@ -582,6 +590,7 @@ impl Profile {
|
||||
linked_data_project_id,
|
||||
linked_data_version_id,
|
||||
linked_data_locked,
|
||||
preferred_update_channel,
|
||||
created,
|
||||
modified,
|
||||
last_played,
|
||||
@@ -744,9 +753,15 @@ impl Profile {
|
||||
let file_updates = file_hashes
|
||||
.iter()
|
||||
.filter_map(|file| {
|
||||
all.iter()
|
||||
.find(|prof| file.path.contains(&prof.path))
|
||||
.map(|profile| Self::get_cache_key(file, profile))
|
||||
all.iter().find(|prof| file.path.contains(&prof.path)).map(
|
||||
|profile| {
|
||||
Self::get_cache_key(
|
||||
file,
|
||||
profile,
|
||||
profile.preferred_update_channel,
|
||||
)
|
||||
},
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
@@ -1017,10 +1032,26 @@ impl Profile {
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let installed_channels = Self::get_installed_update_channels(
|
||||
&file_info_by_hash,
|
||||
cache_behaviour,
|
||||
pool,
|
||||
fetch_semaphore,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let file_updates = file_hashes
|
||||
.iter()
|
||||
.filter(|x| file_info_by_hash.contains_key(&x.hash))
|
||||
.map(|x| Self::get_cache_key(x, self))
|
||||
.map(|x| {
|
||||
Self::get_cache_key(
|
||||
x,
|
||||
self,
|
||||
self.effective_update_channel(
|
||||
installed_channels.get(&x.hash).copied(),
|
||||
),
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let file_updates_ref =
|
||||
@@ -1035,35 +1066,53 @@ impl Profile {
|
||||
|
||||
(file_hashes, file_info_by_hash, file_updates)
|
||||
} else {
|
||||
let file_updates = file_hashes
|
||||
.iter()
|
||||
.map(|x| Self::get_cache_key(x, self))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let file_hashes_ref =
|
||||
file_hashes.iter().map(|x| &*x.hash).collect::<Vec<_>>();
|
||||
let file_updates_ref =
|
||||
file_updates.iter().map(|x| &**x).collect::<Vec<_>>();
|
||||
let (file_info, file_updates) = tokio::try_join!(
|
||||
CachedEntry::get_file_many(
|
||||
&file_hashes_ref,
|
||||
cache_behaviour,
|
||||
pool,
|
||||
fetch_semaphore,
|
||||
),
|
||||
CachedEntry::get_file_update_many(
|
||||
&file_updates_ref,
|
||||
cache_behaviour,
|
||||
pool,
|
||||
fetch_semaphore,
|
||||
)
|
||||
)?;
|
||||
let file_info = CachedEntry::get_file_many(
|
||||
&file_hashes_ref,
|
||||
cache_behaviour,
|
||||
pool,
|
||||
fetch_semaphore,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let file_info_by_hash: HashMap<String, CachedFile> = file_info
|
||||
.into_iter()
|
||||
.map(|f| (f.hash.clone(), f))
|
||||
.collect();
|
||||
|
||||
let installed_channels = Self::get_installed_update_channels(
|
||||
&file_info_by_hash,
|
||||
cache_behaviour,
|
||||
pool,
|
||||
fetch_semaphore,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let file_updates = file_hashes
|
||||
.iter()
|
||||
.filter(|x| file_info_by_hash.contains_key(&x.hash))
|
||||
.map(|x| {
|
||||
Self::get_cache_key(
|
||||
x,
|
||||
self,
|
||||
self.effective_update_channel(
|
||||
installed_channels.get(&x.hash).copied(),
|
||||
),
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let file_updates_ref =
|
||||
file_updates.iter().map(|x| &**x).collect::<Vec<_>>();
|
||||
let file_updates = CachedEntry::get_file_update_many(
|
||||
&file_updates_ref,
|
||||
cache_behaviour,
|
||||
pool,
|
||||
fetch_semaphore,
|
||||
)
|
||||
.await?;
|
||||
|
||||
(file_hashes, file_info_by_hash, file_updates)
|
||||
};
|
||||
|
||||
@@ -1122,6 +1171,59 @@ impl Profile {
|
||||
Ok(files)
|
||||
}
|
||||
|
||||
async fn get_installed_update_channels(
|
||||
file_info_by_hash: &HashMap<String, CachedFile>,
|
||||
cache_behaviour: Option<CacheBehaviour>,
|
||||
pool: &SqlitePool,
|
||||
fetch_semaphore: &FetchSemaphore,
|
||||
) -> crate::Result<HashMap<String, ReleaseChannel>> {
|
||||
let version_ids = file_info_by_hash
|
||||
.values()
|
||||
.map(|file| file.version_id.as_str())
|
||||
.collect::<HashSet<_>>();
|
||||
|
||||
if version_ids.is_empty() {
|
||||
return Ok(HashMap::new());
|
||||
}
|
||||
|
||||
let version_ids_ref = version_ids.iter().copied().collect::<Vec<_>>();
|
||||
let versions = CachedEntry::get_version_many(
|
||||
&version_ids_ref,
|
||||
cache_behaviour,
|
||||
pool,
|
||||
fetch_semaphore,
|
||||
)
|
||||
.await?;
|
||||
let channels_by_version_id = versions
|
||||
.into_iter()
|
||||
.map(|version| {
|
||||
(
|
||||
version.id,
|
||||
ReleaseChannel::from_version_type(&version.version_type),
|
||||
)
|
||||
})
|
||||
.collect::<HashMap<_, _>>();
|
||||
|
||||
Ok(file_info_by_hash
|
||||
.iter()
|
||||
.filter_map(|(hash, file)| {
|
||||
channels_by_version_id
|
||||
.get(&file.version_id)
|
||||
.copied()
|
||||
.map(|channel| (hash.clone(), channel))
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
fn effective_update_channel(
|
||||
&self,
|
||||
installed_channel: Option<ReleaseChannel>,
|
||||
) -> ReleaseChannel {
|
||||
installed_channel.map_or(self.preferred_update_channel, |channel| {
|
||||
self.preferred_update_channel.least_stable(channel)
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn get_installed_project_ids(
|
||||
&self,
|
||||
pool: &SqlitePool,
|
||||
@@ -1210,9 +1312,13 @@ impl Profile {
|
||||
Ok((keys, file_hashes))
|
||||
}
|
||||
|
||||
fn get_cache_key(file: &CachedFileHash, profile: &Profile) -> String {
|
||||
fn get_cache_key(
|
||||
file: &CachedFileHash,
|
||||
profile: &Profile,
|
||||
channel: ReleaseChannel,
|
||||
) -> String {
|
||||
format!(
|
||||
"{}-{}-{}",
|
||||
"{}-{}-{}-{}",
|
||||
file.hash,
|
||||
file.project_type
|
||||
.filter(|x| *x != ProjectType::Mod)
|
||||
@@ -1220,6 +1326,7 @@ impl Profile {
|
||||
|| profile.loader.as_str().to_string(),
|
||||
|x| x.get_loaders().join("+")
|
||||
),
|
||||
channel.key(),
|
||||
profile.game_version
|
||||
)
|
||||
}
|
||||
|
||||
@@ -21,4 +21,5 @@ export { default as ContentCardLayout } from './layout.vue'
|
||||
export { default as ContentPageLayout } from './layout.vue'
|
||||
export * from './providers'
|
||||
export * from './types'
|
||||
export * from './utils/update-channels'
|
||||
export { default as ConfirmLeaveModal } from '#ui/components/modal/ConfirmLeaveModal.vue'
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
import type { Labrinth } from '@modrinth/api-client'
|
||||
|
||||
export type UpdateChannelPolicy = 'release' | 'beta' | 'alpha'
|
||||
|
||||
const channelRank: Record<UpdateChannelPolicy, number> = {
|
||||
release: 0,
|
||||
beta: 1,
|
||||
alpha: 2,
|
||||
}
|
||||
|
||||
function normalizeChannel(versionType: string): UpdateChannelPolicy {
|
||||
if (versionType === 'alpha' || versionType === 'beta') return versionType
|
||||
return 'release'
|
||||
}
|
||||
|
||||
function effectiveUpdateChannel(
|
||||
policy: UpdateChannelPolicy,
|
||||
currentVersionType?: string | null,
|
||||
): UpdateChannelPolicy {
|
||||
if (!currentVersionType) return policy
|
||||
|
||||
const currentChannel = normalizeChannel(currentVersionType)
|
||||
return channelRank[currentChannel] > channelRank[policy] ? currentChannel : policy
|
||||
}
|
||||
|
||||
function channelFallbacks(policy: UpdateChannelPolicy): UpdateChannelPolicy[][] {
|
||||
switch (policy) {
|
||||
case 'release':
|
||||
return [['release'], ['beta'], ['alpha']]
|
||||
case 'beta':
|
||||
return [['release', 'beta'], ['alpha']]
|
||||
case 'alpha':
|
||||
return [['release', 'beta', 'alpha']]
|
||||
}
|
||||
}
|
||||
|
||||
export function allowsUpdateChannel(
|
||||
version: Pick<Labrinth.Versions.v2.Version, 'version_type'>,
|
||||
policy: UpdateChannelPolicy,
|
||||
currentVersionType?: string | null,
|
||||
) {
|
||||
const effectivePolicy = effectiveUpdateChannel(policy, currentVersionType)
|
||||
return channelFallbacks(effectivePolicy)[0].includes(normalizeChannel(version.version_type))
|
||||
}
|
||||
|
||||
export function newestEligibleUpdate(
|
||||
versions: Labrinth.Versions.v2.Version[],
|
||||
currentVersionId: string,
|
||||
currentPublishedAt: string | null | undefined,
|
||||
policy: UpdateChannelPolicy,
|
||||
currentVersionType?: string | null,
|
||||
) {
|
||||
const currentTime = currentPublishedAt ? new Date(currentPublishedAt).getTime() : Number.NaN
|
||||
const sortedVersions = [...versions].sort(
|
||||
(a, b) => new Date(b.date_published).getTime() - new Date(a.date_published).getTime(),
|
||||
)
|
||||
const effectivePolicy = effectiveUpdateChannel(policy, currentVersionType)
|
||||
|
||||
for (const versionTypes of channelFallbacks(effectivePolicy)) {
|
||||
if (
|
||||
!versions.some((version) => versionTypes.includes(normalizeChannel(version.version_type)))
|
||||
) {
|
||||
continue
|
||||
}
|
||||
|
||||
return (
|
||||
sortedVersions.find((version) => {
|
||||
if (version.id === currentVersionId) return false
|
||||
if (!versionTypes.includes(normalizeChannel(version.version_type))) return false
|
||||
if (Number.isNaN(currentTime)) return true
|
||||
return new Date(version.date_published).getTime() > currentTime
|
||||
}) ?? null
|
||||
)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
Reference in New Issue
Block a user