fixes: post content tab release issues (#5566)
* fix: migrate old cache entries for CachedFileUpdate * feat: toggle goofy fix + switch version reimpl in app and panel * fix: multimc detection * fix: add tie breaker for sorting * feat: toggle hover state * fix: lint
This commit is contained in:
@@ -71,6 +71,9 @@
|
|||||||
"app.instance.mods.successfully-uploaded": {
|
"app.instance.mods.successfully-uploaded": {
|
||||||
"message": "Successfully uploaded"
|
"message": "Successfully uploaded"
|
||||||
},
|
},
|
||||||
|
"app.instance.mods.switch-version": {
|
||||||
|
"message": "Switch version"
|
||||||
|
},
|
||||||
"app.instance.mods.unknown-version": {
|
"app.instance.mods.unknown-version": {
|
||||||
"message": "Unknown"
|
"message": "Unknown"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -58,6 +58,7 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { Labrinth } from '@modrinth/api-client'
|
import type { Labrinth } from '@modrinth/api-client'
|
||||||
|
import { ArrowLeftRightIcon, ClipboardCopyIcon, FolderOpenIcon } from '@modrinth/assets'
|
||||||
import {
|
import {
|
||||||
ConfirmModpackUpdateModal,
|
ConfirmModpackUpdateModal,
|
||||||
type ContentItem,
|
type ContentItem,
|
||||||
@@ -158,6 +159,10 @@ const messages = defineMessages({
|
|||||||
id: 'app.instance.mods.copy-link',
|
id: 'app.instance.mods.copy-link',
|
||||||
defaultMessage: 'Copy link',
|
defaultMessage: 'Copy link',
|
||||||
},
|
},
|
||||||
|
switchVersion: {
|
||||||
|
id: 'app.instance.mods.switch-version',
|
||||||
|
defaultMessage: 'Switch version',
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
let savedModalState: ModpackContentModalState | null = null
|
let savedModalState: ModpackContentModalState | null = null
|
||||||
@@ -404,6 +409,34 @@ async function handleUpdate(id: string) {
|
|||||||
updatingProjectVersions.value = versions
|
updatingProjectVersions.value = versions
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function handleSwitchVersion(item: ContentItem) {
|
||||||
|
if (!item.project?.id || !item.version?.id) return
|
||||||
|
|
||||||
|
updatingModpack.value = false
|
||||||
|
updatingProject.value = item
|
||||||
|
updatingProjectVersions.value = []
|
||||||
|
loadingVersions.value = true
|
||||||
|
loadingChangelog.value = false
|
||||||
|
|
||||||
|
await nextTick()
|
||||||
|
|
||||||
|
contentUpdaterModal.value?.show(item.version.id, { switchMode: true })
|
||||||
|
|
||||||
|
const versions = (await get_project_versions(item.project.id).catch((e) => {
|
||||||
|
return handleError(e)
|
||||||
|
})) as Labrinth.Versions.v2.Version[] | null
|
||||||
|
|
||||||
|
loadingVersions.value = false
|
||||||
|
|
||||||
|
if (!versions) return
|
||||||
|
|
||||||
|
versions.sort(
|
||||||
|
(a, b) => new Date(b.date_published).getTime() - new Date(a.date_published).getTime(),
|
||||||
|
)
|
||||||
|
|
||||||
|
updatingProjectVersions.value = versions
|
||||||
|
}
|
||||||
|
|
||||||
async function handleModpackContentToggle(item: ContentItem) {
|
async function handleModpackContentToggle(item: ContentItem) {
|
||||||
await toggleDisableMod(item)
|
await toggleDisableMod(item)
|
||||||
}
|
}
|
||||||
@@ -592,16 +625,26 @@ async function handleShareItems(
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getOverflowOptions(item: ContentItem): OverflowMenuOption[] {
|
function getOverflowOptions(item: ContentItem): OverflowMenuOption[] {
|
||||||
const options: OverflowMenuOption[] = [
|
const options: OverflowMenuOption[] = []
|
||||||
{
|
|
||||||
id: formatMessage(messages.showFile),
|
if (item.project?.id && item.version?.id && !item.has_update) {
|
||||||
action: () => highlightModInProfile(props.instance.path, item.file_path),
|
options.push({
|
||||||
},
|
id: formatMessage(messages.switchVersion),
|
||||||
]
|
icon: ArrowLeftRightIcon,
|
||||||
|
action: () => handleSwitchVersion(item),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
options.push({
|
||||||
|
id: formatMessage(messages.showFile),
|
||||||
|
icon: FolderOpenIcon,
|
||||||
|
action: () => highlightModInProfile(props.instance.path, item.file_path),
|
||||||
|
})
|
||||||
|
|
||||||
if (item.project?.slug) {
|
if (item.project?.slug) {
|
||||||
options.push({
|
options.push({
|
||||||
id: formatMessage(messages.copyLink),
|
id: formatMessage(messages.copyLink),
|
||||||
|
icon: ClipboardCopyIcon,
|
||||||
action: async () => {
|
action: async () => {
|
||||||
await navigator.clipboard.writeText(
|
await navigator.clipboard.writeText(
|
||||||
`https://modrinth.com/${item.project_type}/${item.project?.slug}`,
|
`https://modrinth.com/${item.project_type}/${item.project?.slug}`,
|
||||||
|
|||||||
@@ -171,7 +171,9 @@ pub fn get_default_launcher_path(
|
|||||||
r#type: ImportLauncherType,
|
r#type: ImportLauncherType,
|
||||||
) -> Option<PathBuf> {
|
) -> Option<PathBuf> {
|
||||||
let path = match r#type {
|
let path = match r#type {
|
||||||
ImportLauncherType::MultiMC => None, // multimc data is *in* app dir
|
ImportLauncherType::MultiMC => {
|
||||||
|
return find_multimc_path();
|
||||||
|
}
|
||||||
ImportLauncherType::PrismLauncher => {
|
ImportLauncherType::PrismLauncher => {
|
||||||
Some(dirs::data_dir()?.join("PrismLauncher"))
|
Some(dirs::data_dir()?.join("PrismLauncher"))
|
||||||
}
|
}
|
||||||
@@ -195,6 +197,54 @@ pub fn get_default_launcher_path(
|
|||||||
if path.exists() { Some(path) } else { None }
|
if path.exists() { Some(path) } else { None }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Searches common locations for a MultiMC installation.
|
||||||
|
/// MultiMC stores data in its own application directory (not a standard data dir)
|
||||||
|
fn find_multimc_path() -> Option<PathBuf> {
|
||||||
|
let mut candidates: Vec<PathBuf> = Vec::new();
|
||||||
|
|
||||||
|
// Linux/macOS: ~/.local/share/multimc is the typical location
|
||||||
|
if let Some(data_dir) = dirs::data_dir() {
|
||||||
|
candidates.push(data_dir.join("multimc"));
|
||||||
|
candidates.push(data_dir.join("MultiMC"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Windows: check common extraction locations
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
{
|
||||||
|
if let Some(home) = dirs::home_dir() {
|
||||||
|
candidates.push(home.join("MultiMC"));
|
||||||
|
candidates.push(home.join("Desktop").join("MultiMC"));
|
||||||
|
candidates.push(home.join("Downloads").join("MultiMC"));
|
||||||
|
}
|
||||||
|
candidates.push(PathBuf::from("C:\\MultiMC"));
|
||||||
|
if let Some(program_files) =
|
||||||
|
std::env::var_os("ProgramFiles").map(PathBuf::from)
|
||||||
|
{
|
||||||
|
candidates.push(program_files.join("MultiMC"));
|
||||||
|
}
|
||||||
|
if let Some(program_files_x86) =
|
||||||
|
std::env::var_os("ProgramFiles(x86)").map(PathBuf::from)
|
||||||
|
{
|
||||||
|
candidates.push(program_files_x86.join("MultiMC"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// macOS: MultiMC is a .app bundle with data inside MultiMC.app/Data/
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
{
|
||||||
|
candidates.push(PathBuf::from("/Applications/MultiMC.app/Data"));
|
||||||
|
if let Some(home) = dirs::home_dir() {
|
||||||
|
candidates.push(
|
||||||
|
home.join("Applications").join("MultiMC.app").join("Data"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
candidates
|
||||||
|
.into_iter()
|
||||||
|
.find(|p| p.join("multimc.cfg").exists())
|
||||||
|
}
|
||||||
|
|
||||||
/// Checks if this PathBuf is a valid instance for the given launcher type
|
/// Checks if this PathBuf is a valid instance for the given launcher type
|
||||||
|
|
||||||
#[tracing::instrument]
|
#[tracing::instrument]
|
||||||
|
|||||||
@@ -252,7 +252,7 @@ pub struct SearchResultV3 {
|
|||||||
pub total_hits: u32,
|
pub total_hits: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
#[derive(Serialize, Clone, Debug)]
|
||||||
pub struct CachedFileUpdate {
|
pub struct CachedFileUpdate {
|
||||||
pub hash: String,
|
pub hash: String,
|
||||||
pub game_version: String,
|
pub game_version: String,
|
||||||
@@ -260,6 +260,39 @@ pub struct CachedFileUpdate {
|
|||||||
pub update_version_id: String,
|
pub update_version_id: 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
|
||||||
|
impl<'de> serde::Deserialize<'de> for CachedFileUpdate {
|
||||||
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||||
|
where
|
||||||
|
D: serde::Deserializer<'de>,
|
||||||
|
{
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct Helper {
|
||||||
|
hash: String,
|
||||||
|
game_version: String,
|
||||||
|
#[serde(default)]
|
||||||
|
loaders: Option<Vec<String>>,
|
||||||
|
#[serde(default)]
|
||||||
|
loader: Option<String>,
|
||||||
|
update_version_id: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
let helper = Helper::deserialize(deserializer)?;
|
||||||
|
let loaders = helper.loaders.unwrap_or_else(|| {
|
||||||
|
helper.loader.map(|l| vec![l]).unwrap_or_default()
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(CachedFileUpdate {
|
||||||
|
hash: helper.hash,
|
||||||
|
game_version: helper.game_version,
|
||||||
|
loaders,
|
||||||
|
update_version_id: helper.update_version_id,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||||
pub struct CachedFileHash {
|
pub struct CachedFileHash {
|
||||||
pub path: String,
|
pub path: String,
|
||||||
|
|||||||
@@ -577,7 +577,10 @@ async fn profile_files_to_content_items(
|
|||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|p| p.title.as_str())
|
.map(|p| p.title.as_str())
|
||||||
.unwrap_or(&b.file_name);
|
.unwrap_or(&b.file_name);
|
||||||
name_a.to_lowercase().cmp(&name_b.to_lowercase())
|
name_a
|
||||||
|
.to_lowercase()
|
||||||
|
.cmp(&name_b.to_lowercase())
|
||||||
|
.then_with(|| a.file_name.cmp(&b.file_name))
|
||||||
});
|
});
|
||||||
|
|
||||||
Ok(items)
|
Ok(items)
|
||||||
@@ -765,7 +768,10 @@ pub async fn dependencies_to_content_items(
|
|||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|p| p.title.as_str())
|
.map(|p| p.title.as_str())
|
||||||
.unwrap_or(&b.file_name);
|
.unwrap_or(&b.file_name);
|
||||||
name_a.to_lowercase().cmp(&name_b.to_lowercase())
|
name_a
|
||||||
|
.to_lowercase()
|
||||||
|
.cmp(&name_b.to_lowercase())
|
||||||
|
.then_with(|| a.file_name.cmp(&b.file_name))
|
||||||
});
|
});
|
||||||
|
|
||||||
Ok(items)
|
Ok(items)
|
||||||
|
|||||||
@@ -46,7 +46,10 @@
|
|||||||
}
|
}
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
<template v-if="!$slots[option.id]">{{ option.id }}</template>
|
<template v-if="!$slots[option.id]">
|
||||||
|
<component :is="option.icon" v-if="option.icon" class="size-5" />
|
||||||
|
{{ option.id }}
|
||||||
|
</template>
|
||||||
<slot :name="option.id"></slot>
|
<slot :name="option.id"></slot>
|
||||||
</Button>
|
</Button>
|
||||||
</template>
|
</template>
|
||||||
@@ -55,7 +58,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { type Ref, ref } from 'vue'
|
import { type Component, type Ref, ref } from 'vue'
|
||||||
|
|
||||||
import Button from './Button.vue'
|
import Button from './Button.vue'
|
||||||
import PopoutMenu from './PopoutMenu.vue'
|
import PopoutMenu from './PopoutMenu.vue'
|
||||||
@@ -70,6 +73,7 @@ interface Divider extends BaseOption {
|
|||||||
|
|
||||||
interface Item extends BaseOption {
|
interface Item extends BaseOption {
|
||||||
id: string
|
id: string
|
||||||
|
icon?: Component
|
||||||
action?: (event?: MouseEvent) => void
|
action?: (event?: MouseEvent) => void
|
||||||
link?: string
|
link?: string
|
||||||
external?: boolean
|
external?: boolean
|
||||||
|
|||||||
@@ -5,23 +5,28 @@
|
|||||||
role="switch"
|
role="switch"
|
||||||
:aria-checked="modelValue"
|
:aria-checked="modelValue"
|
||||||
:disabled="disabled"
|
:disabled="disabled"
|
||||||
class="relative inline-flex shrink-0 rounded-full m-0 transition-all duration-200 cursor-pointer border-none"
|
class="group inline-flex shrink-0 items-center rounded-full m-0 p-1 transition-all duration-200 cursor-pointer border-none"
|
||||||
:class="[
|
:class="[
|
||||||
small ? 'h-5 !w-[40px]' : 'h-8 !w-[60px]',
|
small ? 'h-5 !w-[40px]' : 'h-6 !w-[48px]',
|
||||||
modelValue ? 'bg-brand' : 'bg-button-bg',
|
modelValue ? 'bg-brand' : 'bg-button-bg',
|
||||||
disabled ? 'opacity-50 cursor-not-allowed' : 'btn-wrapper',
|
disabled ? 'opacity-50 cursor-not-allowed' : '',
|
||||||
]"
|
]"
|
||||||
@click="toggle"
|
@click="toggle"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="absolute rounded-full transition-all duration-200"
|
class="rounded-full transition-all duration-200"
|
||||||
:class="[
|
:class="[
|
||||||
small ? 'w-4 h-4 top-0.5 left-0.5' : 'w-[24px] h-[24px] top-1 left-1',
|
small ? 'w-3 h-3' : 'w-4 h-4',
|
||||||
modelValue
|
modelValue
|
||||||
? small
|
? small
|
||||||
? 'translate-x-5 bg-black/90'
|
? 'translate-x-[20px] bg-black/90'
|
||||||
: 'translate-x-7 bg-black/90'
|
: 'translate-x-[24px] bg-black/90'
|
||||||
: 'bg-gray',
|
: 'bg-gray',
|
||||||
|
disabled
|
||||||
|
? ''
|
||||||
|
: small
|
||||||
|
? 'group-hover:w-[14px] group-hover:h-[14px] group-hover:m-[-1px] group-active:w-[10px] group-active:h-[10px] group-active:m-[1px]'
|
||||||
|
: 'group-hover:w-[18px] group-hover:h-[18px] group-hover:m-[-1px] group-active:w-[14px] group-active:h-[14px] group-active:m-[1px]',
|
||||||
]"
|
]"
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -55,7 +55,10 @@
|
|||||||
@focus="selectedIndex = index"
|
@focus="selectedIndex = index"
|
||||||
@mouseover="handleMouseOver(index)"
|
@mouseover="handleMouseOver(index)"
|
||||||
>
|
>
|
||||||
<slot :name="option.id">{{ option.id }}</slot>
|
<slot :name="option.id">
|
||||||
|
<component :is="option.icon" v-if="option.icon" class="size-5" />
|
||||||
|
{{ option.id }}
|
||||||
|
</slot>
|
||||||
</button>
|
</button>
|
||||||
<AutoLink
|
<AutoLink
|
||||||
v-else-if="typeof option.action === 'string'"
|
v-else-if="typeof option.action === 'string'"
|
||||||
@@ -72,10 +75,16 @@
|
|||||||
@focus="selectedIndex = index"
|
@focus="selectedIndex = index"
|
||||||
@mouseover="handleMouseOver(index)"
|
@mouseover="handleMouseOver(index)"
|
||||||
>
|
>
|
||||||
<slot :name="option.id">{{ option.id }}</slot>
|
<slot :name="option.id">
|
||||||
|
<component :is="option.icon" v-if="option.icon" class="size-5" />
|
||||||
|
{{ option.id }}
|
||||||
|
</slot>
|
||||||
</AutoLink>
|
</AutoLink>
|
||||||
<span v-else>
|
<span v-else>
|
||||||
<slot :name="option.id">{{ option.id }}</slot>
|
<slot :name="option.id">
|
||||||
|
<component :is="option.icon" v-if="option.icon" class="size-5" />
|
||||||
|
{{ option.id }}
|
||||||
|
</slot>
|
||||||
</span>
|
</span>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
</template>
|
</template>
|
||||||
@@ -88,10 +97,11 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { AutoLink, ButtonStyled } from '@modrinth/ui'
|
import { AutoLink, ButtonStyled } from '@modrinth/ui'
|
||||||
import { onClickOutside, useElementHover } from '@vueuse/core'
|
import { onClickOutside, useElementHover } from '@vueuse/core'
|
||||||
import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue'
|
import { type Component, computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||||
|
|
||||||
interface Option {
|
interface Option {
|
||||||
id: string
|
id: string
|
||||||
|
icon?: Component
|
||||||
action?: (() => void) | string
|
action?: (() => void) | string
|
||||||
shown?: boolean
|
shown?: boolean
|
||||||
color?: 'standard' | 'brand' | 'red' | 'orange' | 'green' | 'blue' | 'purple'
|
color?: 'standard' | 'brand' | 'red' | 'orange' | 'green' | 'blue' | 'purple'
|
||||||
|
|||||||
@@ -10,7 +10,11 @@
|
|||||||
<span class="text-lg font-extrabold text-contrast">{{
|
<span class="text-lg font-extrabold text-contrast">{{
|
||||||
header ??
|
header ??
|
||||||
formatMessage(
|
formatMessage(
|
||||||
isModpack.value ? messages.switchModpackVersionHeader : messages.updateVersionHeader,
|
isModpack.value
|
||||||
|
? messages.switchModpackVersionHeader
|
||||||
|
: switchMode
|
||||||
|
? messages.switchVersionHeader
|
||||||
|
: messages.updateVersionHeader,
|
||||||
)
|
)
|
||||||
}}</span>
|
}}</span>
|
||||||
</template>
|
</template>
|
||||||
@@ -213,9 +217,16 @@
|
|||||||
>
|
>
|
||||||
<DownloadIcon />
|
<DownloadIcon />
|
||||||
{{
|
{{
|
||||||
formatMessage(isDowngrade ? messages.downgradeToVersion : messages.updateToVersion, {
|
formatMessage(
|
||||||
version: selectedVersion?.version_number ?? '...',
|
isDowngrade
|
||||||
})
|
? messages.downgradeToVersion
|
||||||
|
: switchMode
|
||||||
|
? messages.switchToVersion
|
||||||
|
: messages.updateToVersion,
|
||||||
|
{
|
||||||
|
version: selectedVersion?.version_number ?? '...',
|
||||||
|
},
|
||||||
|
)
|
||||||
}}
|
}}
|
||||||
</button>
|
</button>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
@@ -303,6 +314,14 @@ const messages = defineMessages({
|
|||||||
id: 'instances.updater-modal.update-to',
|
id: 'instances.updater-modal.update-to',
|
||||||
defaultMessage: 'Update to {version}',
|
defaultMessage: 'Update to {version}',
|
||||||
},
|
},
|
||||||
|
switchVersionHeader: {
|
||||||
|
id: 'instances.updater-modal.header-switch',
|
||||||
|
defaultMessage: 'Switch version',
|
||||||
|
},
|
||||||
|
switchToVersion: {
|
||||||
|
id: 'instances.updater-modal.switch-to',
|
||||||
|
defaultMessage: 'Switch to {version}',
|
||||||
|
},
|
||||||
currentBadge: {
|
currentBadge: {
|
||||||
id: 'instances.updater-modal.badge.current',
|
id: 'instances.updater-modal.badge.current',
|
||||||
defaultMessage: 'Current',
|
defaultMessage: 'Current',
|
||||||
@@ -361,6 +380,7 @@ const emit = defineEmits<{
|
|||||||
const modal = ref<InstanceType<typeof NewModal>>()
|
const modal = ref<InstanceType<typeof NewModal>>()
|
||||||
const searchQuery = ref('')
|
const searchQuery = ref('')
|
||||||
const hideIncompatibleState = ref(true)
|
const hideIncompatibleState = ref(true)
|
||||||
|
const switchMode = ref(false)
|
||||||
const selectedVersion = ref<Labrinth.Versions.v2.Version | null>(null)
|
const selectedVersion = ref<Labrinth.Versions.v2.Version | null>(null)
|
||||||
// Store the initial version ID to select when versions become available
|
// Store the initial version ID to select when versions become available
|
||||||
const pendingInitialVersionId = ref<string | undefined>(undefined)
|
const pendingInitialVersionId = ref<string | undefined>(undefined)
|
||||||
@@ -558,9 +578,10 @@ function handleCancel() {
|
|||||||
hide()
|
hide()
|
||||||
}
|
}
|
||||||
|
|
||||||
function show(initialVersionId?: string) {
|
function show(initialVersionId?: string, options?: { switchMode?: boolean }) {
|
||||||
searchQuery.value = ''
|
searchQuery.value = ''
|
||||||
hideIncompatibleState.value = true
|
hideIncompatibleState.value = true
|
||||||
|
switchMode.value = options?.switchMode ?? false
|
||||||
|
|
||||||
debug('show() called', {
|
debug('show() called', {
|
||||||
initialVersionId,
|
initialVersionId,
|
||||||
|
|||||||
@@ -186,25 +186,31 @@ const sortedItems = computed(() => {
|
|||||||
return items.sort((a, b) => {
|
return items.sort((a, b) => {
|
||||||
const nameA = a.project?.title ?? a.file_name
|
const nameA = a.project?.title ?? a.file_name
|
||||||
const nameB = b.project?.title ?? b.file_name
|
const nameB = b.project?.title ?? b.file_name
|
||||||
return nameB.toLowerCase().localeCompare(nameA.toLowerCase())
|
return (
|
||||||
|
nameB.toLowerCase().localeCompare(nameA.toLowerCase()) ||
|
||||||
|
a.file_name.localeCompare(b.file_name)
|
||||||
|
)
|
||||||
})
|
})
|
||||||
case 'date-added-newest':
|
case 'date-added-newest':
|
||||||
return items.sort((a, b) => {
|
return items.sort((a, b) => {
|
||||||
const dateA = a.date_added ?? ''
|
const dateA = a.date_added ?? ''
|
||||||
const dateB = b.date_added ?? ''
|
const dateB = b.date_added ?? ''
|
||||||
return dateB.localeCompare(dateA)
|
return dateB.localeCompare(dateA) || a.file_name.localeCompare(b.file_name)
|
||||||
})
|
})
|
||||||
case 'date-added-oldest':
|
case 'date-added-oldest':
|
||||||
return items.sort((a, b) => {
|
return items.sort((a, b) => {
|
||||||
const dateA = a.date_added ?? ''
|
const dateA = a.date_added ?? ''
|
||||||
const dateB = b.date_added ?? ''
|
const dateB = b.date_added ?? ''
|
||||||
return dateA.localeCompare(dateB)
|
return dateA.localeCompare(dateB) || a.file_name.localeCompare(b.file_name)
|
||||||
})
|
})
|
||||||
default:
|
default:
|
||||||
return items.sort((a, b) => {
|
return items.sort((a, b) => {
|
||||||
const nameA = a.project?.title ?? a.file_name
|
const nameA = a.project?.title ?? a.file_name
|
||||||
const nameB = b.project?.title ?? b.file_name
|
const nameB = b.project?.title ?? b.file_name
|
||||||
return nameA.toLowerCase().localeCompare(nameB.toLowerCase())
|
return (
|
||||||
|
nameA.toLowerCase().localeCompare(nameB.toLowerCase()) ||
|
||||||
|
a.file_name.localeCompare(b.file_name)
|
||||||
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { Archon, Labrinth } from '@modrinth/api-client'
|
import type { Archon, Labrinth } from '@modrinth/api-client'
|
||||||
|
import { ArrowLeftRightIcon, ClipboardCopyIcon } from '@modrinth/assets'
|
||||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/vue-query'
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/vue-query'
|
||||||
import { computed, nextTick, onBeforeUnmount, ref, watch } from 'vue'
|
import { computed, nextTick, onBeforeUnmount, ref, watch } from 'vue'
|
||||||
import { onBeforeRouteLeave, useRoute, useRouter } from 'vue-router'
|
import { onBeforeRouteLeave, useRoute, useRouter } from 'vue-router'
|
||||||
@@ -77,6 +78,14 @@ const messages = defineMessages({
|
|||||||
id: 'hosting.content.failed-to-bulk-update',
|
id: 'hosting.content.failed-to-bulk-update',
|
||||||
defaultMessage: 'Failed to update content',
|
defaultMessage: 'Failed to update content',
|
||||||
},
|
},
|
||||||
|
switchVersion: {
|
||||||
|
id: 'hosting.content.switch-version',
|
||||||
|
defaultMessage: 'Switch version',
|
||||||
|
},
|
||||||
|
copyLink: {
|
||||||
|
id: 'hosting.content.copy-link',
|
||||||
|
defaultMessage: 'Copy link',
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
@@ -628,6 +637,38 @@ async function handleUpdateItem(fileNameKey: string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function handleSwitchVersion(item: ContentItem) {
|
||||||
|
if (!item.project?.id || !item.version?.id) return
|
||||||
|
|
||||||
|
updatingModpack.value = false
|
||||||
|
updatingProject.value = item
|
||||||
|
updatingProjectVersions.value = []
|
||||||
|
loadingVersions.value = true
|
||||||
|
loadingChangelog.value = false
|
||||||
|
|
||||||
|
await nextTick()
|
||||||
|
|
||||||
|
contentUpdaterModal.value?.show(item.version.id, { switchMode: true })
|
||||||
|
|
||||||
|
try {
|
||||||
|
const versions = await client.labrinth.versions_v2.getProjectVersions(item.project.id, {
|
||||||
|
include_changelog: false,
|
||||||
|
})
|
||||||
|
versions.sort(
|
||||||
|
(a, b) => new Date(b.date_published).getTime() - new Date(a.date_published).getTime(),
|
||||||
|
)
|
||||||
|
updatingProjectVersions.value = versions
|
||||||
|
} catch (err) {
|
||||||
|
addNotification({
|
||||||
|
type: 'error',
|
||||||
|
title: formatMessage(messages.failedToLoadVersions),
|
||||||
|
text: err instanceof Error ? err.message : undefined,
|
||||||
|
})
|
||||||
|
} finally {
|
||||||
|
loadingVersions.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function handleModpackUpdate() {
|
async function handleModpackUpdate() {
|
||||||
const mp = contentQuery.data.value?.modpack
|
const mp = contentQuery.data.value?.modpack
|
||||||
if (!mp?.spec.project_id) return
|
if (!mp?.spec.project_id) return
|
||||||
@@ -780,6 +821,32 @@ function handleModpackUpdateCancel() {
|
|||||||
pendingModpackUpdateVersion.value = null
|
pendingModpackUpdateVersion.value = null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getOverflowOptions(item: ContentItem) {
|
||||||
|
const options: { id: string; icon?: typeof ArrowLeftRightIcon; action: () => void }[] = []
|
||||||
|
|
||||||
|
if (item.project?.id && item.version?.id && !item.has_update) {
|
||||||
|
options.push({
|
||||||
|
id: formatMessage(messages.switchVersion),
|
||||||
|
icon: ArrowLeftRightIcon,
|
||||||
|
action: () => handleSwitchVersion(item),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.project?.slug) {
|
||||||
|
options.push({
|
||||||
|
id: formatMessage(messages.copyLink),
|
||||||
|
icon: ClipboardCopyIcon,
|
||||||
|
action: async () => {
|
||||||
|
await navigator.clipboard.writeText(
|
||||||
|
`https://modrinth.com/${item.project_type}/${item.project?.slug}`,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return options
|
||||||
|
}
|
||||||
|
|
||||||
provideContentManager({
|
provideContentManager({
|
||||||
items: contentItems,
|
items: contentItems,
|
||||||
loading: computed(() => contentQuery.isLoading.value),
|
loading: computed(() => contentQuery.isLoading.value),
|
||||||
@@ -827,6 +894,7 @@ provideContentManager({
|
|||||||
viewModpackContent: handleViewModpackContent,
|
viewModpackContent: handleViewModpackContent,
|
||||||
unlinkModpack: handleModpackUnlink,
|
unlinkModpack: handleModpackUnlink,
|
||||||
openSettings: () => router.push(`/hosting/manage/${serverId}/options/loader`),
|
openSettings: () => router.push(`/hosting/manage/${serverId}/options/loader`),
|
||||||
|
getOverflowOptions,
|
||||||
mapToTableItem: (item) => {
|
mapToTableItem: (item) => {
|
||||||
const projectType = item.project_type ?? type.value
|
const projectType = item.project_type ?? type.value
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -515,6 +515,9 @@
|
|||||||
"header.category.resolutions": {
|
"header.category.resolutions": {
|
||||||
"defaultMessage": "Resolution"
|
"defaultMessage": "Resolution"
|
||||||
},
|
},
|
||||||
|
"hosting.content.copy-link": {
|
||||||
|
"defaultMessage": "Copy link"
|
||||||
|
},
|
||||||
"hosting.content.failed-to-bulk-delete": {
|
"hosting.content.failed-to-bulk-delete": {
|
||||||
"defaultMessage": "Failed to delete content"
|
"defaultMessage": "Failed to delete content"
|
||||||
},
|
},
|
||||||
@@ -548,6 +551,9 @@
|
|||||||
"hosting.content.failed-to-upload": {
|
"hosting.content.failed-to-upload": {
|
||||||
"defaultMessage": "Failed to upload file"
|
"defaultMessage": "Failed to upload file"
|
||||||
},
|
},
|
||||||
|
"hosting.content.switch-version": {
|
||||||
|
"defaultMessage": "Switch version"
|
||||||
|
},
|
||||||
"hosting.specs.burst": {
|
"hosting.specs.burst": {
|
||||||
"defaultMessage": "Bursts up to {cpus} CPUs"
|
"defaultMessage": "Bursts up to {cpus} CPUs"
|
||||||
},
|
},
|
||||||
@@ -818,6 +824,9 @@
|
|||||||
"instances.updater-modal.header-modpack": {
|
"instances.updater-modal.header-modpack": {
|
||||||
"defaultMessage": "Switch modpack version"
|
"defaultMessage": "Switch modpack version"
|
||||||
},
|
},
|
||||||
|
"instances.updater-modal.header-switch": {
|
||||||
|
"defaultMessage": "Switch version"
|
||||||
|
},
|
||||||
"instances.updater-modal.hide-incompatible": {
|
"instances.updater-modal.hide-incompatible": {
|
||||||
"defaultMessage": "Hide incompatible"
|
"defaultMessage": "Hide incompatible"
|
||||||
},
|
},
|
||||||
@@ -842,6 +851,9 @@
|
|||||||
"instances.updater-modal.show-incompatible": {
|
"instances.updater-modal.show-incompatible": {
|
||||||
"defaultMessage": "Show incompatible"
|
"defaultMessage": "Show incompatible"
|
||||||
},
|
},
|
||||||
|
"instances.updater-modal.switch-to": {
|
||||||
|
"defaultMessage": "Switch to {version}"
|
||||||
|
},
|
||||||
"instances.updater-modal.update-to": {
|
"instances.updater-modal.update-to": {
|
||||||
"defaultMessage": "Update to {version}"
|
"defaultMessage": "Update to {version}"
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user