Add connected library for Git modpack manifests
Some checks failed
Build / verify (push) Failing after 18m55s
Some checks failed
Build / verify (push) Failing after 18m55s
This commit is contained in:
@@ -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"
|
||||
|
||||
60
apps/app-frontend/src/helpers/connected-library.ts
Normal file
60
apps/app-frontend/src/helpers/connected-library.ts
Normal 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 })
|
||||
}
|
||||
334
apps/app-frontend/src/pages/library/Connected.vue
Normal file
334
apps/app-frontend/src/pages/library/Connected.vue
Normal 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>
|
||||
@@ -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">
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -115,6 +115,11 @@ export default new createRouter({
|
||||
name: 'Modpacks',
|
||||
component: Library.Modpacks,
|
||||
},
|
||||
{
|
||||
path: 'connected',
|
||||
name: 'ConnectedLibrary',
|
||||
component: Library.Connected,
|
||||
},
|
||||
{
|
||||
path: 'servers',
|
||||
name: 'LibraryServers',
|
||||
|
||||
Reference in New Issue
Block a user