Add connected library for Git modpack manifests
Some checks failed
Build / verify (push) Failing after 18m55s

This commit is contained in:
MrSphay
2026-05-03 01:43:34 +02:00
parent 4348664618
commit dd7f9de5ce
14 changed files with 1109 additions and 3 deletions

View File

@@ -14,6 +14,7 @@ import {
CompassIcon,
DownloadIcon,
ExternalIcon,
GitGraphIcon,
HomeIcon,
LeftArrowIcon,
LibraryIcon,
@@ -75,6 +76,7 @@ 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 UnknownPackWarningModal from '@/components/ui/install_flow/UnknownPackWarningModal.vue'
import { check_all_connected_packs } from '@/helpers/connected-library'
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'
@@ -425,6 +427,9 @@ initialize_state()
console.error(err)
error.showError(err, null, false, 'state_init')
})
check_all_connected_packs().catch((err) => {
console.warn('Connected Library startup check failed', err)
})
})
.catch((err) => {
stateFailed.value = true
@@ -1244,6 +1249,13 @@ provideAppUpdateDownloadProgress(appUpdateDownload)
>
<LibraryIcon />
</NavButton>
<NavButton
v-tooltip.right="'Connected Library'"
to="/library/connected"
:is-primary="(r) => r.path === '/library/connected'"
>
<GitGraphIcon />
</NavButton>
<NavButton
v-tooltip.right="'Modrinth Hosting'"
to="/hosting/manage"

View File

@@ -0,0 +1,60 @@
import { invoke } from '@tauri-apps/api/core'
export interface ConnectedPack {
id: string
sourceUrl: string
manifestUrl: string
name: string
version: string
versionId: string
mrpackUrl: string
sha512: string
changelog: string | null
profilePath: string | null
installedVersionId: string | null
autoUpdate: boolean
lastChecked: string | null
lastError: string | null
created: string
updated: string
updateAvailable: boolean
}
export interface ConnectedCheckResult {
pack: ConnectedPack
installed: boolean
}
export async function list_connected_packs(): Promise<ConnectedPack[]> {
return await invoke('plugin:connected-library|connected_library_list')
}
export async function connect_pack(sourceUrl: string): Promise<ConnectedPack> {
return await invoke('plugin:connected-library|connected_library_connect', { sourceUrl })
}
export async function remove_connected_pack(id: string): Promise<void> {
return await invoke('plugin:connected-library|connected_library_remove', { id })
}
export async function set_connected_pack_auto_update(
id: string,
autoUpdate: boolean,
): Promise<ConnectedPack> {
return await invoke('plugin:connected-library|connected_library_set_auto_update', {
id,
autoUpdate,
})
}
export async function check_connected_pack(id: string): Promise<ConnectedCheckResult> {
return await invoke('plugin:connected-library|connected_library_check', { id })
}
export async function check_all_connected_packs(): Promise<ConnectedCheckResult[]> {
return await invoke('plugin:connected-library|connected_library_check_all')
}
export async function install_connected_pack(id: string): Promise<ConnectedPack> {
return await invoke('plugin:connected-library|connected_library_install', { id })
}

View File

@@ -0,0 +1,334 @@
<script setup lang="ts">
import {
CheckIcon,
DownloadIcon,
GitGraphIcon,
PlugIcon,
RefreshCwIcon,
TrashIcon,
} from '@modrinth/assets'
import { Button, Card, injectNotificationManager, ProgressSpinner, Toggle } from '@modrinth/ui'
import { computed, onMounted, ref } from 'vue'
import {
check_all_connected_packs,
connect_pack,
install_connected_pack,
list_connected_packs,
remove_connected_pack,
set_connected_pack_auto_update,
type ConnectedPack,
} from '@/helpers/connected-library'
const { addNotification, handleError } = injectNotificationManager()
const packs = ref<ConnectedPack[]>([])
const sourceUrl = ref('')
const connecting = ref(false)
const checking = ref(false)
const installing = ref(new Set<string>())
const hasPacks = computed(() => packs.value.length > 0)
async function refresh() {
packs.value = await list_connected_packs().catch(handleError)
}
async function connect() {
if (!sourceUrl.value.trim()) return
connecting.value = true
try {
const pack = await connect_pack(sourceUrl.value.trim())
sourceUrl.value = ''
await refresh()
addNotification({
title: 'Connected modpack',
text: `${pack.name} is now in your Connected Library.`,
type: 'success',
})
} catch (error) {
handleError(error)
} finally {
connecting.value = false
}
}
async function checkForUpdates() {
checking.value = true
try {
await check_all_connected_packs()
await refresh()
} catch (error) {
handleError(error)
} finally {
checking.value = false
}
}
async function installPack(pack: ConnectedPack) {
installing.value = new Set(installing.value).add(pack.id)
try {
await install_connected_pack(pack.id)
await refresh()
addNotification({
title: pack.profilePath ? 'Updated modpack' : 'Installed modpack',
text: `${pack.name} is ready in your Library.`,
type: 'success',
})
} catch (error) {
handleError(error)
} finally {
const next = new Set(installing.value)
next.delete(pack.id)
installing.value = next
}
}
async function toggleAutoUpdate(pack: ConnectedPack, value: boolean) {
try {
await set_connected_pack_auto_update(pack.id, value)
await refresh()
} catch (error) {
handleError(error)
}
}
async function removePack(pack: ConnectedPack) {
try {
await remove_connected_pack(pack.id)
await refresh()
} catch (error) {
handleError(error)
}
}
onMounted(refresh)
</script>
<template>
<div class="connected-library">
<section class="connect-row">
<div class="input-wrap">
<GitGraphIcon />
<input
v-model="sourceUrl"
type="url"
placeholder="Git repo URL or raw modrinth-plus.json URL"
@keydown.enter="connect"
/>
</div>
<Button color="primary" :disabled="connecting || !sourceUrl.trim()" @click="connect">
<ProgressSpinner v-if="connecting" />
<PlugIcon v-else />
Connect modpack
</Button>
<Button :disabled="checking || !hasPacks" @click="checkForUpdates">
<ProgressSpinner v-if="checking" />
<RefreshCwIcon v-else />
Check for updates
</Button>
</section>
<div v-if="hasPacks" class="pack-list">
<Card v-for="pack in packs" :key="pack.id" class="pack-card">
<div class="pack-main">
<div class="pack-title">
<h2>{{ pack.name }}</h2>
<span v-if="pack.updateAvailable" class="status update">Update available</span>
<span v-else-if="pack.profilePath" class="status current">Current</span>
<span v-else class="status">Not installed</span>
</div>
<p v-if="pack.changelog">{{ pack.changelog }}</p>
<div class="meta">
<span>Latest {{ pack.version }}</span>
<span v-if="pack.installedVersionId">Installed {{ pack.installedVersionId }}</span>
<span v-if="pack.lastChecked">Checked {{ new Date(pack.lastChecked).toLocaleString() }}</span>
</div>
<p v-if="pack.lastError" class="error">{{ pack.lastError }}</p>
</div>
<div class="pack-actions">
<label class="auto-update">
<Toggle
:model-value="pack.autoUpdate"
@update:model-value="toggleAutoUpdate(pack, $event)"
/>
Auto update
</label>
<Button
v-if="pack.updateAvailable || !pack.profilePath"
color="primary"
:disabled="installing.has(pack.id)"
@click="installPack(pack)"
>
<ProgressSpinner v-if="installing.has(pack.id)" />
<DownloadIcon v-else />
{{ pack.profilePath ? 'Update' : 'Install' }}
</Button>
<Button v-else disabled>
<CheckIcon />
Installed
</Button>
<Button color="danger" @click="removePack(pack)">
<TrashIcon />
Remove
</Button>
</div>
</Card>
</div>
<div v-else class="empty">
<GitGraphIcon />
<h2>No connected modpacks</h2>
<p>Add a public Git repository or raw manifest URL to track exported .mrpack releases.</p>
</div>
</div>
</template>
<style scoped lang="scss">
.connected-library {
display: flex;
flex-direction: column;
gap: var(--gap-lg);
}
.connect-row {
display: grid;
grid-template-columns: minmax(16rem, 1fr) auto auto;
gap: var(--gap-sm);
align-items: center;
}
.input-wrap {
display: flex;
align-items: center;
gap: var(--gap-sm);
min-width: 0;
padding: 0.625rem 0.75rem;
border: 1px solid var(--color-raised-bg);
border-radius: var(--radius-md);
background: var(--color-bg);
svg {
width: 1rem;
height: 1rem;
flex: none;
color: var(--color-brand);
}
input {
width: 100%;
min-width: 0;
border: 0;
outline: 0;
background: transparent;
color: var(--color-text);
}
}
.pack-list {
display: flex;
flex-direction: column;
gap: var(--gap-md);
}
.pack-card {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
gap: var(--gap-lg);
align-items: start;
}
.pack-main {
display: flex;
flex-direction: column;
gap: var(--gap-xs);
min-width: 0;
h2,
p {
margin: 0;
}
}
.pack-title {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: var(--gap-sm);
}
.status {
border-radius: var(--radius-sm);
padding: 0.125rem 0.375rem;
background: var(--color-raised-bg);
font-size: var(--font-size-xs);
&.update {
color: var(--color-brand);
}
&.current {
color: var(--color-green);
}
}
.meta {
display: flex;
flex-wrap: wrap;
gap: var(--gap-sm);
color: var(--color-secondary);
font-size: var(--font-size-sm);
}
.error {
color: var(--color-red);
}
.pack-actions {
display: flex;
align-items: center;
gap: var(--gap-sm);
flex-wrap: wrap;
justify-content: flex-end;
}
.auto-update {
display: flex;
align-items: center;
gap: var(--gap-xs);
white-space: nowrap;
}
.empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 18rem;
gap: var(--gap-sm);
text-align: center;
color: var(--color-secondary);
svg {
width: 3rem;
height: 3rem;
color: var(--color-brand);
}
h2,
p {
margin: 0;
}
}
@media (max-width: 860px) {
.connect-row,
.pack-card {
grid-template-columns: 1fr;
}
.pack-actions {
justify-content: flex-start;
}
}
</style>

View File

@@ -41,13 +41,14 @@ onUnmounted(() => {
:links="[
{ label: 'All instances', href: `/library` },
{ label: 'Modpacks', href: `/library/modpacks` },
{ label: 'Connected Library', href: `/library/connected` },
{ label: 'Servers', href: `/library/servers` },
{ label: 'Custom', href: `/library/custom` },
{ label: 'Shared with me', href: `/library/shared`, shown: false },
{ label: 'Saved', href: `/library/saved`, shown: false },
]"
/>
<template v-if="instances && instances.length > 0">
<template v-if="route.path.startsWith('/library/connected') || (instances && instances.length > 0)">
<RouterView v-if="route.path.startsWith('/library')" :instances="instances" />
</template>
<div v-else class="no-instance">

View File

@@ -1,8 +1,9 @@
import Custom from './Custom.vue'
import Connected from './Connected.vue'
import Downloaded from './Downloaded.vue'
import Index from './Index.vue'
import Modpacks from './Modpacks.vue'
import Overview from './Overview.vue'
import Servers from './Servers.vue'
export { Custom, Downloaded, Index, Modpacks, Overview, Servers }
export { Connected, Custom, Downloaded, Index, Modpacks, Overview, Servers }

View File

@@ -115,6 +115,11 @@ export default new createRouter({
name: 'Modpacks',
component: Library.Modpacks,
},
{
path: 'connected',
name: 'ConnectedLibrary',
component: Library.Connected,
},
{
path: 'servers',
name: 'LibraryServers',

View File

@@ -0,0 +1,62 @@
use crate::api::Result;
use theseus::connected_library::{
ConnectedCheckResult, ConnectedPack, check, check_all, connect, install,
list, remove, set_auto_update,
};
pub fn init<R: tauri::Runtime>() -> tauri::plugin::TauriPlugin<R> {
tauri::plugin::Builder::new("connected-library")
.invoke_handler(tauri::generate_handler![
connected_library_list,
connected_library_connect,
connected_library_remove,
connected_library_set_auto_update,
connected_library_check,
connected_library_check_all,
connected_library_install,
])
.build()
}
#[tauri::command]
pub async fn connected_library_list() -> Result<Vec<ConnectedPack>> {
Ok(list().await?)
}
#[tauri::command]
pub async fn connected_library_connect(
source_url: String,
) -> Result<ConnectedPack> {
Ok(connect(source_url).await?)
}
#[tauri::command]
pub async fn connected_library_remove(id: String) -> Result<()> {
Ok(remove(id).await?)
}
#[tauri::command]
pub async fn connected_library_set_auto_update(
id: String,
auto_update: bool,
) -> Result<ConnectedPack> {
Ok(set_auto_update(id, auto_update).await?)
}
#[tauri::command]
pub async fn connected_library_check(
id: String,
) -> Result<ConnectedCheckResult> {
Ok(check(id).await?)
}
#[tauri::command]
pub async fn connected_library_check_all() -> Result<Vec<ConnectedCheckResult>>
{
Ok(check_all().await?)
}
#[tauri::command]
pub async fn connected_library_install(id: String) -> Result<ConnectedPack> {
Ok(install(id).await?)
}

View File

@@ -19,6 +19,7 @@ pub mod utils;
pub mod ads;
pub mod cache;
pub mod connected_library;
pub mod files;
pub mod friends;
pub mod worlds;

View File

@@ -233,6 +233,7 @@ fn main() {
.plugin(api::tags::init())
.plugin(api::utils::init())
.plugin(api::cache::init())
.plugin(api::connected_library::init())
.plugin(api::files::init())
.plugin(api::ads::init())
.plugin(api::friends::init())