This commit is contained in:
1 parent
b0178c046f
commit
ede28e0062
6 files changed
+1051
-2
No files matched your search
@@ -9,6 +9,7 @@ import AdminBroadcastPage from './admin/AdminBroadcastPage.vue'
|
|||||||
import AdminDonationsPage from './admin/AdminDonationsPage.vue'
|
import AdminDonationsPage from './admin/AdminDonationsPage.vue'
|
||||||
import AdminDownloadsPage from './admin/AdminDownloadsPage.vue'
|
import AdminDownloadsPage from './admin/AdminDownloadsPage.vue'
|
||||||
import AdminLauncherPage from './admin/AdminLauncherPage.vue'
|
import AdminLauncherPage from './admin/AdminLauncherPage.vue'
|
||||||
|
import AdminModsPage from './admin/AdminModsPage.vue'
|
||||||
import AdminPlayersPage from './admin/AdminPlayersPage.vue'
|
import AdminPlayersPage from './admin/AdminPlayersPage.vue'
|
||||||
import AdminReviewPage from './admin/AdminReviewPage.vue'
|
import AdminReviewPage from './admin/AdminReviewPage.vue'
|
||||||
import AdminServersPage from './admin/AdminServersPage.vue'
|
import AdminServersPage from './admin/AdminServersPage.vue'
|
||||||
@@ -179,6 +180,7 @@ const current = ref<AdminKey>('summary')
|
|||||||
<AdminPlayersPage v-else-if="current === 'players'" />
|
<AdminPlayersPage v-else-if="current === 'players'" />
|
||||||
<AdminReviewPage v-else-if="current === 'review'" />
|
<AdminReviewPage v-else-if="current === 'review'" />
|
||||||
<AdminLauncherPage v-else-if="current === 'launcher'" />
|
<AdminLauncherPage v-else-if="current === 'launcher'" />
|
||||||
|
<AdminModsPage v-else-if="current === 'mods'" />
|
||||||
<AdminDownloadsPage v-else-if="current === 'downloads'" />
|
<AdminDownloadsPage v-else-if="current === 'downloads'" />
|
||||||
<AdminDonationsPage v-else-if="current === 'donations' && hasWebAdmin" />
|
<AdminDonationsPage v-else-if="current === 'donations' && hasWebAdmin" />
|
||||||
<p v-else class="m-0 text-sm text-secondary">{{ current }}</p>
|
<p v-else class="m-0 text-sm text-secondary">{{ current }}</p>
|
||||||
|
|||||||
@@ -0,0 +1,788 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { SpinnerIcon } from '@modrinth/assets'
|
||||||
|
import { defineMessages, injectNotificationManager, useVIntl } from '@modrinth/ui'
|
||||||
|
import { computed, onMounted, ref } from 'vue'
|
||||||
|
|
||||||
|
import {
|
||||||
|
skinSiteActivateModRuntime,
|
||||||
|
skinSiteCreateModRuleTag,
|
||||||
|
skinSiteDeleteModFile,
|
||||||
|
skinSiteDeleteModRuleTag,
|
||||||
|
skinSiteDeleteModRuntime,
|
||||||
|
skinSiteModCount,
|
||||||
|
type SkinSiteModInfo,
|
||||||
|
skinSiteModList,
|
||||||
|
type SkinSiteModRuleTag,
|
||||||
|
skinSiteModRuleTags,
|
||||||
|
type SkinSiteModRuntimeAdminState,
|
||||||
|
type SkinSiteModRuntimeRelease,
|
||||||
|
skinSiteModRuntimeReleases,
|
||||||
|
skinSiteModSearch,
|
||||||
|
skinSiteUpdateModInfo,
|
||||||
|
skinSiteUpdateModRuleTag,
|
||||||
|
skinSiteUploadModRuntime,
|
||||||
|
} from '@/helpers/skin-site-api'
|
||||||
|
|
||||||
|
const { formatMessage } = useVIntl()
|
||||||
|
const { handleError, addNotification } = injectNotificationManager()
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
title: { id: 'app.skin-site.admin.mods.title', defaultMessage: 'Mods' },
|
||||||
|
tabMods: { id: 'app.skin-site.admin.mods.tab-mods', defaultMessage: 'Mod files' },
|
||||||
|
tabTags: { id: 'app.skin-site.admin.mods.tab-tags', defaultMessage: 'Rule tags' },
|
||||||
|
tabRuntime: { id: 'app.skin-site.admin.mods.tab-runtime', defaultMessage: 'Runtime' },
|
||||||
|
loading: { id: 'app.skin-site.admin.mods.loading', defaultMessage: 'Loading…' },
|
||||||
|
failed: { id: 'app.skin-site.admin.mods.failed', defaultMessage: 'Could not load mods.' },
|
||||||
|
empty: { id: 'app.skin-site.admin.mods.empty', defaultMessage: 'No mods found.' },
|
||||||
|
total: { id: 'app.skin-site.admin.mods.total', defaultMessage: '{count} mods' },
|
||||||
|
search: { id: 'app.skin-site.admin.mods.search', defaultMessage: 'Search by name' },
|
||||||
|
filterAll: { id: 'app.skin-site.admin.mods.filter-all', defaultMessage: 'All tags' },
|
||||||
|
prev: { id: 'app.skin-site.admin.mods.prev', defaultMessage: 'Previous' },
|
||||||
|
next: { id: 'app.skin-site.admin.mods.next', defaultMessage: 'Next' },
|
||||||
|
page: { id: 'app.skin-site.admin.mods.page', defaultMessage: 'Page {page}' },
|
||||||
|
name: { id: 'app.skin-site.admin.mods.name', defaultMessage: 'Name' },
|
||||||
|
version: { id: 'app.skin-site.admin.mods.version', defaultMessage: 'Version' },
|
||||||
|
modId: { id: 'app.skin-site.admin.mods.mod-id', defaultMessage: 'Mod ID' },
|
||||||
|
size: { id: 'app.skin-site.admin.mods.size', defaultMessage: 'Size' },
|
||||||
|
loaders: { id: 'app.skin-site.admin.mods.loaders', defaultMessage: 'Loaders' },
|
||||||
|
domainRules: { id: 'app.skin-site.admin.mods.domain-rules', defaultMessage: 'Domain rules' },
|
||||||
|
edit: { id: 'app.skin-site.admin.mods.edit', defaultMessage: 'Edit' },
|
||||||
|
save: { id: 'app.skin-site.admin.mods.save', defaultMessage: 'Save' },
|
||||||
|
cancel: { id: 'app.skin-site.admin.mods.cancel', defaultMessage: 'Cancel' },
|
||||||
|
remove: { id: 'app.skin-site.admin.mods.remove', defaultMessage: 'Delete' },
|
||||||
|
updated: { id: 'app.skin-site.admin.mods.updated', defaultMessage: 'Mod updated.' },
|
||||||
|
deleted: { id: 'app.skin-site.admin.mods.deleted', defaultMessage: 'Mod deleted.' },
|
||||||
|
confirmDelete: {
|
||||||
|
id: 'app.skin-site.admin.mods.confirm-delete',
|
||||||
|
defaultMessage: 'Delete mod “{name}”? This cannot be undone.',
|
||||||
|
},
|
||||||
|
ruleTags: { id: 'app.skin-site.admin.mods.rule-tags', defaultMessage: 'Rule tags' },
|
||||||
|
tagName: { id: 'app.skin-site.admin.mods.tag-name', defaultMessage: 'Tag name' },
|
||||||
|
tagRules: { id: 'app.skin-site.admin.mods.tag-rules', defaultMessage: 'Rules' },
|
||||||
|
tagRulesHint: {
|
||||||
|
id: 'app.skin-site.admin.mods.tag-rules-hint',
|
||||||
|
defaultMessage: 'One rule per line.',
|
||||||
|
},
|
||||||
|
newTag: { id: 'app.skin-site.admin.mods.new-tag', defaultMessage: 'New tag' },
|
||||||
|
tagCreated: { id: 'app.skin-site.admin.mods.tag-created', defaultMessage: 'Tag created.' },
|
||||||
|
tagUpdated: { id: 'app.skin-site.admin.mods.tag-updated', defaultMessage: 'Tag updated.' },
|
||||||
|
tagDeleted: { id: 'app.skin-site.admin.mods.tag-deleted', defaultMessage: 'Tag deleted.' },
|
||||||
|
confirmDeleteTag: {
|
||||||
|
id: 'app.skin-site.admin.mods.confirm-delete-tag',
|
||||||
|
defaultMessage: 'Delete tag “{name}”?',
|
||||||
|
},
|
||||||
|
runtime: { id: 'app.skin-site.admin.mods.runtime', defaultMessage: 'Runtime releases' },
|
||||||
|
manifestUrl: { id: 'app.skin-site.admin.mods.manifest-url', defaultMessage: 'Manifest URL' },
|
||||||
|
uploadRuntime: {
|
||||||
|
id: 'app.skin-site.admin.mods.upload-runtime',
|
||||||
|
defaultMessage: 'Upload runtime',
|
||||||
|
},
|
||||||
|
runtimeVersion: {
|
||||||
|
id: 'app.skin-site.admin.mods.runtime-version',
|
||||||
|
defaultMessage: 'Version',
|
||||||
|
},
|
||||||
|
uploading: { id: 'app.skin-site.admin.mods.uploading', defaultMessage: 'Uploading…' },
|
||||||
|
runtimeUploaded: {
|
||||||
|
id: 'app.skin-site.admin.mods.runtime-uploaded',
|
||||||
|
defaultMessage: 'Runtime uploaded.',
|
||||||
|
},
|
||||||
|
activate: { id: 'app.skin-site.admin.mods.activate', defaultMessage: 'Activate' },
|
||||||
|
activated: { id: 'app.skin-site.admin.mods.activated', defaultMessage: 'Runtime activated.' },
|
||||||
|
available: { id: 'app.skin-site.admin.mods.available', defaultMessage: 'Available' },
|
||||||
|
unavailable: { id: 'app.skin-site.admin.mods.unavailable', defaultMessage: 'Unavailable' },
|
||||||
|
active: { id: 'app.skin-site.admin.mods.active', defaultMessage: 'Active' },
|
||||||
|
uploader: { id: 'app.skin-site.admin.mods.uploader', defaultMessage: 'by {name}' },
|
||||||
|
})
|
||||||
|
|
||||||
|
type Tab = 'mods' | 'tags' | 'runtime'
|
||||||
|
const tab = ref<Tab>('mods')
|
||||||
|
|
||||||
|
const PAGE_SIZE = 20
|
||||||
|
|
||||||
|
const mods = ref<SkinSiteModInfo[]>([])
|
||||||
|
const modTotal = ref(0)
|
||||||
|
const modOffset = ref(0)
|
||||||
|
const modLoading = ref(true)
|
||||||
|
const modFailed = ref(false)
|
||||||
|
const searchKeyword = ref('')
|
||||||
|
const tagFilter = ref<number | null>(null)
|
||||||
|
|
||||||
|
const editingHash = ref<string | null>(null)
|
||||||
|
const editForm = ref({ name: '', modId: '', version: '', domainRules: '' })
|
||||||
|
const savingMod = ref(false)
|
||||||
|
const deletingHash = ref<string | null>(null)
|
||||||
|
|
||||||
|
const tags = ref<SkinSiteModRuleTag[]>([])
|
||||||
|
const tagsLoading = ref(false)
|
||||||
|
const tagEditorOpen = ref(false)
|
||||||
|
const editingTagId = ref<number | null>(null)
|
||||||
|
const tagForm = ref({ name: '', rules: '' })
|
||||||
|
const savingTag = ref(false)
|
||||||
|
|
||||||
|
const runtimeState = ref<SkinSiteModRuntimeAdminState | null>(null)
|
||||||
|
const runtimeLoading = ref(false)
|
||||||
|
const runtimeVersion = ref('')
|
||||||
|
const runtimeFile = ref<HTMLInputElement | null>(null)
|
||||||
|
const runtimeUploading = ref(false)
|
||||||
|
const activatingRuntimeId = ref<number | null>(null)
|
||||||
|
const deletingRuntimeId = ref<number | null>(null)
|
||||||
|
|
||||||
|
const modPage = computed(() => Math.floor(modOffset.value / PAGE_SIZE) + 1)
|
||||||
|
const modPageCount = computed(() => Math.max(1, Math.ceil(modTotal.value / PAGE_SIZE)))
|
||||||
|
const hasPrev = computed(() => modOffset.value > 0)
|
||||||
|
const hasNext = computed(() => modOffset.value + PAGE_SIZE < modTotal.value)
|
||||||
|
|
||||||
|
function formatSize(bytes: number): string {
|
||||||
|
if (!Number.isFinite(bytes) || bytes < 0) return '—'
|
||||||
|
const units = ['B', 'KB', 'MB', 'GB']
|
||||||
|
let v = bytes
|
||||||
|
let i = 0
|
||||||
|
while (v >= 1024 && i < units.length - 1) {
|
||||||
|
v /= 1024
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
return v.toFixed(i === 0 ? 0 : 2) + ' ' + units[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDateTime(ts?: number | null): string {
|
||||||
|
if (!ts) return '—'
|
||||||
|
try {
|
||||||
|
return new Date(ts).toLocaleString()
|
||||||
|
} catch {
|
||||||
|
return '—'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadMods(reset = true) {
|
||||||
|
if (reset) {
|
||||||
|
modLoading.value = true
|
||||||
|
mods.value = []
|
||||||
|
modOffset.value = 0
|
||||||
|
}
|
||||||
|
modFailed.value = false
|
||||||
|
try {
|
||||||
|
const kw = searchKeyword.value.trim()
|
||||||
|
if (kw) {
|
||||||
|
const result = await skinSiteModSearch(kw, tagFilter.value)
|
||||||
|
if (!result.success) throw new Error(result.errorMessage)
|
||||||
|
mods.value = result.payload ?? []
|
||||||
|
modTotal.value = mods.value.length
|
||||||
|
} else {
|
||||||
|
const [listResult, countResult] = await Promise.all([
|
||||||
|
skinSiteModList({
|
||||||
|
limit: PAGE_SIZE,
|
||||||
|
offset: modOffset.value,
|
||||||
|
ruleTagId: tagFilter.value,
|
||||||
|
}),
|
||||||
|
skinSiteModCount({ ruleTagId: tagFilter.value }),
|
||||||
|
])
|
||||||
|
if (!listResult.success) throw new Error(listResult.errorMessage)
|
||||||
|
mods.value = listResult.payload ?? []
|
||||||
|
if (countResult.success && typeof countResult.payload === 'number') {
|
||||||
|
modTotal.value = countResult.payload
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
modFailed.value = true
|
||||||
|
handleError(error)
|
||||||
|
} finally {
|
||||||
|
modLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function prevPage() {
|
||||||
|
if (hasPrev.value) {
|
||||||
|
modOffset.value = Math.max(0, modOffset.value - PAGE_SIZE)
|
||||||
|
void loadMods(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function nextPage() {
|
||||||
|
if (hasNext.value) {
|
||||||
|
modOffset.value += PAGE_SIZE
|
||||||
|
void loadMods(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadTags() {
|
||||||
|
tagsLoading.value = true
|
||||||
|
try {
|
||||||
|
const result = await skinSiteModRuleTags()
|
||||||
|
if (result.success) tags.value = result.payload ?? []
|
||||||
|
} catch (error) {
|
||||||
|
handleError(error)
|
||||||
|
} finally {
|
||||||
|
tagsLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadRuntime() {
|
||||||
|
runtimeLoading.value = true
|
||||||
|
try {
|
||||||
|
const result = await skinSiteModRuntimeReleases()
|
||||||
|
if (result.success) runtimeState.value = result.payload
|
||||||
|
} catch (error) {
|
||||||
|
handleError(error)
|
||||||
|
} finally {
|
||||||
|
runtimeLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function switchTab(next: Tab) {
|
||||||
|
tab.value = next
|
||||||
|
if (next === 'tags' && !tags.value.length) await loadTags()
|
||||||
|
if (next === 'runtime' && !runtimeState.value) await loadRuntime()
|
||||||
|
}
|
||||||
|
|
||||||
|
function startEdit(mod: SkinSiteModInfo) {
|
||||||
|
editingHash.value = mod.hash
|
||||||
|
editForm.value = {
|
||||||
|
name: mod.metadata.name ?? mod.name,
|
||||||
|
modId: mod.metadata.modId ?? '',
|
||||||
|
version: mod.metadata.version ?? '',
|
||||||
|
domainRules: mod.domainRules.join(', '),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function cancelEdit() {
|
||||||
|
editingHash.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveMod(mod: SkinSiteModInfo) {
|
||||||
|
if (savingMod.value) return
|
||||||
|
savingMod.value = true
|
||||||
|
try {
|
||||||
|
const payload: SkinSiteModInfo = {
|
||||||
|
...mod,
|
||||||
|
name: editForm.value.name.trim() || mod.name,
|
||||||
|
domainRules: editForm.value.domainRules
|
||||||
|
.split(',')
|
||||||
|
.map((r) => r.trim())
|
||||||
|
.filter(Boolean),
|
||||||
|
metadata: {
|
||||||
|
...mod.metadata,
|
||||||
|
name: editForm.value.name.trim() || undefined,
|
||||||
|
modId: editForm.value.modId.trim() || undefined,
|
||||||
|
version: editForm.value.version.trim() || undefined,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
const result = await skinSiteUpdateModInfo(payload)
|
||||||
|
if (!result.success) throw new Error(result.errorMessage)
|
||||||
|
addNotification({ type: 'success', title: formatMessage(messages.updated) })
|
||||||
|
cancelEdit()
|
||||||
|
await loadMods(false)
|
||||||
|
} catch (error) {
|
||||||
|
handleError(error)
|
||||||
|
} finally {
|
||||||
|
savingMod.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function removeMod(mod: SkinSiteModInfo) {
|
||||||
|
if (deletingHash.value !== null) return
|
||||||
|
if (!window.confirm(formatMessage(messages.confirmDelete, { name: mod.name }))) return
|
||||||
|
deletingHash.value = mod.hash
|
||||||
|
try {
|
||||||
|
const result = await skinSiteDeleteModFile(mod.hash)
|
||||||
|
if (!result.success) throw new Error(result.errorMessage)
|
||||||
|
addNotification({ type: 'success', title: formatMessage(messages.deleted) })
|
||||||
|
await loadMods(false)
|
||||||
|
} catch (error) {
|
||||||
|
handleError(error)
|
||||||
|
} finally {
|
||||||
|
deletingHash.value = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function openNewTag() {
|
||||||
|
editingTagId.value = null
|
||||||
|
tagForm.value = { name: '', rules: '' }
|
||||||
|
tagEditorOpen.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function openEditTag(tag: SkinSiteModRuleTag) {
|
||||||
|
editingTagId.value = tag.id
|
||||||
|
tagForm.value = { name: tag.name, rules: tag.rules.join('\n') }
|
||||||
|
tagEditorOpen.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeTagEditor() {
|
||||||
|
tagEditorOpen.value = false
|
||||||
|
tagForm.value = { name: '', rules: '' }
|
||||||
|
editingTagId.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveTag() {
|
||||||
|
const name = tagForm.value.name.trim()
|
||||||
|
if (!name || savingTag.value) return
|
||||||
|
savingTag.value = true
|
||||||
|
try {
|
||||||
|
const rules = tagForm.value.rules
|
||||||
|
.split('\n')
|
||||||
|
.map((r) => r.trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
const result =
|
||||||
|
editingTagId.value === null
|
||||||
|
? await skinSiteCreateModRuleTag(name, rules)
|
||||||
|
: await skinSiteUpdateModRuleTag(editingTagId.value, name, rules)
|
||||||
|
if (!result.success) throw new Error(result.errorMessage)
|
||||||
|
addNotification({
|
||||||
|
type: 'success',
|
||||||
|
title: formatMessage(editingTagId.value === null ? messages.tagCreated : messages.tagUpdated),
|
||||||
|
})
|
||||||
|
closeTagEditor()
|
||||||
|
await loadTags()
|
||||||
|
} catch (error) {
|
||||||
|
handleError(error)
|
||||||
|
} finally {
|
||||||
|
savingTag.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function removeTag(tag: SkinSiteModRuleTag) {
|
||||||
|
if (!window.confirm(formatMessage(messages.confirmDeleteTag, { name: tag.name }))) return
|
||||||
|
try {
|
||||||
|
const result = await skinSiteDeleteModRuleTag(tag.id)
|
||||||
|
if (!result.success) throw new Error(result.errorMessage)
|
||||||
|
addNotification({ type: 'success', title: formatMessage(messages.tagDeleted) })
|
||||||
|
if (tagFilter.value === tag.id) tagFilter.value = null
|
||||||
|
await loadTags()
|
||||||
|
} catch (error) {
|
||||||
|
handleError(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function pickRuntimeFile() {
|
||||||
|
runtimeFile.value?.click()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onRuntimeFile(event: Event) {
|
||||||
|
const input = event.target as HTMLInputElement
|
||||||
|
const file = input.files?.[0]
|
||||||
|
input.value = ''
|
||||||
|
const version = runtimeVersion.value.trim()
|
||||||
|
if (!file || !version || runtimeUploading.value) return
|
||||||
|
runtimeUploading.value = true
|
||||||
|
try {
|
||||||
|
const result = await skinSiteUploadModRuntime(version, file)
|
||||||
|
if (!result.success) throw new Error(result.errorMessage)
|
||||||
|
addNotification({ type: 'success', title: formatMessage(messages.runtimeUploaded) })
|
||||||
|
runtimeVersion.value = ''
|
||||||
|
await loadRuntime()
|
||||||
|
} catch (error) {
|
||||||
|
handleError(error)
|
||||||
|
} finally {
|
||||||
|
runtimeUploading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function activateRuntime(release: SkinSiteModRuntimeRelease) {
|
||||||
|
if (activatingRuntimeId.value !== null) return
|
||||||
|
activatingRuntimeId.value = release.id
|
||||||
|
try {
|
||||||
|
const result = await skinSiteActivateModRuntime(release.id)
|
||||||
|
if (!result.success) throw new Error(result.errorMessage)
|
||||||
|
addNotification({ type: 'success', title: formatMessage(messages.activated) })
|
||||||
|
await loadRuntime()
|
||||||
|
} catch (error) {
|
||||||
|
handleError(error)
|
||||||
|
} finally {
|
||||||
|
activatingRuntimeId.value = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function removeRuntime(release: SkinSiteModRuntimeRelease) {
|
||||||
|
if (deletingRuntimeId.value !== null) return
|
||||||
|
deletingRuntimeId.value = release.id
|
||||||
|
try {
|
||||||
|
const result = await skinSiteDeleteModRuntime(release.id)
|
||||||
|
if (!result.success) throw new Error(result.errorMessage)
|
||||||
|
await loadRuntime()
|
||||||
|
} catch (error) {
|
||||||
|
handleError(error)
|
||||||
|
} finally {
|
||||||
|
deletingRuntimeId.value = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await loadTags()
|
||||||
|
await loadMods(true)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div class="flex flex-col gap-4">
|
||||||
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
|
<h2 class="m-0 text-lg font-bold text-contrast">{{ formatMessage(messages.title) }}</h2>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="rounded-full px-3 py-1 text-xs font-semibold"
|
||||||
|
:class="tab === 'mods' ? 'bg-brand text-contrast' : 'bg-button-bg text-secondary'"
|
||||||
|
@click="switchTab('mods')"
|
||||||
|
>
|
||||||
|
{{ formatMessage(messages.tabMods) }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="rounded-full px-3 py-1 text-xs font-semibold"
|
||||||
|
:class="tab === 'tags' ? 'bg-brand text-contrast' : 'bg-button-bg text-secondary'"
|
||||||
|
@click="switchTab('tags')"
|
||||||
|
>
|
||||||
|
{{ formatMessage(messages.tabTags) }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="rounded-full px-3 py-1 text-xs font-semibold"
|
||||||
|
:class="tab === 'runtime' ? 'bg-brand text-contrast' : 'bg-button-bg text-secondary'"
|
||||||
|
@click="switchTab('runtime')"
|
||||||
|
>
|
||||||
|
{{ formatMessage(messages.tabRuntime) }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template v-if="tab === 'mods'">
|
||||||
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
|
<input
|
||||||
|
v-model="searchKeyword"
|
||||||
|
type="text"
|
||||||
|
:placeholder="formatMessage(messages.search)"
|
||||||
|
class="min-w-0 flex-1 rounded-full border border-solid border-surface-5 bg-bg px-3 py-1 text-xs text-contrast outline-none focus:border-brand"
|
||||||
|
@keyup.enter="loadMods(true)"
|
||||||
|
/>
|
||||||
|
<select
|
||||||
|
v-model="tagFilter"
|
||||||
|
class="rounded-full bg-button-bg px-3 py-1 text-xs font-semibold text-contrast"
|
||||||
|
@change="loadMods(true)"
|
||||||
|
>
|
||||||
|
<option :value="null">{{ formatMessage(messages.filterAll) }}</option>
|
||||||
|
<option v-for="tag in tags" :key="tag.id" :value="tag.id">{{ tag.name }}</option>
|
||||||
|
</select>
|
||||||
|
<span class="text-xs text-secondary">
|
||||||
|
{{ formatMessage(messages.total, { count: modTotal }) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="modLoading" class="flex items-center gap-2 text-sm text-secondary">
|
||||||
|
<SpinnerIcon class="size-4 animate-spin" />
|
||||||
|
{{ formatMessage(messages.loading) }}
|
||||||
|
</div>
|
||||||
|
<p v-else-if="modFailed" class="m-0 text-sm text-secondary">
|
||||||
|
{{ formatMessage(messages.failed) }}
|
||||||
|
</p>
|
||||||
|
<p v-else-if="!mods.length" class="m-0 rounded-xl bg-bg-raised p-4 text-sm text-secondary">
|
||||||
|
{{ formatMessage(messages.empty) }}
|
||||||
|
</p>
|
||||||
|
<div v-else class="flex flex-col gap-2">
|
||||||
|
<div
|
||||||
|
v-for="mod in mods"
|
||||||
|
:key="mod.hash"
|
||||||
|
class="flex flex-col gap-2 rounded-xl bg-bg-raised p-3"
|
||||||
|
>
|
||||||
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
|
<span class="min-w-0 flex-1 truncate text-sm font-semibold text-contrast">
|
||||||
|
{{ mod.metadata.name || mod.name }}
|
||||||
|
</span>
|
||||||
|
<span class="shrink-0 text-xs text-secondary">{{ formatSize(mod.size) }}</span>
|
||||||
|
<span
|
||||||
|
v-for="loader in mod.metadata.loaderType"
|
||||||
|
:key="loader"
|
||||||
|
class="shrink-0 rounded-full bg-button-bg px-2 py-0.5 text-[10px] font-semibold text-secondary"
|
||||||
|
>
|
||||||
|
{{ loader }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<span class="truncate font-mono text-[10px] text-secondary">{{ mod.hash }}</span>
|
||||||
|
|
||||||
|
<div v-if="editingHash === mod.hash" class="grid grid-cols-1 gap-2 sm:grid-cols-3">
|
||||||
|
<label class="flex flex-col gap-1">
|
||||||
|
<span class="text-xs text-secondary">{{ formatMessage(messages.name) }}</span>
|
||||||
|
<input
|
||||||
|
v-model="editForm.name"
|
||||||
|
type="text"
|
||||||
|
class="rounded-lg border border-solid border-surface-5 bg-bg px-2 py-1 text-xs text-contrast outline-none focus:border-brand"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label class="flex flex-col gap-1">
|
||||||
|
<span class="text-xs text-secondary">{{ formatMessage(messages.modId) }}</span>
|
||||||
|
<input
|
||||||
|
v-model="editForm.modId"
|
||||||
|
type="text"
|
||||||
|
class="rounded-lg border border-solid border-surface-5 bg-bg px-2 py-1 text-xs text-contrast outline-none focus:border-brand"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label class="flex flex-col gap-1">
|
||||||
|
<span class="text-xs text-secondary">{{ formatMessage(messages.version) }}</span>
|
||||||
|
<input
|
||||||
|
v-model="editForm.version"
|
||||||
|
type="text"
|
||||||
|
class="rounded-lg border border-solid border-surface-5 bg-bg px-2 py-1 text-xs text-contrast outline-none focus:border-brand"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label class="flex flex-col gap-1 sm:col-span-3">
|
||||||
|
<span class="text-xs text-secondary">{{ formatMessage(messages.domainRules) }}</span>
|
||||||
|
<input
|
||||||
|
v-model="editForm.domainRules"
|
||||||
|
type="text"
|
||||||
|
class="rounded-lg border border-solid border-surface-5 bg-bg px-2 py-1 text-xs text-contrast outline-none focus:border-brand"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-wrap gap-2">
|
||||||
|
<template v-if="editingHash === mod.hash">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="rounded-full bg-button-bg px-3 py-1 text-xs font-semibold text-contrast hover:brightness-110"
|
||||||
|
@click="cancelEdit"
|
||||||
|
>
|
||||||
|
{{ formatMessage(messages.cancel) }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="rounded-full bg-brand px-3 py-1 text-xs font-semibold text-contrast hover:brightness-110 disabled:opacity-50"
|
||||||
|
:disabled="savingMod"
|
||||||
|
@click="saveMod(mod)"
|
||||||
|
>
|
||||||
|
{{ formatMessage(messages.save) }}
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<span
|
||||||
|
v-for="rule in mod.domainRules"
|
||||||
|
:key="rule"
|
||||||
|
class="rounded bg-bg px-2 py-0.5 font-mono text-[10px] text-secondary"
|
||||||
|
>
|
||||||
|
{{ rule }}
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="ml-auto rounded-full bg-button-bg px-3 py-1 text-xs font-semibold text-contrast hover:brightness-110"
|
||||||
|
@click="startEdit(mod)"
|
||||||
|
>
|
||||||
|
{{ formatMessage(messages.edit) }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="rounded-full bg-red px-3 py-1 text-xs font-semibold text-contrast hover:brightness-110 disabled:opacity-50"
|
||||||
|
:disabled="deletingHash !== null"
|
||||||
|
@click="removeMod(mod)"
|
||||||
|
>
|
||||||
|
{{ formatMessage(messages.remove) }}
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center justify-center gap-3 pt-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="rounded-full bg-button-bg px-3 py-1 text-xs font-semibold text-contrast hover:brightness-110 disabled:opacity-50"
|
||||||
|
:disabled="!hasPrev"
|
||||||
|
@click="prevPage"
|
||||||
|
>
|
||||||
|
{{ formatMessage(messages.prev) }}
|
||||||
|
</button>
|
||||||
|
<span class="text-xs text-secondary">
|
||||||
|
{{ formatMessage(messages.page, { page: modPage }) }} / {{ modPageCount }}
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="rounded-full bg-button-bg px-3 py-1 text-xs font-semibold text-contrast hover:brightness-110 disabled:opacity-50"
|
||||||
|
:disabled="!hasNext"
|
||||||
|
@click="nextPage"
|
||||||
|
>
|
||||||
|
{{ formatMessage(messages.next) }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="tab === 'tags'">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="rounded-full bg-brand px-4 py-2 text-sm font-semibold text-contrast hover:brightness-110"
|
||||||
|
@click="openNewTag"
|
||||||
|
>
|
||||||
|
{{ formatMessage(messages.newTag) }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="tagsLoading" class="flex items-center gap-2 text-sm text-secondary">
|
||||||
|
<SpinnerIcon class="size-4 animate-spin" />
|
||||||
|
{{ formatMessage(messages.loading) }}
|
||||||
|
</div>
|
||||||
|
<div v-else-if="!tags.length" class="rounded-xl bg-bg-raised p-4 text-sm text-secondary">
|
||||||
|
{{ formatMessage(messages.empty) }}
|
||||||
|
</div>
|
||||||
|
<div v-else class="flex flex-col gap-2">
|
||||||
|
<div
|
||||||
|
v-for="tag in tags"
|
||||||
|
:key="tag.id"
|
||||||
|
class="flex flex-wrap items-center gap-2 rounded-xl bg-bg-raised p-3"
|
||||||
|
>
|
||||||
|
<span class="min-w-0 flex-1 truncate text-sm font-semibold text-contrast">
|
||||||
|
{{ tag.name }}
|
||||||
|
</span>
|
||||||
|
<span class="text-xs text-secondary">×{{ tag.rules.length }}</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="rounded-full bg-button-bg px-3 py-1 text-xs font-semibold text-contrast hover:brightness-110"
|
||||||
|
@click="openEditTag(tag)"
|
||||||
|
>
|
||||||
|
{{ formatMessage(messages.edit) }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="rounded-full bg-red px-3 py-1 text-xs font-semibold text-contrast hover:brightness-110"
|
||||||
|
@click="removeTag(tag)"
|
||||||
|
>
|
||||||
|
{{ formatMessage(messages.remove) }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="tagEditorOpen"
|
||||||
|
class="flex flex-col gap-2 rounded-xl border border-solid border-surface-5 bg-bg-raised p-4"
|
||||||
|
>
|
||||||
|
<label class="flex flex-col gap-1">
|
||||||
|
<span class="text-sm font-semibold text-contrast">
|
||||||
|
{{ formatMessage(messages.tagName) }}
|
||||||
|
</span>
|
||||||
|
<input
|
||||||
|
v-model="tagForm.name"
|
||||||
|
type="text"
|
||||||
|
class="rounded-lg border border-solid border-surface-5 bg-bg px-3 py-2 text-sm text-contrast outline-none focus:border-brand"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label class="flex flex-col gap-1">
|
||||||
|
<span class="text-sm font-semibold text-contrast">
|
||||||
|
{{ formatMessage(messages.tagRules) }}
|
||||||
|
</span>
|
||||||
|
<textarea
|
||||||
|
v-model="tagForm.rules"
|
||||||
|
rows="5"
|
||||||
|
:placeholder="formatMessage(messages.tagRulesHint)"
|
||||||
|
class="w-full resize-y rounded-lg border border-solid border-surface-5 bg-bg px-3 py-2 font-mono text-xs text-contrast outline-none focus:border-brand"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<div class="flex justify-end gap-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="rounded-full bg-button-bg px-4 py-2 text-sm font-semibold text-contrast hover:brightness-110"
|
||||||
|
@click="closeTagEditor"
|
||||||
|
>
|
||||||
|
{{ formatMessage(messages.cancel) }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="rounded-full bg-brand px-4 py-2 text-sm font-semibold text-contrast disabled:opacity-50"
|
||||||
|
:disabled="savingTag || !tagForm.name.trim()"
|
||||||
|
@click="saveTag"
|
||||||
|
>
|
||||||
|
{{ formatMessage(messages.save) }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
|
<input ref="runtimeFile" type="file" class="hidden" @change="onRuntimeFile" />
|
||||||
|
<input
|
||||||
|
v-model="runtimeVersion"
|
||||||
|
type="text"
|
||||||
|
:placeholder="formatMessage(messages.runtimeVersion)"
|
||||||
|
class="w-40 rounded-lg border border-solid border-surface-5 bg-bg px-3 py-1.5 text-xs text-contrast outline-none focus:border-brand"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="rounded-full bg-brand px-4 py-2 text-sm font-semibold text-contrast hover:brightness-110 disabled:opacity-50"
|
||||||
|
:disabled="runtimeUploading || !runtimeVersion.trim()"
|
||||||
|
@click="pickRuntimeFile"
|
||||||
|
>
|
||||||
|
{{
|
||||||
|
runtimeUploading
|
||||||
|
? formatMessage(messages.uploading)
|
||||||
|
: formatMessage(messages.uploadRuntime)
|
||||||
|
}}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="runtimeState && runtimeState.manifestUrl"
|
||||||
|
class="flex flex-wrap items-center gap-2 rounded-xl bg-bg-raised p-3"
|
||||||
|
>
|
||||||
|
<span class="text-xs font-semibold text-contrast">
|
||||||
|
{{ formatMessage(messages.manifestUrl) }}
|
||||||
|
</span>
|
||||||
|
<code class="min-w-0 flex-1 truncate text-xs text-secondary">
|
||||||
|
{{ runtimeState.manifestUrl }}
|
||||||
|
</code>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="runtimeLoading" class="flex items-center gap-2 text-sm text-secondary">
|
||||||
|
<SpinnerIcon class="size-4 animate-spin" />
|
||||||
|
{{ formatMessage(messages.loading) }}
|
||||||
|
</div>
|
||||||
|
<p
|
||||||
|
v-else-if="!runtimeState || !runtimeState.releases.length"
|
||||||
|
class="m-0 rounded-xl bg-bg-raised p-4 text-sm text-secondary"
|
||||||
|
>
|
||||||
|
{{ formatMessage(messages.empty) }}
|
||||||
|
</p>
|
||||||
|
<div v-else class="flex flex-col gap-2">
|
||||||
|
<div
|
||||||
|
v-for="release in runtimeState.releases"
|
||||||
|
:key="release.id"
|
||||||
|
class="flex flex-wrap items-center gap-3 rounded-xl bg-bg-raised p-3"
|
||||||
|
>
|
||||||
|
<div class="flex min-w-0 flex-1 flex-col">
|
||||||
|
<span class="truncate text-sm font-semibold text-contrast">
|
||||||
|
{{ release.version }}
|
||||||
|
<span class="text-xs font-normal text-secondary">#{{ release.id }}</span>
|
||||||
|
</span>
|
||||||
|
<span class="truncate text-xs text-secondary">
|
||||||
|
{{ formatMessage(messages.uploader, { name: release.uploadedBy }) }} ·
|
||||||
|
{{ formatDateTime(release.uploadedAt) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<span class="shrink-0 text-xs text-secondary">
|
||||||
|
{{ formatSize(release.fileSize) }}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
v-if="release.active"
|
||||||
|
class="shrink-0 rounded-full bg-green px-2 py-0.5 text-[10px] font-semibold text-contrast"
|
||||||
|
>
|
||||||
|
{{ formatMessage(messages.active) }}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
class="shrink-0 rounded-full px-2 py-0.5 text-[10px] font-semibold"
|
||||||
|
:class="release.available ? 'bg-button-bg text-secondary' : 'bg-orange text-contrast'"
|
||||||
|
>
|
||||||
|
{{
|
||||||
|
release.available
|
||||||
|
? formatMessage(messages.available)
|
||||||
|
: formatMessage(messages.unavailable)
|
||||||
|
}}
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
v-if="!release.active"
|
||||||
|
type="button"
|
||||||
|
class="shrink-0 rounded-full bg-brand px-3 py-1 text-xs font-semibold text-contrast hover:brightness-110 disabled:opacity-50"
|
||||||
|
:disabled="activatingRuntimeId !== null"
|
||||||
|
@click="activateRuntime(release)"
|
||||||
|
>
|
||||||
|
{{ formatMessage(messages.activate) }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="shrink-0 rounded-full bg-red px-3 py-1 text-xs font-semibold text-contrast hover:brightness-110 disabled:opacity-50"
|
||||||
|
:disabled="deletingRuntimeId !== null || release.active"
|
||||||
|
@click="removeRuntime(release)"
|
||||||
|
>
|
||||||
|
{{ formatMessage(messages.remove) }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -1532,3 +1532,167 @@ export function skinSiteDeleteDonation(
|
|||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type SkinSiteModLoaderType = 'FABRIC' | 'FORGE'
|
||||||
|
|
||||||
|
export interface SkinSiteModMetadata {
|
||||||
|
modId?: string
|
||||||
|
name?: string
|
||||||
|
version?: string
|
||||||
|
iconBytes?: string
|
||||||
|
loaderType: SkinSiteModLoaderType[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SkinSiteModInfo {
|
||||||
|
domainRules: string[]
|
||||||
|
hash: string
|
||||||
|
name: string
|
||||||
|
size: number
|
||||||
|
metadata: SkinSiteModMetadata
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SkinSiteModRuleTag {
|
||||||
|
id: number
|
||||||
|
name: string
|
||||||
|
rules: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export function skinSiteModCount(params?: {
|
||||||
|
serverAddr?: string
|
||||||
|
ruleTagId?: number | null
|
||||||
|
}): Promise<SkinSiteResponse<number>> {
|
||||||
|
const query = new URLSearchParams()
|
||||||
|
const addr = params?.serverAddr?.trim()
|
||||||
|
if (addr) query.set('server_addr', addr)
|
||||||
|
if (params?.ruleTagId != null) query.set('rule_tag_id', String(params.ruleTagId))
|
||||||
|
const suffix = query.toString() ? '?' + query.toString() : ''
|
||||||
|
return skinSiteRequest<number>('query/mod/number' + suffix, { type: 'mod' })
|
||||||
|
}
|
||||||
|
|
||||||
|
export function skinSiteModList(params: {
|
||||||
|
limit: number
|
||||||
|
offset: number
|
||||||
|
serverAddr?: string
|
||||||
|
ruleTagId?: number | null
|
||||||
|
}): Promise<SkinSiteResponse<SkinSiteModInfo[]>> {
|
||||||
|
return skinSiteRequest<SkinSiteModInfo[]>('query/mod/list', {
|
||||||
|
method: 'POST',
|
||||||
|
type: 'mod',
|
||||||
|
body: {
|
||||||
|
offset: params.offset || 0,
|
||||||
|
limit: params.limit || 0,
|
||||||
|
serverAddr: params.serverAddr?.trim() || undefined,
|
||||||
|
ruleTagId: params.ruleTagId ?? undefined,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function skinSiteModSearch(
|
||||||
|
name: string,
|
||||||
|
ruleTagId?: number | null,
|
||||||
|
): Promise<SkinSiteResponse<SkinSiteModInfo[]>> {
|
||||||
|
const query = new URLSearchParams()
|
||||||
|
if (ruleTagId != null) query.set('rule_tag_id', String(ruleTagId))
|
||||||
|
const suffix = query.toString() ? '?' + query.toString() : ''
|
||||||
|
return skinSiteRequest<SkinSiteModInfo[]>(
|
||||||
|
'query/mod/search/' + encodeURIComponent(name) + suffix,
|
||||||
|
{ type: 'mod' },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function skinSiteUpdateModInfo(info: SkinSiteModInfo): Promise<SkinSiteResponse<string>> {
|
||||||
|
return skinSiteRequest<string>('update/info', { method: 'POST', type: 'mod', body: info })
|
||||||
|
}
|
||||||
|
|
||||||
|
export function skinSiteDeleteModFile(hash: string): Promise<SkinSiteResponse<string>> {
|
||||||
|
return skinSiteRequest<string>(hash, { method: 'DELETE', type: 'mod' })
|
||||||
|
}
|
||||||
|
|
||||||
|
export function skinSiteDeleteMultiModFile(hashes: string[]): Promise<SkinSiteResponse<string>> {
|
||||||
|
return skinSiteRequest<string>('delete/multimod', {
|
||||||
|
method: 'POST',
|
||||||
|
type: 'mod',
|
||||||
|
body: { modHashes: hashes },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function skinSiteModRuleTags(): Promise<SkinSiteResponse<SkinSiteModRuleTag[]>> {
|
||||||
|
return skinSiteRequest<SkinSiteModRuleTag[]>('rule-tags', { type: 'mod' })
|
||||||
|
}
|
||||||
|
|
||||||
|
export function skinSiteCreateModRuleTag(
|
||||||
|
name: string,
|
||||||
|
rules: string[],
|
||||||
|
): Promise<SkinSiteResponse<SkinSiteModRuleTag>> {
|
||||||
|
return skinSiteRequest<SkinSiteModRuleTag>('rule-tags', {
|
||||||
|
method: 'POST',
|
||||||
|
type: 'mod',
|
||||||
|
body: { name, rules },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function skinSiteUpdateModRuleTag(
|
||||||
|
id: number,
|
||||||
|
name: string,
|
||||||
|
rules: string[],
|
||||||
|
): Promise<SkinSiteResponse<SkinSiteModRuleTag>> {
|
||||||
|
return skinSiteRequest<SkinSiteModRuleTag>('rule-tags/' + id, {
|
||||||
|
method: 'POST',
|
||||||
|
type: 'mod',
|
||||||
|
body: { name, rules },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function skinSiteDeleteModRuleTag(id: number): Promise<SkinSiteResponse<string>> {
|
||||||
|
return skinSiteRequest<string>('rule-tags/' + id, { method: 'DELETE', type: 'mod' })
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SkinSiteModRuntimeRelease {
|
||||||
|
id: number
|
||||||
|
version: string
|
||||||
|
fileName: string
|
||||||
|
sha256: string
|
||||||
|
fileSize: number
|
||||||
|
uploadedBy: string
|
||||||
|
uploadedAt: number
|
||||||
|
publishedAt: number | null
|
||||||
|
active: boolean
|
||||||
|
available: boolean
|
||||||
|
downloadUrl: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SkinSiteModRuntimeAdminState {
|
||||||
|
manifestUrl: string
|
||||||
|
releases: SkinSiteModRuntimeRelease[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export function skinSiteModRuntimeReleases(): Promise<
|
||||||
|
SkinSiteResponse<SkinSiteModRuntimeAdminState>
|
||||||
|
> {
|
||||||
|
return skinSiteRequest<SkinSiteModRuntimeAdminState>('runtime/admin', { type: 'mod' })
|
||||||
|
}
|
||||||
|
|
||||||
|
export function skinSiteUploadModRuntime(
|
||||||
|
version: string,
|
||||||
|
file: File,
|
||||||
|
): Promise<SkinSiteResponse<SkinSiteModRuntimeRelease>> {
|
||||||
|
const form = new FormData()
|
||||||
|
form.append('runtime', file, file.name)
|
||||||
|
return skinSiteRequest<SkinSiteModRuntimeRelease>(
|
||||||
|
'runtime/admin/upload?version=' + encodeURIComponent(version),
|
||||||
|
{ method: 'POST', type: 'mod', form },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function skinSiteActivateModRuntime(
|
||||||
|
id: number,
|
||||||
|
): Promise<SkinSiteResponse<SkinSiteModRuntimeRelease>> {
|
||||||
|
return skinSiteRequest<SkinSiteModRuntimeRelease>('runtime/admin/' + id + '/activate', {
|
||||||
|
method: 'POST',
|
||||||
|
type: 'mod',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function skinSiteDeleteModRuntime(id: number): Promise<SkinSiteResponse<string>> {
|
||||||
|
return skinSiteRequest<string>('runtime/admin/' + id, { method: 'DELETE', type: 'mod' })
|
||||||
|
}
|
||||||
@@ -12049,5 +12049,52 @@
|
|||||||
"app.skin-site.admin.donations.updated": {"message": "Donation updated."},
|
"app.skin-site.admin.donations.updated": {"message": "Donation updated."},
|
||||||
"app.skin-site.admin.donations.deleted": {"message": "Donation deleted."},
|
"app.skin-site.admin.donations.deleted": {"message": "Donation deleted."},
|
||||||
"app.skin-site.admin.donations.confirm-delete": {"message": "Delete the donation from “{name}”? This cannot be undone."},
|
"app.skin-site.admin.donations.confirm-delete": {"message": "Delete the donation from “{name}”? This cannot be undone."},
|
||||||
"app.skin-site.admin.donations.load-more": {"message": "Load more"}
|
"app.skin-site.admin.donations.load-more": {"message": "Load more"},
|
||||||
|
"app.skin-site.admin.mods.title": {"message": "Mods"},
|
||||||
|
"app.skin-site.admin.mods.tab-mods": {"message": "Mod files"},
|
||||||
|
"app.skin-site.admin.mods.tab-tags": {"message": "Rule tags"},
|
||||||
|
"app.skin-site.admin.mods.tab-runtime": {"message": "Runtime"},
|
||||||
|
"app.skin-site.admin.mods.loading": {"message": "Loading…"},
|
||||||
|
"app.skin-site.admin.mods.failed": {"message": "Could not load mods."},
|
||||||
|
"app.skin-site.admin.mods.empty": {"message": "No mods found."},
|
||||||
|
"app.skin-site.admin.mods.total": {"message": "{count} mods"},
|
||||||
|
"app.skin-site.admin.mods.search": {"message": "Search by name"},
|
||||||
|
"app.skin-site.admin.mods.filter-all": {"message": "All tags"},
|
||||||
|
"app.skin-site.admin.mods.prev": {"message": "Previous"},
|
||||||
|
"app.skin-site.admin.mods.next": {"message": "Next"},
|
||||||
|
"app.skin-site.admin.mods.page": {"message": "Page {page}"},
|
||||||
|
"app.skin-site.admin.mods.name": {"message": "Name"},
|
||||||
|
"app.skin-site.admin.mods.version": {"message": "Version"},
|
||||||
|
"app.skin-site.admin.mods.mod-id": {"message": "Mod ID"},
|
||||||
|
"app.skin-site.admin.mods.size": {"message": "Size"},
|
||||||
|
"app.skin-site.admin.mods.loaders": {"message": "Loaders"},
|
||||||
|
"app.skin-site.admin.mods.domain-rules": {"message": "Domain rules"},
|
||||||
|
"app.skin-site.admin.mods.edit": {"message": "Edit"},
|
||||||
|
"app.skin-site.admin.mods.save": {"message": "Save"},
|
||||||
|
"app.skin-site.admin.mods.cancel": {"message": "Cancel"},
|
||||||
|
"app.skin-site.admin.mods.remove": {"message": "Delete"},
|
||||||
|
"app.skin-site.admin.mods.updated": {"message": "Mod updated."},
|
||||||
|
"app.skin-site.admin.mods.deleted": {"message": "Mod deleted."},
|
||||||
|
"app.skin-site.admin.mods.confirm-delete": {"message": "Delete mod “{name}”? This cannot be undone."},
|
||||||
|
"app.skin-site.admin.mods.rule-tags": {"message": "Rule tags"},
|
||||||
|
"app.skin-site.admin.mods.tag-name": {"message": "Tag name"},
|
||||||
|
"app.skin-site.admin.mods.tag-rules": {"message": "Rules"},
|
||||||
|
"app.skin-site.admin.mods.tag-rules-hint": {"message": "One rule per line."},
|
||||||
|
"app.skin-site.admin.mods.new-tag": {"message": "New tag"},
|
||||||
|
"app.skin-site.admin.mods.tag-created": {"message": "Tag created."},
|
||||||
|
"app.skin-site.admin.mods.tag-updated": {"message": "Tag updated."},
|
||||||
|
"app.skin-site.admin.mods.tag-deleted": {"message": "Tag deleted."},
|
||||||
|
"app.skin-site.admin.mods.confirm-delete-tag": {"message": "Delete tag “{name}”?"},
|
||||||
|
"app.skin-site.admin.mods.runtime": {"message": "Runtime releases"},
|
||||||
|
"app.skin-site.admin.mods.manifest-url": {"message": "Manifest URL"},
|
||||||
|
"app.skin-site.admin.mods.upload-runtime": {"message": "Upload runtime"},
|
||||||
|
"app.skin-site.admin.mods.runtime-version": {"message": "Version"},
|
||||||
|
"app.skin-site.admin.mods.uploading": {"message": "Uploading…"},
|
||||||
|
"app.skin-site.admin.mods.runtime-uploaded": {"message": "Runtime uploaded."},
|
||||||
|
"app.skin-site.admin.mods.activate": {"message": "Activate"},
|
||||||
|
"app.skin-site.admin.mods.activated": {"message": "Runtime activated."},
|
||||||
|
"app.skin-site.admin.mods.available": {"message": "Available"},
|
||||||
|
"app.skin-site.admin.mods.unavailable": {"message": "Unavailable"},
|
||||||
|
"app.skin-site.admin.mods.active": {"message": "Active"},
|
||||||
|
"app.skin-site.admin.mods.uploader": {"message": "by {name}"}
|
||||||
}
|
}
|
||||||
@@ -11880,5 +11880,52 @@
|
|||||||
"app.skin-site.admin.donations.updated": {"message": "捐赠记录已更新。"},
|
"app.skin-site.admin.donations.updated": {"message": "捐赠记录已更新。"},
|
||||||
"app.skin-site.admin.donations.deleted": {"message": "捐赠记录已删除。"},
|
"app.skin-site.admin.donations.deleted": {"message": "捐赠记录已删除。"},
|
||||||
"app.skin-site.admin.donations.confirm-delete": {"message": "确定删除「{name}」的捐赠记录吗?此操作不可撤销。"},
|
"app.skin-site.admin.donations.confirm-delete": {"message": "确定删除「{name}」的捐赠记录吗?此操作不可撤销。"},
|
||||||
"app.skin-site.admin.donations.load-more": {"message": "加载更多"}
|
"app.skin-site.admin.donations.load-more": {"message": "加载更多"},
|
||||||
|
"app.skin-site.admin.mods.title": {"message": "Mod 管理"},
|
||||||
|
"app.skin-site.admin.mods.tab-mods": {"message": "Mod 文件"},
|
||||||
|
"app.skin-site.admin.mods.tab-tags": {"message": "规则标签"},
|
||||||
|
"app.skin-site.admin.mods.tab-runtime": {"message": "运行时"},
|
||||||
|
"app.skin-site.admin.mods.loading": {"message": "加载中…"},
|
||||||
|
"app.skin-site.admin.mods.failed": {"message": "无法加载 Mod 列表。"},
|
||||||
|
"app.skin-site.admin.mods.empty": {"message": "没有找到 Mod。"},
|
||||||
|
"app.skin-site.admin.mods.total": {"message": "共 {count} 个 Mod"},
|
||||||
|
"app.skin-site.admin.mods.search": {"message": "按名称搜索"},
|
||||||
|
"app.skin-site.admin.mods.filter-all": {"message": "全部标签"},
|
||||||
|
"app.skin-site.admin.mods.prev": {"message": "上一页"},
|
||||||
|
"app.skin-site.admin.mods.next": {"message": "下一页"},
|
||||||
|
"app.skin-site.admin.mods.page": {"message": "第 {page} 页"},
|
||||||
|
"app.skin-site.admin.mods.name": {"message": "名称"},
|
||||||
|
"app.skin-site.admin.mods.version": {"message": "版本"},
|
||||||
|
"app.skin-site.admin.mods.mod-id": {"message": "Mod ID"},
|
||||||
|
"app.skin-site.admin.mods.size": {"message": "大小"},
|
||||||
|
"app.skin-site.admin.mods.loaders": {"message": "加载器"},
|
||||||
|
"app.skin-site.admin.mods.domain-rules": {"message": "域名规则"},
|
||||||
|
"app.skin-site.admin.mods.edit": {"message": "编辑"},
|
||||||
|
"app.skin-site.admin.mods.save": {"message": "保存"},
|
||||||
|
"app.skin-site.admin.mods.cancel": {"message": "取消"},
|
||||||
|
"app.skin-site.admin.mods.remove": {"message": "删除"},
|
||||||
|
"app.skin-site.admin.mods.updated": {"message": "Mod 已更新。"},
|
||||||
|
"app.skin-site.admin.mods.deleted": {"message": "Mod 已删除。"},
|
||||||
|
"app.skin-site.admin.mods.confirm-delete": {"message": "确定删除 Mod「{name}」吗?此操作不可撤销。"},
|
||||||
|
"app.skin-site.admin.mods.rule-tags": {"message": "规则标签"},
|
||||||
|
"app.skin-site.admin.mods.tag-name": {"message": "标签名称"},
|
||||||
|
"app.skin-site.admin.mods.tag-rules": {"message": "规则"},
|
||||||
|
"app.skin-site.admin.mods.tag-rules-hint": {"message": "每行一条规则。"},
|
||||||
|
"app.skin-site.admin.mods.new-tag": {"message": "新建标签"},
|
||||||
|
"app.skin-site.admin.mods.tag-created": {"message": "标签已创建。"},
|
||||||
|
"app.skin-site.admin.mods.tag-updated": {"message": "标签已更新。"},
|
||||||
|
"app.skin-site.admin.mods.tag-deleted": {"message": "标签已删除。"},
|
||||||
|
"app.skin-site.admin.mods.confirm-delete-tag": {"message": "确定删除标签「{name}」吗?"},
|
||||||
|
"app.skin-site.admin.mods.runtime": {"message": "运行时版本"},
|
||||||
|
"app.skin-site.admin.mods.manifest-url": {"message": "清单地址"},
|
||||||
|
"app.skin-site.admin.mods.upload-runtime": {"message": "上传运行时"},
|
||||||
|
"app.skin-site.admin.mods.runtime-version": {"message": "版本"},
|
||||||
|
"app.skin-site.admin.mods.uploading": {"message": "上传中…"},
|
||||||
|
"app.skin-site.admin.mods.runtime-uploaded": {"message": "运行时已上传。"},
|
||||||
|
"app.skin-site.admin.mods.activate": {"message": "设为当前"},
|
||||||
|
"app.skin-site.admin.mods.activated": {"message": "运行时已启用。"},
|
||||||
|
"app.skin-site.admin.mods.available": {"message": "可用"},
|
||||||
|
"app.skin-site.admin.mods.unavailable": {"message": "不可用"},
|
||||||
|
"app.skin-site.admin.mods.active": {"message": "当前版本"},
|
||||||
|
"app.skin-site.admin.mods.uploader": {"message": "由 {name} 上传"}
|
||||||
}
|
}
|
||||||
@@ -34,6 +34,7 @@ const allowedUntranslatedMessages = new Set([
|
|||||||
'QQ',
|
'QQ',
|
||||||
'IP',
|
'IP',
|
||||||
'URL',
|
'URL',
|
||||||
|
'Mod ID',
|
||||||
'LogShare AI',
|
'LogShare AI',
|
||||||
'NeoForge',
|
'NeoForge',
|
||||||
'OptiFine',
|
'OptiFine',
|
||||||
|
|||||||
Reference in new issue
Block a user