feat: backups page cleanup before worlds (#5844)
* feat: card alignment + fix modals * feat: change admon title in restore alert modal * fix: lint * feat: backups queue api into api-client * feat: impl backup queue api endpoints into frontend * feat: ack fix * feat: bulk actions * feat: bulk delete impl * fix: lint * fix: align error states * fix: transition group * feat: ready for qa * fix: lint * feat: qa * feat: stacked admonitions component * fix: issues with stacking * feat: hook up admonition stacking + fix app csp for staging kyros nodes * fix: logs.vue * qa: close stack on admonitions click * fix: all problems with stacked admonitions * qa: admonition cleanup and copy overhaul draft * fix: qa issues padding * fix: padding bug * feat: qa * fix: intercom in app csp bug * fix: positioning intercom * feat: loading overlay on top of console + admon consistency changes * feat: scroll indicator fade in backup delete modal + admon timestamp fix * feat: move action bar behind modal * fix: lint + i18n * fix: server ping spam on filter (cache but clear on unmount) * fix: 1 admon fade in flicker issue * chore: temp staging undo * qa: changes * fix: lint * chore: revert staging to use staging * fix: scoping
This commit is contained in:
@@ -13,6 +13,7 @@
|
||||
"test": "vue-tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@intercom/messenger-js-sdk": "^0.0.14",
|
||||
"@modrinth/api-client": "workspace:^",
|
||||
"@modrinth/assets": "workspace:*",
|
||||
"@modrinth/ui": "workspace:*",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script setup>
|
||||
import { Intercom, shutdown as shutdownIntercom } from '@intercom/messenger-js-sdk'
|
||||
import {
|
||||
AuthFeature,
|
||||
NodeAuthFeature,
|
||||
@@ -238,6 +239,7 @@ onMounted(async () => {
|
||||
onUnmounted(async () => {
|
||||
document.querySelector('body').removeEventListener('click', handleClick)
|
||||
document.querySelector('body').removeEventListener('auxclick', handleAuxClick)
|
||||
shutdownHostingIntercom()
|
||||
|
||||
await unlistenUpdateDownload?.()
|
||||
})
|
||||
@@ -652,6 +654,102 @@ const sidebarVisible = computed(() => sidebarToggled.value || forceSidebar.value
|
||||
const showAd = computed(
|
||||
() => sidebarVisible.value && !hasPlus.value && credentials.value !== undefined,
|
||||
)
|
||||
const hostingRouteActive = computed(() => route.path.startsWith('/hosting'))
|
||||
const INTERCOM_DEFAULT_PADDING = 20
|
||||
const INTERCOM_APP_SIDEBAR_WIDTH = 300
|
||||
|
||||
let intercomBooting = false
|
||||
let intercomBooted = false
|
||||
|
||||
async function fetchIntercomToken() {
|
||||
const creds = await getCreds()
|
||||
if (!creds?.session) {
|
||||
throw new Error('Not authenticated')
|
||||
}
|
||||
|
||||
const params = new URLSearchParams()
|
||||
if (route.path.startsWith('/hosting/manage/') && typeof route.params.id === 'string') {
|
||||
params.set('server_id', route.params.id)
|
||||
}
|
||||
const query = params.size > 0 ? `?${params.toString()}` : ''
|
||||
|
||||
const response = await tauriFetch(`${config.siteUrl}/api/intercom/messenger-jwt${query}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Authorization: `Bearer ${creds.session}`,
|
||||
},
|
||||
})
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch Intercom token: ${response.status}`)
|
||||
}
|
||||
return await response.json()
|
||||
}
|
||||
|
||||
async function bootIntercom() {
|
||||
if (
|
||||
intercomBooting ||
|
||||
intercomBooted ||
|
||||
!hostingRouteActive.value ||
|
||||
!credentials.value?.session
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
intercomBooting = true
|
||||
console.debug('[APP][INTERCOM] initializing secure support chat')
|
||||
try {
|
||||
const { token } = await fetchIntercomToken()
|
||||
Intercom({
|
||||
app_id: 'ykeritl9',
|
||||
intercom_user_jwt: token,
|
||||
session_duration: 1000 * 60 * 60 * 24,
|
||||
alignment: 'right',
|
||||
horizontal_padding: sidebarVisible.value
|
||||
? INTERCOM_APP_SIDEBAR_WIDTH + INTERCOM_DEFAULT_PADDING
|
||||
: INTERCOM_DEFAULT_PADDING,
|
||||
vertical_padding: INTERCOM_DEFAULT_PADDING,
|
||||
})
|
||||
intercomBooted = true
|
||||
} catch (error) {
|
||||
console.warn('[APP][INTERCOM] failed to initialize secure support chat', error)
|
||||
} finally {
|
||||
intercomBooting = false
|
||||
}
|
||||
}
|
||||
|
||||
function shutdownHostingIntercom() {
|
||||
if (!intercomBooted && !intercomBooting) return
|
||||
shutdownIntercom()
|
||||
intercomBooting = false
|
||||
intercomBooted = false
|
||||
}
|
||||
|
||||
watch(
|
||||
sidebarVisible,
|
||||
(visible) => {
|
||||
if (intercomBooted) {
|
||||
window.Intercom?.('update', {
|
||||
horizontal_padding: visible
|
||||
? INTERCOM_APP_SIDEBAR_WIDTH + INTERCOM_DEFAULT_PADDING
|
||||
: INTERCOM_DEFAULT_PADDING,
|
||||
vertical_padding: INTERCOM_DEFAULT_PADDING,
|
||||
})
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
watch(
|
||||
[hostingRouteActive, credentials],
|
||||
([active]) => {
|
||||
if (active) {
|
||||
void bootIntercom()
|
||||
} else {
|
||||
shutdownHostingIntercom()
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
watch(showAd, () => {
|
||||
if (!showAd.value) {
|
||||
|
||||
@@ -180,13 +180,6 @@ img {
|
||||
}
|
||||
}
|
||||
|
||||
button,
|
||||
input[type='button'] {
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
outline: 2px solid transparent;
|
||||
}
|
||||
|
||||
@import '@modrinth/assets/omorphia.scss';
|
||||
|
||||
input {
|
||||
|
||||
@@ -30,10 +30,10 @@
|
||||
"message": "Discover servers"
|
||||
},
|
||||
"app.browse.hide-added-servers": {
|
||||
"message": "Hide added servers"
|
||||
"message": "Hide already added servers"
|
||||
},
|
||||
"app.browse.hide-installed-content": {
|
||||
"message": "Hide installed content"
|
||||
"message": "Hide already installed content"
|
||||
},
|
||||
"app.browse.install-content-to-instance": {
|
||||
"message": "Install content to instance"
|
||||
|
||||
@@ -127,6 +127,8 @@ const instance: Ref<Instance | null> = ref(null)
|
||||
const installedProjectIds: Ref<string[] | null> = ref(null)
|
||||
const instanceHideInstalled = ref(false)
|
||||
const newlyInstalled = ref<string[]>([])
|
||||
const hiddenInstanceProjectIds = ref<Set<string>>(new Set())
|
||||
const hiddenInstanceProjectIdsInitialized = ref(false)
|
||||
const isServerInstance = ref(false)
|
||||
|
||||
if (isFromWorlds.value && route.params.projectType !== 'server') {
|
||||
@@ -142,6 +144,25 @@ const allInstalledIds = computed(
|
||||
() => new Set([...newlyInstalled.value, ...(installedProjectIds.value ?? [])]),
|
||||
)
|
||||
|
||||
function syncHiddenInstanceProjectIds() {
|
||||
hiddenInstanceProjectIds.value = new Set([
|
||||
...(installedProjectIds.value ?? []),
|
||||
...newlyInstalled.value,
|
||||
])
|
||||
hiddenInstanceProjectIdsInitialized.value = true
|
||||
}
|
||||
|
||||
watch(
|
||||
installedProjectIds,
|
||||
(ids) => {
|
||||
if (!ids) return
|
||||
if (!hiddenInstanceProjectIdsInitialized.value) {
|
||||
syncHiddenInstanceProjectIds()
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
watchServerContextChanges()
|
||||
|
||||
await initInstanceContext()
|
||||
@@ -222,14 +243,10 @@ const instanceFilters = computed(() => {
|
||||
filters.push({ type: 'environment', option: 'client' })
|
||||
}
|
||||
|
||||
if (
|
||||
instanceHideInstalled.value &&
|
||||
(installedProjectIds.value || newlyInstalled.value.length > 0)
|
||||
) {
|
||||
const allInstalled = [...(installedProjectIds.value ?? []), ...newlyInstalled.value]
|
||||
allInstalled
|
||||
.map((x) => ({ type: 'project_id', option: `project_id:${x}`, negative: true }))
|
||||
.forEach((x) => filters.push(x))
|
||||
if (instanceHideInstalled.value && hiddenInstanceProjectIds.value.size > 0) {
|
||||
for (const id of hiddenInstanceProjectIds.value) {
|
||||
filters.push({ type: 'project_id', option: `project_id:${id}`, negative: true })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -240,6 +257,23 @@ const serverHideInstalled = ref(false)
|
||||
if (route.query.shi) {
|
||||
serverHideInstalled.value = route.query.shi === 'true'
|
||||
}
|
||||
const hiddenServerContentProjectIds = ref<Set<string>>(new Set())
|
||||
const hiddenServerContentProjectIdsInitialized = ref(false)
|
||||
|
||||
function syncHiddenServerContentProjectIds() {
|
||||
hiddenServerContentProjectIds.value = new Set(serverContentProjectIds.value)
|
||||
hiddenServerContentProjectIdsInitialized.value = true
|
||||
}
|
||||
|
||||
watch(
|
||||
serverContentProjectIds,
|
||||
() => {
|
||||
if (!hiddenServerContentProjectIdsInitialized.value) {
|
||||
syncHiddenServerContentProjectIds()
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
const serverContextFilters = computed(() => {
|
||||
const filters: { type: string; option: string; negative?: boolean }[] = []
|
||||
@@ -266,8 +300,8 @@ const serverContextFilters = computed(() => {
|
||||
)
|
||||
}
|
||||
|
||||
if (serverHideInstalled.value && serverContentProjectIds.value.size > 0) {
|
||||
for (const id of serverContentProjectIds.value) {
|
||||
if (serverHideInstalled.value && hiddenServerContentProjectIds.value.size > 0) {
|
||||
for (const id of hiddenServerContentProjectIds.value) {
|
||||
filters.push({ type: 'project_id', option: `project_id:${id}`, negative: true })
|
||||
}
|
||||
}
|
||||
@@ -280,6 +314,9 @@ const combinedProvidedFilters = computed(() =>
|
||||
)
|
||||
|
||||
const serverPings = shallowRef<Record<string, number | undefined>>({})
|
||||
const serverPingCache = new Map<string, number | undefined>()
|
||||
const pendingServerPings = new Map<string, Promise<number | undefined>>()
|
||||
let serverPingCacheActive = true
|
||||
const runningServerProjects = ref<Record<string, string>>({})
|
||||
|
||||
async function checkServerRunningStates(hits: Labrinth.Search.v3.ResultSearchProject[]) {
|
||||
@@ -342,16 +379,44 @@ async function handleAddServerToInstance(project: Labrinth.Search.v3.ResultSearc
|
||||
|
||||
async function pingServerHits(hits: Labrinth.Search.v3.ResultSearchProject[]) {
|
||||
debugLog('pingServerHits', { hitCount: hits.length })
|
||||
const pingsToFetch = hits.filter((hit) => hit.minecraft_java_server?.address)
|
||||
const pingsToFetch = hits.flatMap((hit) => {
|
||||
const address = hit.minecraft_java_server?.address
|
||||
if (!address) return []
|
||||
return [{ hit, address }]
|
||||
})
|
||||
const nextPings = { ...serverPings.value }
|
||||
for (const { hit, address } of pingsToFetch) {
|
||||
if (serverPingCache.has(address)) {
|
||||
nextPings[hit.project_id] = serverPingCache.get(address)
|
||||
}
|
||||
}
|
||||
serverPings.value = nextPings
|
||||
|
||||
await Promise.all(
|
||||
pingsToFetch.map(async (hit) => {
|
||||
const address = hit.minecraft_java_server!.address!
|
||||
try {
|
||||
const latency = await getServerLatency(address)
|
||||
serverPings.value = { ...serverPings.value, [hit.project_id]: latency }
|
||||
} catch (err) {
|
||||
console.error(`Failed to ping server ${address}:`, err)
|
||||
pingsToFetch.map(async ({ hit, address }) => {
|
||||
if (serverPingCache.has(address)) return
|
||||
|
||||
let pending = pendingServerPings.get(address)
|
||||
if (!pending) {
|
||||
pending = getServerLatency(address)
|
||||
.then((latency) => {
|
||||
if (serverPingCacheActive) serverPingCache.set(address, latency)
|
||||
return latency
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(`Failed to ping server ${address}:`, err)
|
||||
if (serverPingCacheActive) serverPingCache.set(address, undefined)
|
||||
return undefined
|
||||
})
|
||||
.finally(() => {
|
||||
pendingServerPings.delete(address)
|
||||
})
|
||||
pendingServerPings.set(address, pending)
|
||||
}
|
||||
|
||||
const latency = await pending
|
||||
if (!serverPingCacheActive) return
|
||||
serverPings.value = { ...serverPings.value, [hit.project_id]: latency }
|
||||
}),
|
||||
)
|
||||
}
|
||||
@@ -372,7 +437,10 @@ const unlistenProcesses = await process_listener(
|
||||
)
|
||||
|
||||
onUnmounted(() => {
|
||||
serverPingCacheActive = false
|
||||
unlistenProcesses()
|
||||
serverPingCache.clear()
|
||||
pendingServerPings.clear()
|
||||
})
|
||||
|
||||
const offline = ref(!navigator.onLine)
|
||||
@@ -432,11 +500,11 @@ const messages = defineMessages({
|
||||
},
|
||||
hideAddedServers: {
|
||||
id: 'app.browse.hide-added-servers',
|
||||
defaultMessage: 'Hide added servers',
|
||||
defaultMessage: 'Hide already added servers',
|
||||
},
|
||||
hideInstalledContent: {
|
||||
id: 'app.browse.hide-installed-content',
|
||||
defaultMessage: 'Hide installed content',
|
||||
defaultMessage: 'Hide already installed content',
|
||||
},
|
||||
installContentToInstance: {
|
||||
id: 'app.browse.install-content-to-instance',
|
||||
@@ -663,6 +731,16 @@ const installContext = computed(() => {
|
||||
|
||||
const installingProjectIds = ref<Set<string>>(new Set())
|
||||
|
||||
function setProjectInstalling(projectId: string, installing: boolean) {
|
||||
const next = new Set(installingProjectIds.value)
|
||||
if (installing) {
|
||||
next.add(projectId)
|
||||
} else {
|
||||
next.delete(projectId)
|
||||
}
|
||||
installingProjectIds.value = next
|
||||
}
|
||||
|
||||
function getCardActions(
|
||||
result: Labrinth.Search.v2.ResultSearchProject | Labrinth.Search.v3.ResultSearchProject,
|
||||
currentProjectType: string,
|
||||
@@ -757,11 +835,12 @@ function getCardActions(
|
||||
: messages.installToServer,
|
||||
),
|
||||
icon: isInstalled ? CheckIcon : PlusIcon,
|
||||
iconClass: isInstalling ? 'animate-spin' : undefined,
|
||||
disabled: isInstalled || isInstalling,
|
||||
color: 'brand',
|
||||
type: 'outlined',
|
||||
onClick: async () => {
|
||||
installingProjectIds.value.add(projectResult.project_id)
|
||||
setProjectInstalling(projectResult.project_id, true)
|
||||
try {
|
||||
const didInstall = await installProjectToServer(projectResult)
|
||||
if (didInstall !== false) {
|
||||
@@ -770,7 +849,7 @@ function getCardActions(
|
||||
} catch (err) {
|
||||
handleError(err as Error)
|
||||
} finally {
|
||||
installingProjectIds.value.delete(projectResult.project_id)
|
||||
setProjectInstalling(projectResult.project_id, false)
|
||||
}
|
||||
},
|
||||
},
|
||||
@@ -791,18 +870,19 @@ function getCardActions(
|
||||
? 'Install'
|
||||
: 'Add to an instance',
|
||||
icon: isInstalling ? SpinnerIcon : isInstalled ? CheckIcon : PlusIcon,
|
||||
iconClass: isInstalling ? 'animate-spin' : undefined,
|
||||
disabled: isInstalled || isInstalling,
|
||||
color: 'brand',
|
||||
type: 'outlined',
|
||||
onClick: async () => {
|
||||
installingProjectIds.value.add(projectResult.project_id)
|
||||
setProjectInstalling(projectResult.project_id, true)
|
||||
await installVersion(
|
||||
projectResult.project_id,
|
||||
null,
|
||||
instance.value ? instance.value.path : null,
|
||||
'SearchCard',
|
||||
(versionId) => {
|
||||
installingProjectIds.value.delete(projectResult.project_id)
|
||||
setProjectInstalling(projectResult.project_id, false)
|
||||
if (versionId) {
|
||||
onSearchResultInstalled(projectResult.project_id)
|
||||
}
|
||||
@@ -815,7 +895,7 @@ function getCardActions(
|
||||
preferredGameVersion: instance.value?.game_version ?? undefined,
|
||||
},
|
||||
).catch((err) => {
|
||||
installingProjectIds.value.delete(projectResult.project_id)
|
||||
setProjectInstalling(projectResult.project_id, false)
|
||||
handleError(err)
|
||||
})
|
||||
},
|
||||
@@ -858,7 +938,6 @@ async function search(requestParams: string) {
|
||||
if (isServer) {
|
||||
const hits = rawResults.result.hits ?? []
|
||||
lastServerHits.value = hits
|
||||
serverPings.value = {}
|
||||
pingServerHits(hits)
|
||||
checkServerRunningStates(hits)
|
||||
return {
|
||||
@@ -928,6 +1007,23 @@ const searchState = useBrowseSearch({
|
||||
}),
|
||||
})
|
||||
|
||||
watch(
|
||||
[
|
||||
() => searchState.query.value,
|
||||
() => searchState.currentFilters.value,
|
||||
() => searchState.serverCurrentFilters.value,
|
||||
() => projectType.value,
|
||||
],
|
||||
() => {
|
||||
if (isServerContext.value) {
|
||||
syncHiddenServerContentProjectIds()
|
||||
} else if (instance.value) {
|
||||
syncHiddenInstanceProjectIds()
|
||||
}
|
||||
},
|
||||
{ deep: true },
|
||||
)
|
||||
|
||||
if (instance.value?.game_version) {
|
||||
const gv = instance.value.game_version
|
||||
const alreadyHasGv = searchState.serverCurrentFilters.value.some(
|
||||
@@ -959,8 +1055,13 @@ provideBrowseManager({
|
||||
hideInstalled: computed({
|
||||
get: () => (isServerContext.value ? serverHideInstalled.value : instanceHideInstalled.value),
|
||||
set: (val: boolean) => {
|
||||
if (isServerContext.value) serverHideInstalled.value = val
|
||||
else instanceHideInstalled.value = val
|
||||
if (isServerContext.value) {
|
||||
serverHideInstalled.value = val
|
||||
if (val) syncHiddenServerContentProjectIds()
|
||||
} else {
|
||||
instanceHideInstalled.value = val
|
||||
if (val) syncHiddenInstanceProjectIds()
|
||||
}
|
||||
},
|
||||
}),
|
||||
showHideInstalled: computed(
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
:resolve-viewer="resolveViewer"
|
||||
:show-copy-id-action="themeStore.devMode"
|
||||
:auth-user="authUser"
|
||||
:fetch-intercom-token="fetchIntercomToken"
|
||||
:navigate-to-billing="() => openUrl('https://modrinth.com/settings/billing')"
|
||||
:navigate-to-servers="() => router.push('/hosting/manage')"
|
||||
:browse-modpacks="
|
||||
@@ -47,12 +46,10 @@
|
||||
import type { Archon, Labrinth } from '@modrinth/api-client'
|
||||
import { injectAuth, injectModrinthClient, ServersManageRootLayout } from '@modrinth/ui'
|
||||
import { useQuery, useQueryClient } from '@tanstack/vue-query'
|
||||
import { fetch as tauriFetch } from '@tauri-apps/plugin-http'
|
||||
import { openUrl } from '@tauri-apps/plugin-opener'
|
||||
import { computed, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
|
||||
import { config } from '@/config'
|
||||
import { get_user } from '@/helpers/cache'
|
||||
import { get as getCreds } from '@/helpers/mr_auth'
|
||||
import { useBreadcrumbs } from '@/store/breadcrumbs'
|
||||
@@ -123,26 +120,6 @@ const authUser = computed(() => {
|
||||
}
|
||||
})
|
||||
|
||||
async function fetchIntercomToken(): Promise<{ token: string }> {
|
||||
const credentials = await getCreds()
|
||||
if (!credentials?.session) {
|
||||
throw new Error('Not authenticated')
|
||||
}
|
||||
const response = await tauriFetch(
|
||||
`${config.siteUrl}/api/intercom/messenger-jwt?server_id=${encodeURIComponent(serverId.value)}`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Authorization: `Bearer ${credentials.session}`,
|
||||
},
|
||||
},
|
||||
)
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch Intercom token: ${response.status}`)
|
||||
}
|
||||
return (await response.json()) as { token: string }
|
||||
}
|
||||
|
||||
async function resolveViewer(): Promise<{ userId: string | null; userRole: string | null }> {
|
||||
const credentials = await getCreds().catch(() => null)
|
||||
if (!credentials?.user_id) {
|
||||
|
||||
@@ -240,7 +240,7 @@ export default new createRouter({
|
||||
component: Instance.Logs,
|
||||
meta: {
|
||||
useRootContext: true,
|
||||
renderMode: 'fixed',
|
||||
// renderMode: 'fixed',
|
||||
breadcrumb: [{ name: '?Instance', link: '/instance/{id}/' }, { name: 'Logs' }],
|
||||
},
|
||||
},
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
"identifier": "plugins",
|
||||
"description": "",
|
||||
"local": true,
|
||||
"windows": ["main"],
|
||||
"windows": [
|
||||
"main"
|
||||
],
|
||||
"permissions": [
|
||||
"dialog:allow-open",
|
||||
"dialog:allow-confirm",
|
||||
@@ -19,21 +21,36 @@
|
||||
"window-state:default",
|
||||
"window-state:allow-restore-state",
|
||||
"window-state:allow-save-window-state",
|
||||
|
||||
{
|
||||
"identifier": "http:default",
|
||||
"allow": [
|
||||
{ "url": "https://modrinth.com/*" },
|
||||
{ "url": "https://*.modrinth.com/*" },
|
||||
{ "url": "https://*.nodes.modrinth.com/*" },
|
||||
{ "url": "https://api.mclo.gs/*" },
|
||||
{ "url": "https://fill.papermc.io/*" },
|
||||
{ "url": "https://api.purpurmc.org/*" }
|
||||
{
|
||||
"url": "https://modrinth.com/*"
|
||||
},
|
||||
{
|
||||
"url": "https://*.modrinth.com/*"
|
||||
},
|
||||
{
|
||||
"url": "https://*.nodes.modrinth.com/*"
|
||||
},
|
||||
{
|
||||
"url": "https://api.mclo.gs/*"
|
||||
},
|
||||
{
|
||||
"url": "https://fill.papermc.io/*"
|
||||
},
|
||||
{
|
||||
"url": "https://api.purpurmc.org/*"
|
||||
},
|
||||
{
|
||||
"url": "http://*.taila228c5.ts.net/*"
|
||||
},
|
||||
{
|
||||
"url": "https://*.taila228c5.ts.net/*"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
"dialog:allow-save",
|
||||
|
||||
"fs:allow-read-dir",
|
||||
"fs:allow-read-file",
|
||||
"fs:allow-read-text-file",
|
||||
@@ -49,15 +66,26 @@
|
||||
{
|
||||
"identifier": "fs:scope",
|
||||
"allow": [
|
||||
{ "path": "$APPDATA/profiles" },
|
||||
{ "path": "$APPDATA/profiles/**" },
|
||||
{ "path": "$APPCONFIG/profiles" },
|
||||
{ "path": "$APPCONFIG/profiles/**" },
|
||||
{ "path": "$CONFIG/profiles" },
|
||||
{ "path": "$CONFIG/profiles/**" }
|
||||
{
|
||||
"path": "$APPDATA/profiles"
|
||||
},
|
||||
{
|
||||
"path": "$APPDATA/profiles/**"
|
||||
},
|
||||
{
|
||||
"path": "$APPCONFIG/profiles"
|
||||
},
|
||||
{
|
||||
"path": "$APPCONFIG/profiles/**"
|
||||
},
|
||||
{
|
||||
"path": "$CONFIG/profiles"
|
||||
},
|
||||
{
|
||||
"path": "$CONFIG/profiles/**"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
"auth:default",
|
||||
"import:default",
|
||||
"jre:default",
|
||||
|
||||
@@ -12,7 +12,12 @@
|
||||
"copyright": "",
|
||||
"targets": "all",
|
||||
"externalBin": [],
|
||||
"icon": ["icons/128x128.png", "icons/128x128@2x.png", "icons/icon.icns", "icons/icon.ico"],
|
||||
"icon": [
|
||||
"icons/128x128.png",
|
||||
"icons/128x128@2x.png",
|
||||
"icons/icon.icns",
|
||||
"icons/icon.ico"
|
||||
],
|
||||
"windows": {
|
||||
"nsis": {
|
||||
"installMode": "currentUser",
|
||||
@@ -35,7 +40,9 @@
|
||||
},
|
||||
"fileAssociations": [
|
||||
{
|
||||
"ext": ["mrpack"],
|
||||
"ext": [
|
||||
"mrpack"
|
||||
],
|
||||
"mimeType": "application/x-modrinth-modpack+zip"
|
||||
}
|
||||
]
|
||||
@@ -47,7 +54,9 @@
|
||||
"plugins": {
|
||||
"deep-link": {
|
||||
"desktop": {
|
||||
"schemes": ["modrinth"]
|
||||
"schemes": [
|
||||
"modrinth"
|
||||
]
|
||||
},
|
||||
"mobile": []
|
||||
}
|
||||
@@ -84,15 +93,22 @@
|
||||
],
|
||||
"enable": true
|
||||
},
|
||||
"capabilities": ["ads", "core", "plugins"],
|
||||
"capabilities": [
|
||||
"ads",
|
||||
"core",
|
||||
"plugins"
|
||||
],
|
||||
"csp": {
|
||||
"default-src": "'self' customprotocol: asset:",
|
||||
"connect-src": "ipc: http://ipc.localhost https://modrinth.com https://*.modrinth.com https://*.nodes.modrinth.com https://*.posthog.com https://posthog.modrinth.com https://*.sentry.io https://api.mclo.gs http://textures.minecraft.net https://textures.minecraft.net https://js.stripe.com https://*.stripe.com wss://*.stripe.com wss://*.nodes.modrinth.com wss://*.ts.net https://fill.papermc.io https://api.purpurmc.org 'self' data: blob:",
|
||||
"font-src": ["https://cdn-raw.modrinth.com/fonts/"],
|
||||
"connect-src": "ipc: http://ipc.localhost https://modrinth.com https://*.modrinth.com https://*.nodes.modrinth.com https://*.posthog.com https://posthog.modrinth.com https://*.sentry.io https://api.mclo.gs http://textures.minecraft.net https://textures.minecraft.net https://js.stripe.com https://*.stripe.com wss://*.stripe.com https://*.intercom.io wss://*.intercom.io https://*.intercomcdn.com https://www.intercom-reporting.com https://app.getsentry.com wss://*.nodes.modrinth.com https://*.taila228c5.ts.net https://*.taila228c5.ts.net wss://*.taila228c5.ts.net https://fill.papermc.io https://api.purpurmc.org 'self' data: blob:",
|
||||
"font-src": [
|
||||
"https://cdn-raw.modrinth.com/fonts/",
|
||||
"https://js.intercomcdn.com"
|
||||
],
|
||||
"img-src": "https: 'unsafe-inline' 'self' asset: http://asset.localhost http://textures.minecraft.net blob: data:",
|
||||
"style-src": "'unsafe-inline' 'self'",
|
||||
"script-src": "https://*.posthog.com https://posthog.modrinth.com https://js.stripe.com https://tally.so/widgets/embed.js 'self'",
|
||||
"frame-src": "https://www.youtube.com https://www.youtube-nocookie.com https://discord.com https://tally.so/popup/ https://js.stripe.com https://hooks.stripe.com 'self'",
|
||||
"script-src": "https://*.posthog.com https://posthog.modrinth.com https://js.stripe.com https://widget.intercom.io https://js.intercomcdn.com https://tally.so/widgets/embed.js 'self'",
|
||||
"frame-src": "https://www.youtube.com https://www.youtube-nocookie.com https://discord.com https://tally.so/popup/ https://js.stripe.com https://hooks.stripe.com https://*.intercom.io https://intercom-sheets.com https://www.intercom-reporting.com https://app.intercom.com 'self'",
|
||||
"media-src": "https://*.githubusercontent.com"
|
||||
}
|
||||
}
|
||||
|
||||
1
apps/frontend/AGENTS.md
Symbolic link
1
apps/frontend/AGENTS.md
Symbolic link
@@ -0,0 +1 @@
|
||||
CLAUDE.md
|
||||
@@ -1,3 +1,5 @@
|
||||
@import '@modrinth/assets/styles/reset.scss';
|
||||
|
||||
html {
|
||||
--dark-color-text: #b0bac5;
|
||||
--dark-color-text-dark: #ecf9fb;
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
ImageIcon,
|
||||
ListIcon,
|
||||
MoreVerticalIcon,
|
||||
SpinnerIcon,
|
||||
} from '@modrinth/assets'
|
||||
import type { CardAction, CreationFlowContextValue } from '@modrinth/ui'
|
||||
import {
|
||||
@@ -172,6 +173,43 @@ const serverIcon = computed(() => {
|
||||
})
|
||||
|
||||
const serverHideInstalled = ref(false)
|
||||
const installingProjectIds = ref<Set<string>>(new Set())
|
||||
const optimisticallyInstalledProjectIds = ref<Set<string>>(new Set())
|
||||
const hiddenInstalledProjectIds = ref<Set<string>>(new Set())
|
||||
const hiddenInstalledProjectIdsInitialized = ref(false)
|
||||
|
||||
function setProjectInstalling(projectId: string, installing: boolean) {
|
||||
const next = new Set(installingProjectIds.value)
|
||||
if (installing) {
|
||||
next.add(projectId)
|
||||
} else {
|
||||
next.delete(projectId)
|
||||
}
|
||||
installingProjectIds.value = next
|
||||
}
|
||||
|
||||
function markProjectInstalled(projectId: string) {
|
||||
optimisticallyInstalledProjectIds.value = new Set([
|
||||
...optimisticallyInstalledProjectIds.value,
|
||||
projectId,
|
||||
])
|
||||
}
|
||||
|
||||
function getServerInstalledProjectIds(data = serverContentData.value) {
|
||||
return new Set(
|
||||
(data?.addons ?? [])
|
||||
.map((addon) => addon.project_id)
|
||||
.filter((projectId): projectId is string => !!projectId),
|
||||
)
|
||||
}
|
||||
|
||||
function syncHiddenInstalledProjectIds() {
|
||||
hiddenInstalledProjectIds.value = new Set([
|
||||
...getServerInstalledProjectIds(),
|
||||
...optimisticallyInstalledProjectIds.value,
|
||||
])
|
||||
hiddenInstalledProjectIdsInitialized.value = true
|
||||
}
|
||||
|
||||
const contentQueryKey = computed(() => ['content', 'list', currentServerId.value ?? ''] as const)
|
||||
const { data: serverContentData, error: serverContentError } = useQuery({
|
||||
@@ -187,6 +225,17 @@ watch(serverContentError, (error) => {
|
||||
}
|
||||
})
|
||||
|
||||
watch(
|
||||
serverContentData,
|
||||
(data) => {
|
||||
if (!data) return
|
||||
if (!hiddenInstalledProjectIdsInitialized.value) {
|
||||
syncHiddenInstalledProjectIds()
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
const installContentMutation = useMutation({
|
||||
mutationFn: ({
|
||||
serverId,
|
||||
@@ -212,6 +261,12 @@ if (route.query.shi && projectType.value?.id !== 'modpack') {
|
||||
serverHideInstalled.value = route.query.shi === 'true'
|
||||
}
|
||||
|
||||
watch(serverHideInstalled, (hideInstalled) => {
|
||||
if (hideInstalled) {
|
||||
syncHiddenInstalledProjectIds()
|
||||
}
|
||||
})
|
||||
|
||||
const serverFilters = computed(() => {
|
||||
debug(
|
||||
'serverFilters recomputing, serverData:',
|
||||
@@ -242,19 +297,14 @@ const serverFilters = computed(() => {
|
||||
filters.push({ type: 'environment', option: 'server' })
|
||||
}
|
||||
|
||||
if (serverHideInstalled.value && serverContentData.value) {
|
||||
const installedIds = (serverContentData.value.addons ?? [])
|
||||
.filter((x) => x.project_id)
|
||||
.map((x) => x.project_id)
|
||||
.filter((id): id is string => id !== null)
|
||||
|
||||
installedIds
|
||||
.map((x: string) => ({
|
||||
if (serverHideInstalled.value && hiddenInstalledProjectIds.value.size > 0) {
|
||||
for (const x of hiddenInstalledProjectIds.value) {
|
||||
filters.push({
|
||||
type: 'project_id',
|
||||
option: `project_id:${x}`,
|
||||
negative: true,
|
||||
}))
|
||||
.forEach((x) => filters.push(x))
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -269,7 +319,6 @@ const serverFilters = computed(() => {
|
||||
})
|
||||
|
||||
interface InstallableSearchResult extends Labrinth.Search.v2.ResultSearchProject {
|
||||
installing?: boolean
|
||||
installed?: boolean
|
||||
}
|
||||
|
||||
@@ -278,7 +327,7 @@ async function serverInstall(project: InstallableSearchResult) {
|
||||
handleError(new Error('No server to install to.'))
|
||||
return
|
||||
}
|
||||
project.installing = true
|
||||
setProjectInstalling(project.project_id, true)
|
||||
try {
|
||||
if (projectType.value?.id === 'modpack') {
|
||||
const versions = await client.labrinth.versions_v2.getProjectVersions(project.project_id, {
|
||||
@@ -287,7 +336,7 @@ async function serverInstall(project: InstallableSearchResult) {
|
||||
const versionId = versions[0]?.id ?? project.latest_version
|
||||
if (!versionId) {
|
||||
handleError(new Error('No version found for this modpack'))
|
||||
project.installing = false
|
||||
setProjectInstalling(project.project_id, false)
|
||||
return
|
||||
}
|
||||
const modalInstance = onboardingModalRef.value
|
||||
@@ -326,7 +375,7 @@ async function serverInstall(project: InstallableSearchResult) {
|
||||
: `No compatible version found for ${serverData.value!.mc_version} / ${serverData.value!.loader}`,
|
||||
),
|
||||
)
|
||||
project.installing = false
|
||||
setProjectInstalling(project.project_id, false)
|
||||
return
|
||||
}
|
||||
await installContentMutation.mutateAsync({
|
||||
@@ -334,13 +383,13 @@ async function serverInstall(project: InstallableSearchResult) {
|
||||
projectId: version.project_id,
|
||||
versionId: version.id,
|
||||
})
|
||||
project.installed = true
|
||||
markProjectInstalled(project.project_id)
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
handleError(new Error(`Error installing content ${e}`))
|
||||
}
|
||||
project.installing = false
|
||||
setProjectInstalling(project.project_id, false)
|
||||
}
|
||||
|
||||
function getServerModpackContent(project: Labrinth.Search.v3.ResultSearchProject) {
|
||||
@@ -447,16 +496,19 @@ function getCardActions(
|
||||
if (serverData.value) {
|
||||
const isInstalled =
|
||||
projectResult.installed ||
|
||||
optimisticallyInstalledProjectIds.value.has(result.project_id) ||
|
||||
(serverContentData.value &&
|
||||
(serverContentData.value.addons ?? []).find((x) => x.project_id === result.project_id)) ||
|
||||
serverData.value.upstream?.project_id === result.project_id
|
||||
const isInstalling = installingProjectIds.value.has(result.project_id)
|
||||
|
||||
return [
|
||||
{
|
||||
key: 'install',
|
||||
label: projectResult.installing ? 'Installing...' : isInstalled ? 'Installed' : 'Install',
|
||||
icon: isInstalled ? CheckIcon : DownloadIcon,
|
||||
disabled: !!isInstalled || !!projectResult.installing,
|
||||
label: isInstalling ? 'Installing...' : isInstalled ? 'Installed' : 'Install',
|
||||
icon: isInstalling ? SpinnerIcon : isInstalled ? CheckIcon : DownloadIcon,
|
||||
iconClass: isInstalling ? 'animate-spin' : undefined,
|
||||
disabled: !!isInstalled || isInstalling,
|
||||
color: 'brand',
|
||||
type: 'outlined',
|
||||
onClick: () => serverInstall(projectResult),
|
||||
@@ -472,7 +524,7 @@ const onboardingInstallingProject = ref<InstallableSearchResult | null>(null)
|
||||
|
||||
function onOnboardingHide() {
|
||||
if (onboardingInstallingProject.value) {
|
||||
onboardingInstallingProject.value.installing = false
|
||||
setProjectInstalling(onboardingInstallingProject.value.project_id, false)
|
||||
onboardingInstallingProject.value = null
|
||||
}
|
||||
}
|
||||
@@ -587,6 +639,19 @@ const searchState = useBrowseSearch({
|
||||
displayMode: resultsDisplayMode,
|
||||
})
|
||||
|
||||
watch(
|
||||
[
|
||||
() => searchState.query.value,
|
||||
() => searchState.currentFilters.value,
|
||||
() => searchState.serverCurrentFilters.value,
|
||||
() => projectTypeId.value,
|
||||
],
|
||||
() => {
|
||||
syncHiddenInstalledProjectIds()
|
||||
},
|
||||
{ deep: true },
|
||||
)
|
||||
|
||||
debug('calling initial refreshSearch')
|
||||
searchState.refreshSearch()
|
||||
|
||||
@@ -622,7 +687,7 @@ provideBrowseManager({
|
||||
providedFilters: serverFilters,
|
||||
hideInstalled: serverHideInstalled,
|
||||
showHideInstalled: computed(() => !!serverData.value && projectType.value?.id !== 'modpack'),
|
||||
hideInstalledLabel: computed(() => 'Hide installed content'),
|
||||
hideInstalledLabel: computed(() => 'Hide already installed content'),
|
||||
displayMode: resultsDisplayMode,
|
||||
cycleDisplayMode: cycleSearchDisplayMode,
|
||||
maxResultsOptions: currentMaxResultsOptions,
|
||||
|
||||
1
apps/labrinth/AGENTS.md
Symbolic link
1
apps/labrinth/AGENTS.md
Symbolic link
@@ -0,0 +1 @@
|
||||
CLAUDE.md
|
||||
Reference in New Issue
Block a user