mirror of
https://github.com/modrinth/code.git
synced 2026-07-31 21:26:40 +00:00
Compare commits
26
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c6c671c668 | ||
|
|
dc569ecd89 | ||
|
|
7bfb418a11 | ||
|
|
b569354793 | ||
|
|
b667b3ae9e | ||
|
|
16de6182e4 | ||
|
|
364fed52dd | ||
|
|
0fe695721a | ||
|
|
692f22b749 | ||
|
|
ed7792097d | ||
|
|
c56ce6c9ee | ||
|
|
91bc58b8ee | ||
|
|
38d757b66a | ||
|
|
862b54f00d | ||
|
|
c2f4835fe7 | ||
|
|
b97c191d75 | ||
|
|
b0c6a06909 | ||
|
|
fc3e384f54 | ||
|
|
87ff8fd4d9 | ||
|
|
288c1fb970 | ||
|
|
f6e6ac28d4 | ||
|
|
4a972bca73 | ||
|
|
b09c26f616 | ||
|
|
204bd52a5a | ||
|
|
b59f327b04 | ||
|
|
129dfa66ca |
@@ -76,13 +76,6 @@ jobs:
|
||||
env:
|
||||
SCCACHE_DIR: ${{ needs.skip-if-clean.outputs.internal == 'true' && '/mnt/sccache' || '' }}
|
||||
SCCACHE_CACHE_SIZE: ${{ needs.skip-if-clean.outputs.internal == 'true' && '10G' || '' }}
|
||||
SCCACHE_MULTILEVEL_CHAIN: ${{ needs.skip-if-clean.outputs.internal == 'true' && 'disk,s3' || '' }}
|
||||
SCCACHE_S3_KEY_PREFIX: ${{ needs.skip-if-clean.outputs.internal == 'true' && format('{0}/', github.repository) || '' }}
|
||||
SCCACHE_BUCKET: ${{ secrets.SCCACHE_BUCKET }}
|
||||
SCCACHE_REGION: ${{ secrets.SCCACHE_REGION }}
|
||||
SCCACHE_ENDPOINT: ${{ secrets.SCCACHE_ENDPOINT }}
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.SCCACHE_S3_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.SCCACHE_S3_SECRET_ACCESS_KEY }}
|
||||
RUSTC_WRAPPER: ${{ needs.skip-if-clean.outputs.internal == 'true' && 'sccache' || '' }}
|
||||
needs: [skip-if-clean]
|
||||
if: ${{ needs.skip-if-clean.outputs.skip != 'true' }}
|
||||
|
||||
@@ -78,13 +78,6 @@ jobs:
|
||||
GIT_HASH: ${{ github.sha }}
|
||||
SCCACHE_DIR: ${{ needs.skip-if-clean.outputs.internal == 'true' && '/mnt/sccache' || '' }}
|
||||
SCCACHE_CACHE_SIZE: ${{ needs.skip-if-clean.outputs.internal == 'true' && '10G' || '' }}
|
||||
SCCACHE_MULTILEVEL_CHAIN: ${{ needs.skip-if-clean.outputs.internal == 'true' && 'disk,s3' || '' }}
|
||||
SCCACHE_S3_KEY_PREFIX: ${{ needs.skip-if-clean.outputs.internal == 'true' && format('{0}/', github.repository) || '' }}
|
||||
SCCACHE_BUCKET: ${{ secrets.SCCACHE_BUCKET }}
|
||||
SCCACHE_REGION: ${{ secrets.SCCACHE_REGION }}
|
||||
SCCACHE_ENDPOINT: ${{ secrets.SCCACHE_ENDPOINT }}
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.SCCACHE_S3_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.SCCACHE_S3_SECRET_ACCESS_KEY }}
|
||||
RUSTC_WRAPPER: ${{ needs.skip-if-clean.outputs.internal == 'true' && 'sccache' || '' }}
|
||||
steps:
|
||||
- name: Check out code
|
||||
|
||||
@@ -81,13 +81,6 @@ jobs:
|
||||
# blacksmith runner)
|
||||
SCCACHE_DIR: ${{ needs.skip-if-clean.outputs.internal == 'true' && '/mnt/sccache' || '' }}
|
||||
SCCACHE_CACHE_SIZE: ${{ needs.skip-if-clean.outputs.internal == 'true' && '10G' || '' }}
|
||||
SCCACHE_MULTILEVEL_CHAIN: ${{ needs.skip-if-clean.outputs.internal == 'true' && 'disk,s3' || '' }}
|
||||
SCCACHE_S3_KEY_PREFIX: ${{ needs.skip-if-clean.outputs.internal == 'true' && format('{0}/', github.repository) || '' }}
|
||||
SCCACHE_BUCKET: ${{ secrets.SCCACHE_BUCKET }}
|
||||
SCCACHE_REGION: ${{ secrets.SCCACHE_REGION }}
|
||||
SCCACHE_ENDPOINT: ${{ secrets.SCCACHE_ENDPOINT }}
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.SCCACHE_S3_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.SCCACHE_S3_SECRET_ACCESS_KEY }}
|
||||
RUSTC_WRAPPER: ${{ needs.skip-if-clean.outputs.internal == 'true' && 'sccache' || '' }}
|
||||
|
||||
steps:
|
||||
|
||||
@@ -1,28 +1,32 @@
|
||||
<script setup>
|
||||
import { WrenchIcon, XIcon } from '@modrinth/assets'
|
||||
import { XIcon } from '@modrinth/assets'
|
||||
import {
|
||||
Accordion,
|
||||
ButtonStyled,
|
||||
Checkbox,
|
||||
commonMessages,
|
||||
defineMessages,
|
||||
FileTreeSelect,
|
||||
injectNotificationManager,
|
||||
NewModal,
|
||||
StyledInput,
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
import { save } from '@tauri-apps/plugin-dialog'
|
||||
import { readDir, stat } from '@tauri-apps/plugin-fs'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import { PackageIcon, VersionIcon } from '@/assets/icons'
|
||||
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
||||
import { export_instance_mrpack, get_pack_export_candidates } from '@/helpers/instance'
|
||||
import { PackageIcon } from '@/assets/icons'
|
||||
import {
|
||||
export_instance_mrpack,
|
||||
get_full_path,
|
||||
get_pack_export_candidates,
|
||||
} from '@/helpers/instance'
|
||||
|
||||
const { handleError } = injectNotificationManager()
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
const messages = defineMessages({
|
||||
header: { id: 'app.export-modal.header', defaultMessage: 'Export modpack' },
|
||||
modpackNameLabel: { id: 'app.export-modal.modpack-name-label', defaultMessage: 'Modpack Name' },
|
||||
modpackNameLabel: { id: 'app.export-modal.modpack-name-label', defaultMessage: 'Modpack name' },
|
||||
modpackNamePlaceholder: {
|
||||
id: 'app.export-modal.modpack-name-placeholder',
|
||||
defaultMessage: 'Modpack name',
|
||||
@@ -39,15 +43,7 @@ const messages = defineMessages({
|
||||
id: 'app.export-modal.description-placeholder',
|
||||
defaultMessage: 'Enter modpack description...',
|
||||
},
|
||||
selectFilesLabel: {
|
||||
id: 'app.export-modal.select-files-label',
|
||||
defaultMessage: 'Configure which files are included in this export',
|
||||
},
|
||||
exportButton: { id: 'app.export-modal.export-button', defaultMessage: 'Export' },
|
||||
includeFile: {
|
||||
id: 'app.export-modal.include-file-accessibility-label',
|
||||
defaultMessage: 'Include "{file}"?',
|
||||
},
|
||||
})
|
||||
|
||||
const props = defineProps({
|
||||
@@ -59,8 +55,9 @@ const props = defineProps({
|
||||
|
||||
defineExpose({
|
||||
show: () => {
|
||||
resetExportState()
|
||||
exportModal.value.show()
|
||||
initFiles()
|
||||
void initFiles().catch(handleError)
|
||||
},
|
||||
})
|
||||
|
||||
@@ -69,62 +66,33 @@ const nameInput = ref(props.instance.name)
|
||||
const exportDescription = ref('')
|
||||
const versionInput = ref('1.0.0')
|
||||
const files = ref([])
|
||||
const folders = ref([])
|
||||
const selectedFilePaths = ref([])
|
||||
const fileTreeKey = ref(0)
|
||||
const filesLoadId = ref(0)
|
||||
const instanceRoot = ref('')
|
||||
const loadedDirectories = ref(new Set())
|
||||
|
||||
const initFiles = async () => {
|
||||
const newFolders = new Map()
|
||||
const sep = '/'
|
||||
files.value = []
|
||||
await get_pack_export_candidates(props.instance.id).then((filePaths) =>
|
||||
filePaths
|
||||
.map((folder) => ({
|
||||
path: folder,
|
||||
name: folder.split(sep).pop(),
|
||||
selected:
|
||||
folder.startsWith('mods') ||
|
||||
folder.startsWith('datapacks') ||
|
||||
folder.startsWith('resourcepacks') ||
|
||||
folder.startsWith('shaderpacks') ||
|
||||
folder.startsWith('config'),
|
||||
disabled:
|
||||
folder === 'profile.json' ||
|
||||
folder.startsWith('modrinth_logs') ||
|
||||
folder.startsWith('.fabric') ||
|
||||
folder.startsWith('__MACOSX'),
|
||||
}))
|
||||
.forEach((pathData) => {
|
||||
const parent = pathData.path.split(sep).slice(0, -1).join(sep)
|
||||
if (parent !== '') {
|
||||
if (newFolders.has(parent)) {
|
||||
newFolders.get(parent).push(pathData)
|
||||
} else {
|
||||
newFolders.set(parent, [pathData])
|
||||
}
|
||||
} else {
|
||||
files.value.push(pathData)
|
||||
}
|
||||
}),
|
||||
)
|
||||
folders.value = [...newFolders.entries()].map(([name, value]) => [
|
||||
{
|
||||
name,
|
||||
showingMore: false,
|
||||
},
|
||||
value,
|
||||
async function initFiles() {
|
||||
const loadId = ++filesLoadId.value
|
||||
const [filePaths, root] = await Promise.all([
|
||||
get_pack_export_candidates(props.instance.id),
|
||||
get_full_path(props.instance.id),
|
||||
])
|
||||
if (loadId !== filesLoadId.value) return
|
||||
|
||||
instanceRoot.value = root
|
||||
const exportCandidates = await Promise.all(
|
||||
filePaths.map((path) => buildExportCandidateItem(root, path)),
|
||||
)
|
||||
if (loadId !== filesLoadId.value) return
|
||||
|
||||
files.value = exportCandidates
|
||||
selectedFilePaths.value = files.value
|
||||
.filter((file) => !file.disabled && isDefaultSelectedExportCandidate(file.path))
|
||||
.map((file) => file.path)
|
||||
}
|
||||
|
||||
await initFiles()
|
||||
|
||||
const exportPack = async () => {
|
||||
const filesToExport = files.value.filter((file) => file.selected).map((file) => file.path)
|
||||
folders.value.forEach((args) => {
|
||||
args[1].forEach((child) => {
|
||||
if (child.selected) {
|
||||
filesToExport.push(child.path)
|
||||
}
|
||||
})
|
||||
})
|
||||
const outputPath = await save({
|
||||
defaultPath: `${nameInput.value} ${versionInput.value}.mrpack`,
|
||||
filters: [
|
||||
@@ -139,7 +107,7 @@ const exportPack = async () => {
|
||||
export_instance_mrpack(
|
||||
props.instance.id,
|
||||
outputPath,
|
||||
filesToExport,
|
||||
selectedFilePaths.value,
|
||||
versionInput.value,
|
||||
exportDescription.value,
|
||||
nameInput.value,
|
||||
@@ -147,94 +115,176 @@ const exportPack = async () => {
|
||||
exportModal.value.hide()
|
||||
}
|
||||
}
|
||||
|
||||
function resetExportState() {
|
||||
nameInput.value = props.instance.name
|
||||
exportDescription.value = ''
|
||||
versionInput.value = '1.0.0'
|
||||
files.value = []
|
||||
selectedFilePaths.value = []
|
||||
fileTreeKey.value += 1
|
||||
instanceRoot.value = ''
|
||||
loadedDirectories.value = new Set()
|
||||
}
|
||||
|
||||
async function loadExportDirectory(path) {
|
||||
if (!path || !instanceRoot.value || loadedDirectories.value.has(path)) return
|
||||
|
||||
const loadId = filesLoadId.value
|
||||
loadedDirectories.value.add(path)
|
||||
|
||||
try {
|
||||
const entries = await readDir(`${instanceRoot.value}/${path}`)
|
||||
const childItems = await Promise.all(
|
||||
entries.map((entry) => buildExportDirectoryChildItem(instanceRoot.value, path, entry)),
|
||||
)
|
||||
if (loadId !== filesLoadId.value) return
|
||||
|
||||
appendExportItems(childItems)
|
||||
} catch {
|
||||
loadedDirectories.value.delete(path)
|
||||
}
|
||||
}
|
||||
|
||||
async function buildExportCandidateItem(instanceRoot, path) {
|
||||
try {
|
||||
const entries = await readDir(`${instanceRoot}/${path}`)
|
||||
const metadata = await getExportCandidateMetadata(instanceRoot, path)
|
||||
return {
|
||||
path,
|
||||
type: 'directory',
|
||||
disabled: isExportCandidateDisabled(path),
|
||||
modified: metadata.modified,
|
||||
count: entries.length,
|
||||
}
|
||||
} catch {
|
||||
return buildExportFileItem(instanceRoot, path)
|
||||
}
|
||||
}
|
||||
|
||||
async function buildExportDirectoryChildItem(instanceRoot, parentPath, entry) {
|
||||
const path = `${parentPath}/${entry.name}`
|
||||
if (entry.isDirectory) {
|
||||
const metadata = await getExportCandidateMetadata(instanceRoot, path)
|
||||
return {
|
||||
path,
|
||||
type: 'directory',
|
||||
disabled: isExportCandidateDisabled(path),
|
||||
modified: metadata.modified,
|
||||
}
|
||||
}
|
||||
|
||||
return buildExportFileItem(instanceRoot, path)
|
||||
}
|
||||
|
||||
async function buildExportFileItem(instanceRoot, path) {
|
||||
const metadata = await getExportCandidateMetadata(instanceRoot, path)
|
||||
return {
|
||||
path,
|
||||
type: 'file',
|
||||
disabled: isExportCandidateDisabled(path),
|
||||
size: metadata.size,
|
||||
modified: metadata.modified,
|
||||
}
|
||||
}
|
||||
|
||||
function appendExportItems(items) {
|
||||
const nextFiles = new Map(files.value.map((file) => [normalizeExportPath(file.path), file]))
|
||||
for (const item of items) {
|
||||
nextFiles.set(normalizeExportPath(item.path), item)
|
||||
}
|
||||
files.value = [...nextFiles.values()]
|
||||
}
|
||||
|
||||
async function getExportCandidateMetadata(instanceRoot, path) {
|
||||
try {
|
||||
const metadata = await stat(`${instanceRoot}/${path}`)
|
||||
return {
|
||||
size: metadata.size,
|
||||
modified: metadata.mtime ? Math.floor(metadata.mtime.getTime() / 1000) : undefined,
|
||||
}
|
||||
} catch {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeExportPath(path) {
|
||||
return path.replaceAll('\\', '/').split('/').filter(Boolean).join('/')
|
||||
}
|
||||
|
||||
function isDefaultSelectedExportCandidate(path) {
|
||||
return (
|
||||
path.startsWith('mods') ||
|
||||
path.startsWith('datapacks') ||
|
||||
path.startsWith('resourcepacks') ||
|
||||
path.startsWith('shaderpacks') ||
|
||||
path.startsWith('config')
|
||||
)
|
||||
}
|
||||
|
||||
function isExportCandidateDisabled(path) {
|
||||
return (
|
||||
path === 'profile.json' ||
|
||||
path.startsWith('modrinth_logs') ||
|
||||
path.startsWith('.fabric') ||
|
||||
path.startsWith('__MACOSX')
|
||||
)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ModalWrapper ref="exportModal" :header="formatMessage(messages.header)">
|
||||
<div class="flex flex-col gap-4 w-[40rem]">
|
||||
<NewModal
|
||||
ref="exportModal"
|
||||
:header="formatMessage(messages.header)"
|
||||
scrollable
|
||||
width="46rem"
|
||||
max-width="calc(100vw - 2rem)"
|
||||
>
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="labeled_input">
|
||||
<p>{{ formatMessage(messages.modpackNameLabel) }}</p>
|
||||
<div class="labeled_input w-full">
|
||||
<p class="text-contrast font-semibold">{{ formatMessage(messages.modpackNameLabel) }}</p>
|
||||
<StyledInput
|
||||
v-model="nameInput"
|
||||
:icon="PackageIcon"
|
||||
type="text"
|
||||
:placeholder="formatMessage(messages.modpackNamePlaceholder)"
|
||||
clearable
|
||||
wrapper-class="w-full"
|
||||
/>
|
||||
</div>
|
||||
<div class="labeled_input">
|
||||
<p>{{ formatMessage(messages.versionNumberLabel) }}</p>
|
||||
<div class="labeled_input w-full">
|
||||
<p class="text-contrast font-semibold">
|
||||
{{ formatMessage(messages.versionNumberLabel) }}
|
||||
</p>
|
||||
<StyledInput
|
||||
v-model="versionInput"
|
||||
:icon="VersionIcon"
|
||||
type="text"
|
||||
:placeholder="formatMessage(messages.versionNumberPlaceholder)"
|
||||
clearable
|
||||
wrapper-class="w-full"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col gap-2">
|
||||
<p class="m-0">{{ formatMessage(commonMessages.descriptionLabel) }}</p>
|
||||
<div class="flex flex-col gap-2 min-w-0">
|
||||
<p class="m-0 text-contrast font-semibold">
|
||||
{{ formatMessage(commonMessages.descriptionLabel) }}
|
||||
</p>
|
||||
<StyledInput
|
||||
v-model="exportDescription"
|
||||
multiline
|
||||
:placeholder="formatMessage(messages.descriptionPlaceholder)"
|
||||
wrapper-class="w-full"
|
||||
/>
|
||||
</div>
|
||||
<Accordion
|
||||
class="w-full bg-surface-4 border border-solid border-surface-5 rounded-2xl overflow-clip"
|
||||
button-class="p-4 w-full border-b border-solid border-b-surface-5 bg-surface-2 -mb-px hover:brightness-[--hover-brightness] group"
|
||||
>
|
||||
<template #title>
|
||||
<span class="flex items-center gap-3 text-contrast group-active:scale-[0.98]">
|
||||
<WrenchIcon aria-hidden="true" class="size-5 text-secondary" />
|
||||
Configure which files are included in this export
|
||||
</span>
|
||||
</template>
|
||||
<div class="flex flex-col [&>*:nth-child(even)]:bg-surface-3">
|
||||
<div v-for="[path, children] in folders" :key="path.name" class="flex flex-col">
|
||||
<Accordion
|
||||
class="flex flex-col"
|
||||
button-class="flex gap-3 pr-4 hover:bg-surface-5 group"
|
||||
>
|
||||
<template #title>
|
||||
<Checkbox
|
||||
:model-value="children.every((child) => child.selected)"
|
||||
:indeterminate="
|
||||
!children.every((child) => child.selected) &&
|
||||
children.some((child) => child.selected)
|
||||
"
|
||||
:description="formatMessage(messages.includeFile, { file: path.name })"
|
||||
class="pl-4 py-2"
|
||||
:disabled="children.every((x) => x.disabled)"
|
||||
@update:model-value="
|
||||
(newValue) => children.forEach((child) => (child.selected = newValue))
|
||||
"
|
||||
@click.stop
|
||||
/>
|
||||
<span class="ml-2 group-active:scale-95">{{ path.name }}/</span>
|
||||
</template>
|
||||
<div v-for="child in children" :key="child.path">
|
||||
<Checkbox
|
||||
v-model="child.selected"
|
||||
:label="child.name"
|
||||
class="w-full px-8 py-2 hover:bg-surface-4 text-primary"
|
||||
:disabled="child.disabled"
|
||||
/>
|
||||
</div>
|
||||
</Accordion>
|
||||
</div>
|
||||
<Checkbox
|
||||
v-for="file in files"
|
||||
:key="file.path"
|
||||
v-model="file.selected"
|
||||
:label="file.name"
|
||||
:disabled="file.disabled"
|
||||
class="w-full px-4 py-2 hover:bg-surface-4 text-primary"
|
||||
/>
|
||||
</div>
|
||||
</Accordion>
|
||||
<FileTreeSelect
|
||||
:key="fileTreeKey"
|
||||
v-model="selectedFilePaths"
|
||||
class="min-w-0"
|
||||
:items="files"
|
||||
@navigate="loadExportDirectory"
|
||||
/>
|
||||
</div>
|
||||
<template #actions>
|
||||
<div class="flex items-center justify-end gap-2">
|
||||
<ButtonStyled type="outlined">
|
||||
<button @click="exportModal.hide">
|
||||
@@ -249,6 +299,6 @@ const exportPack = async () => {
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</div>
|
||||
</ModalWrapper>
|
||||
</template>
|
||||
</NewModal>
|
||||
</template>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { MailIcon, SendIcon, UserIcon, UserPlusIcon, XIcon } from '@modrinth/assets'
|
||||
import { MailIcon, SearchIcon, SendIcon, UserIcon, UserPlusIcon, XIcon } from '@modrinth/assets'
|
||||
import {
|
||||
Avatar,
|
||||
ButtonStyled,
|
||||
@@ -301,11 +301,12 @@ const messages = defineMessages({
|
||||
</ButtonStyled>
|
||||
<StyledInput
|
||||
v-model="search"
|
||||
:icon="SearchIcon"
|
||||
type="text"
|
||||
:placeholder="formatMessage(messages.searchFriends)"
|
||||
clearable
|
||||
variant="outlined"
|
||||
wrapper-class="flex-1"
|
||||
input-class="!bg-transparent !border !border-solid !border-button-bg !text-primary !placeholder:text-primary"
|
||||
wrapper-class="flex-1 [&>svg]:!text-primary [&>svg]:!opacity-100"
|
||||
@keyup.esc="search = ''"
|
||||
/>
|
||||
</template>
|
||||
|
||||
@@ -404,28 +404,35 @@ export async function refreshServerData(
|
||||
): Promise<void> {
|
||||
const refreshTime = Date.now()
|
||||
serverData.refreshing = true
|
||||
await get_server_status(address, protocolVersion)
|
||||
.then((status) => {
|
||||
if (serverData.lastSuccessfulRefresh && serverData.lastSuccessfulRefresh > refreshTime) {
|
||||
// Don't update if there was a more recent successful refresh
|
||||
return
|
||||
}
|
||||
serverData.lastSuccessfulRefresh = Date.now()
|
||||
serverData.status = status
|
||||
if (status.description) {
|
||||
serverData.rawMotd = status.description
|
||||
serverData.renderedMotd = autoToHTML(status.description)
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
serverData.refreshing = false
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(`Refreshing addr ${address}`, protocolVersion, err)
|
||||
if (!protocolVersion?.legacy) {
|
||||
refreshServerData(serverData, { version: 74, legacy: true }, address)
|
||||
}
|
||||
})
|
||||
try {
|
||||
const status = await get_server_status(address, protocolVersion)
|
||||
if (serverData.lastSuccessfulRefresh && serverData.lastSuccessfulRefresh > refreshTime) {
|
||||
// Don't update if there was a more recent successful refresh
|
||||
return
|
||||
}
|
||||
serverData.lastSuccessfulRefresh = Date.now()
|
||||
serverData.status = status
|
||||
if (status.description) {
|
||||
serverData.rawMotd = status.description
|
||||
serverData.renderedMotd = autoToHTML(status.description)
|
||||
} else {
|
||||
delete serverData.rawMotd
|
||||
delete serverData.renderedMotd
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`Refreshing addr ${address}`, protocolVersion, err)
|
||||
if (!protocolVersion?.legacy) {
|
||||
await refreshServerData(serverData, { version: 74, legacy: true }, address)
|
||||
return
|
||||
}
|
||||
if (!serverData.lastSuccessfulRefresh || serverData.lastSuccessfulRefresh <= refreshTime) {
|
||||
delete serverData.status
|
||||
delete serverData.rawMotd
|
||||
delete serverData.renderedMotd
|
||||
}
|
||||
} finally {
|
||||
serverData.refreshing = false
|
||||
}
|
||||
}
|
||||
|
||||
export function refreshServers(
|
||||
@@ -444,7 +451,7 @@ export function refreshServers(
|
||||
}
|
||||
})
|
||||
|
||||
// noinspection ES6MissingAwait - handled with .then by refreshServerData already
|
||||
// noinspection ES6MissingAwait - handled by refreshServerData
|
||||
Object.keys(serverData).forEach((address) =>
|
||||
refreshServerData(serverData[address], protocolVersion, address),
|
||||
)
|
||||
|
||||
@@ -122,9 +122,6 @@
|
||||
"app.export-modal.modpack-name-placeholder": {
|
||||
"message": "إسم حزمة التعديل"
|
||||
},
|
||||
"app.export-modal.select-files-label": {
|
||||
"message": "أختيار الملفات التي سيتم تصديرها"
|
||||
},
|
||||
"app.export-modal.version-number-label": {
|
||||
"message": "رقم الإصدار"
|
||||
},
|
||||
|
||||
@@ -203,18 +203,12 @@
|
||||
"app.export-modal.header": {
|
||||
"message": "Exportovat modpack"
|
||||
},
|
||||
"app.export-modal.include-file-accessibility-label": {
|
||||
"message": "Zahrnout \"{file}\"?"
|
||||
},
|
||||
"app.export-modal.modpack-name-label": {
|
||||
"message": "Název modpacku"
|
||||
},
|
||||
"app.export-modal.modpack-name-placeholder": {
|
||||
"message": "Název modpacku"
|
||||
},
|
||||
"app.export-modal.select-files-label": {
|
||||
"message": "Nastavte, které soubory mají být do tohoto exportu zahrnuty"
|
||||
},
|
||||
"app.export-modal.version-number-label": {
|
||||
"message": "Číslo verze"
|
||||
},
|
||||
|
||||
@@ -164,18 +164,12 @@
|
||||
"app.export-modal.header": {
|
||||
"message": "Eksporter modpack"
|
||||
},
|
||||
"app.export-modal.include-file-accessibility-label": {
|
||||
"message": "Inkludere \"{file}\"?"
|
||||
},
|
||||
"app.export-modal.modpack-name-label": {
|
||||
"message": "Modpack Navn"
|
||||
},
|
||||
"app.export-modal.modpack-name-placeholder": {
|
||||
"message": "Modpack navn"
|
||||
},
|
||||
"app.export-modal.select-files-label": {
|
||||
"message": "Konfigurer hvilke filer inkludere i denne eksport"
|
||||
},
|
||||
"app.export-modal.version-number-label": {
|
||||
"message": "Versionsnummer"
|
||||
},
|
||||
|
||||
@@ -203,18 +203,12 @@
|
||||
"app.export-modal.header": {
|
||||
"message": "Modpack exportieren"
|
||||
},
|
||||
"app.export-modal.include-file-accessibility-label": {
|
||||
"message": "\"{file}\" einschliessen?"
|
||||
},
|
||||
"app.export-modal.modpack-name-label": {
|
||||
"message": "Modpaketname"
|
||||
},
|
||||
"app.export-modal.modpack-name-placeholder": {
|
||||
"message": "Modpaketname"
|
||||
},
|
||||
"app.export-modal.select-files-label": {
|
||||
"message": "Konfiguriere, welche Dateien in diesem Export miteinbezogen werden"
|
||||
},
|
||||
"app.export-modal.version-number-label": {
|
||||
"message": "Versionsnummer"
|
||||
},
|
||||
|
||||
@@ -203,18 +203,12 @@
|
||||
"app.export-modal.header": {
|
||||
"message": "Modpack exportieren"
|
||||
},
|
||||
"app.export-modal.include-file-accessibility-label": {
|
||||
"message": "\"{file}\" einschließen?"
|
||||
},
|
||||
"app.export-modal.modpack-name-label": {
|
||||
"message": "Modpackname"
|
||||
},
|
||||
"app.export-modal.modpack-name-placeholder": {
|
||||
"message": "Modpackname"
|
||||
},
|
||||
"app.export-modal.select-files-label": {
|
||||
"message": "Konfiguriere, welche Dateien in diesen Export enthalten sind"
|
||||
},
|
||||
"app.export-modal.version-number-label": {
|
||||
"message": "Versionsnummer"
|
||||
},
|
||||
|
||||
@@ -203,18 +203,12 @@
|
||||
"app.export-modal.header": {
|
||||
"message": "Export modpack"
|
||||
},
|
||||
"app.export-modal.include-file-accessibility-label": {
|
||||
"message": "Include \"{file}\"?"
|
||||
},
|
||||
"app.export-modal.modpack-name-label": {
|
||||
"message": "Modpack Name"
|
||||
"message": "Modpack name"
|
||||
},
|
||||
"app.export-modal.modpack-name-placeholder": {
|
||||
"message": "Modpack name"
|
||||
},
|
||||
"app.export-modal.select-files-label": {
|
||||
"message": "Configure which files are included in this export"
|
||||
},
|
||||
"app.export-modal.version-number-label": {
|
||||
"message": "Version number"
|
||||
},
|
||||
@@ -542,6 +536,9 @@
|
||||
"app.skins.section.builders-and-biomes": {
|
||||
"message": "Builders & Biomes"
|
||||
},
|
||||
"app.skins.section.chaos-cubed": {
|
||||
"message": "Chaos Cubed"
|
||||
},
|
||||
"app.skins.section.chase-the-skies": {
|
||||
"message": "Chase the Skies"
|
||||
},
|
||||
|
||||
@@ -203,18 +203,12 @@
|
||||
"app.export-modal.header": {
|
||||
"message": "Exportar modpack"
|
||||
},
|
||||
"app.export-modal.include-file-accessibility-label": {
|
||||
"message": "¿Incluir \"{file}\"?"
|
||||
},
|
||||
"app.export-modal.modpack-name-label": {
|
||||
"message": "Nombre del modpack"
|
||||
},
|
||||
"app.export-modal.modpack-name-placeholder": {
|
||||
"message": "Nombre del modpack"
|
||||
},
|
||||
"app.export-modal.select-files-label": {
|
||||
"message": "Configura que archivos incluir en esta exportación"
|
||||
},
|
||||
"app.export-modal.version-number-label": {
|
||||
"message": "Número de la versión"
|
||||
},
|
||||
|
||||
@@ -182,18 +182,12 @@
|
||||
"app.export-modal.header": {
|
||||
"message": "Exportar modpack"
|
||||
},
|
||||
"app.export-modal.include-file-accessibility-label": {
|
||||
"message": "¿Incluir \"{file}\"?"
|
||||
},
|
||||
"app.export-modal.modpack-name-label": {
|
||||
"message": "Nombre del modpack"
|
||||
},
|
||||
"app.export-modal.modpack-name-placeholder": {
|
||||
"message": "Nombre del modpack"
|
||||
},
|
||||
"app.export-modal.select-files-label": {
|
||||
"message": "Configura que archivos se incluirán en esta instancia"
|
||||
},
|
||||
"app.export-modal.version-number-label": {
|
||||
"message": "Número de versión"
|
||||
},
|
||||
|
||||
@@ -203,18 +203,12 @@
|
||||
"app.export-modal.header": {
|
||||
"message": "Vie modipaketti"
|
||||
},
|
||||
"app.export-modal.include-file-accessibility-label": {
|
||||
"message": "Sisällytä \"{file}\"?"
|
||||
},
|
||||
"app.export-modal.modpack-name-label": {
|
||||
"message": "Modipaketin nimi"
|
||||
},
|
||||
"app.export-modal.modpack-name-placeholder": {
|
||||
"message": "Modipaketin nimi"
|
||||
},
|
||||
"app.export-modal.select-files-label": {
|
||||
"message": "Muuta mitkä tiedostot sisältyvät tähän vientiin"
|
||||
},
|
||||
"app.export-modal.version-number-label": {
|
||||
"message": "Versio numero"
|
||||
},
|
||||
|
||||
@@ -164,18 +164,12 @@
|
||||
"app.export-modal.header": {
|
||||
"message": "Iluwas ang modpack"
|
||||
},
|
||||
"app.export-modal.include-file-accessibility-label": {
|
||||
"message": "Salihin ang \"{file}\"?"
|
||||
},
|
||||
"app.export-modal.modpack-name-label": {
|
||||
"message": "Pangalan ng Modpack"
|
||||
},
|
||||
"app.export-modal.modpack-name-placeholder": {
|
||||
"message": "Pangalan ng modpack"
|
||||
},
|
||||
"app.export-modal.select-files-label": {
|
||||
"message": "Isaayos kung anong mga file ang isasali sa pagluwas"
|
||||
},
|
||||
"app.export-modal.version-number-label": {
|
||||
"message": "Numero ng bersiyon"
|
||||
},
|
||||
|
||||
@@ -203,18 +203,12 @@
|
||||
"app.export-modal.header": {
|
||||
"message": "Exporter le modpack"
|
||||
},
|
||||
"app.export-modal.include-file-accessibility-label": {
|
||||
"message": "Inclure « {file} » ?"
|
||||
},
|
||||
"app.export-modal.modpack-name-label": {
|
||||
"message": "Nom du modpack"
|
||||
},
|
||||
"app.export-modal.modpack-name-placeholder": {
|
||||
"message": "Nom du modpack"
|
||||
},
|
||||
"app.export-modal.select-files-label": {
|
||||
"message": "Configurez quels fichiers sont inclus dans cette exportation"
|
||||
},
|
||||
"app.export-modal.version-number-label": {
|
||||
"message": "Numéro de version"
|
||||
},
|
||||
|
||||
@@ -176,18 +176,12 @@
|
||||
"app.export-modal.header": {
|
||||
"message": "Modcsomag exportálása"
|
||||
},
|
||||
"app.export-modal.include-file-accessibility-label": {
|
||||
"message": "Tartalmaz „{file}” fájlokat?"
|
||||
},
|
||||
"app.export-modal.modpack-name-label": {
|
||||
"message": "A modcsomag neve"
|
||||
},
|
||||
"app.export-modal.modpack-name-placeholder": {
|
||||
"message": "A modcsomag neve"
|
||||
},
|
||||
"app.export-modal.select-files-label": {
|
||||
"message": "Állítsd be, hogy mely fájlok kerüljenek bele az exportba"
|
||||
},
|
||||
"app.export-modal.version-number-label": {
|
||||
"message": "Verziószám"
|
||||
},
|
||||
|
||||
@@ -170,18 +170,12 @@
|
||||
"app.export-modal.header": {
|
||||
"message": "Ekspor paket mod"
|
||||
},
|
||||
"app.export-modal.include-file-accessibility-label": {
|
||||
"message": "Sertakan \"{file}\"?"
|
||||
},
|
||||
"app.export-modal.modpack-name-label": {
|
||||
"message": "Nama Paket Mod"
|
||||
},
|
||||
"app.export-modal.modpack-name-placeholder": {
|
||||
"message": "Nama paket mod"
|
||||
},
|
||||
"app.export-modal.select-files-label": {
|
||||
"message": "Konfigurasikan berkas mana yang disertakan dalam pengeksporan ini"
|
||||
},
|
||||
"app.export-modal.version-number-label": {
|
||||
"message": "Nomor versi"
|
||||
},
|
||||
|
||||
@@ -203,18 +203,12 @@
|
||||
"app.export-modal.header": {
|
||||
"message": "Esporta pacchetto"
|
||||
},
|
||||
"app.export-modal.include-file-accessibility-label": {
|
||||
"message": "Includere \"{file}\" nell'esportazione?"
|
||||
},
|
||||
"app.export-modal.modpack-name-label": {
|
||||
"message": "Nome del pacchetto"
|
||||
},
|
||||
"app.export-modal.modpack-name-placeholder": {
|
||||
"message": "Nome del pacchetto"
|
||||
},
|
||||
"app.export-modal.select-files-label": {
|
||||
"message": "Seleziona i file da includere in questa esportazione"
|
||||
},
|
||||
"app.export-modal.version-number-label": {
|
||||
"message": "Numero di versione"
|
||||
},
|
||||
|
||||
@@ -164,18 +164,12 @@
|
||||
"app.export-modal.header": {
|
||||
"message": "모드팩 내보내기"
|
||||
},
|
||||
"app.export-modal.include-file-accessibility-label": {
|
||||
"message": "\"{file}\"(을)를 포함할까요?"
|
||||
},
|
||||
"app.export-modal.modpack-name-label": {
|
||||
"message": "모드팩 이름"
|
||||
},
|
||||
"app.export-modal.modpack-name-placeholder": {
|
||||
"message": "모드팩 이름"
|
||||
},
|
||||
"app.export-modal.select-files-label": {
|
||||
"message": "내보내기에 어느 파일이 포함될지 구성"
|
||||
},
|
||||
"app.export-modal.version-number-label": {
|
||||
"message": "버전 구분"
|
||||
},
|
||||
|
||||
@@ -155,18 +155,12 @@
|
||||
"app.export-modal.header": {
|
||||
"message": "Eksport pek mod"
|
||||
},
|
||||
"app.export-modal.include-file-accessibility-label": {
|
||||
"message": "Sertakan \"{file}\"?"
|
||||
},
|
||||
"app.export-modal.modpack-name-label": {
|
||||
"message": "Nama Pek Mod"
|
||||
},
|
||||
"app.export-modal.modpack-name-placeholder": {
|
||||
"message": "Nama pek mod"
|
||||
},
|
||||
"app.export-modal.select-files-label": {
|
||||
"message": "Konfigurasikan fail yang disertakan dalam eksport ini"
|
||||
},
|
||||
"app.export-modal.version-number-label": {
|
||||
"message": "Nombor versi"
|
||||
},
|
||||
|
||||
@@ -170,18 +170,12 @@
|
||||
"app.export-modal.header": {
|
||||
"message": "Exporteer modpack"
|
||||
},
|
||||
"app.export-modal.include-file-accessibility-label": {
|
||||
"message": "Betrek \"{file}\"?"
|
||||
},
|
||||
"app.export-modal.modpack-name-label": {
|
||||
"message": "Modpack Naam"
|
||||
},
|
||||
"app.export-modal.modpack-name-placeholder": {
|
||||
"message": "Modpack naam"
|
||||
},
|
||||
"app.export-modal.select-files-label": {
|
||||
"message": "Configureer welke bestanden bij deze export zijn betrokken"
|
||||
},
|
||||
"app.export-modal.version-number-label": {
|
||||
"message": "Versie nummer"
|
||||
},
|
||||
|
||||
@@ -203,18 +203,12 @@
|
||||
"app.export-modal.header": {
|
||||
"message": "Eksportuj paczkę modów"
|
||||
},
|
||||
"app.export-modal.include-file-accessibility-label": {
|
||||
"message": "Dołączyć \"{file}\"?"
|
||||
},
|
||||
"app.export-modal.modpack-name-label": {
|
||||
"message": "Nazwa paczki modów"
|
||||
},
|
||||
"app.export-modal.modpack-name-placeholder": {
|
||||
"message": "Nazwa paczki modów"
|
||||
},
|
||||
"app.export-modal.select-files-label": {
|
||||
"message": "Skonfiguruje jakie pliki zostaną załączone w tym eksporcie"
|
||||
},
|
||||
"app.export-modal.version-number-label": {
|
||||
"message": "Numer wersji"
|
||||
},
|
||||
|
||||
@@ -203,18 +203,12 @@
|
||||
"app.export-modal.header": {
|
||||
"message": "Exportar pacote de mods"
|
||||
},
|
||||
"app.export-modal.include-file-accessibility-label": {
|
||||
"message": "Incluir \"{file}\"?"
|
||||
},
|
||||
"app.export-modal.modpack-name-label": {
|
||||
"message": "Nome do pacote de mods"
|
||||
},
|
||||
"app.export-modal.modpack-name-placeholder": {
|
||||
"message": "Nome do pacote de mods"
|
||||
},
|
||||
"app.export-modal.select-files-label": {
|
||||
"message": "Configure quais arquivos serão incluídos na exportação"
|
||||
},
|
||||
"app.export-modal.version-number-label": {
|
||||
"message": "Número da versão"
|
||||
},
|
||||
|
||||
@@ -203,18 +203,12 @@
|
||||
"app.export-modal.header": {
|
||||
"message": "Экспорт сборки"
|
||||
},
|
||||
"app.export-modal.include-file-accessibility-label": {
|
||||
"message": "Добавить «{file}» в экспорт?"
|
||||
},
|
||||
"app.export-modal.modpack-name-label": {
|
||||
"message": "Название сборки"
|
||||
},
|
||||
"app.export-modal.modpack-name-placeholder": {
|
||||
"message": "Название сборки"
|
||||
},
|
||||
"app.export-modal.select-files-label": {
|
||||
"message": "Выберите файлы для экспорта"
|
||||
},
|
||||
"app.export-modal.version-number-label": {
|
||||
"message": "Номер версии"
|
||||
},
|
||||
|
||||
@@ -203,18 +203,12 @@
|
||||
"app.export-modal.header": {
|
||||
"message": "Exportera modpaket"
|
||||
},
|
||||
"app.export-modal.include-file-accessibility-label": {
|
||||
"message": "Inkludera \"{file}\"?"
|
||||
},
|
||||
"app.export-modal.modpack-name-label": {
|
||||
"message": "Modpaketets namn"
|
||||
},
|
||||
"app.export-modal.modpack-name-placeholder": {
|
||||
"message": "Modpaketets namn"
|
||||
},
|
||||
"app.export-modal.select-files-label": {
|
||||
"message": "Konfigurera vilka filer som inkluderas i denna export"
|
||||
},
|
||||
"app.export-modal.version-number-label": {
|
||||
"message": "Versionsnummer"
|
||||
},
|
||||
|
||||
@@ -170,18 +170,12 @@
|
||||
"app.export-modal.header": {
|
||||
"message": "ส่งออกแพ็กม็อด"
|
||||
},
|
||||
"app.export-modal.include-file-accessibility-label": {
|
||||
"message": "รวมถึง \"{file}\" ใช่หรือไม่"
|
||||
},
|
||||
"app.export-modal.modpack-name-label": {
|
||||
"message": "ชื่อแพ็กม็อด"
|
||||
},
|
||||
"app.export-modal.modpack-name-placeholder": {
|
||||
"message": "ชื่อแพ็กม็อด"
|
||||
},
|
||||
"app.export-modal.select-files-label": {
|
||||
"message": "กำหนดไฟล์ที่ต้องการจะส่งออก"
|
||||
},
|
||||
"app.export-modal.version-number-label": {
|
||||
"message": "หมายเลขเวอร์ชัน"
|
||||
},
|
||||
|
||||
@@ -203,18 +203,12 @@
|
||||
"app.export-modal.header": {
|
||||
"message": "Mod paketini dışa aktar"
|
||||
},
|
||||
"app.export-modal.include-file-accessibility-label": {
|
||||
"message": "\"{file}\" dahil mi?"
|
||||
},
|
||||
"app.export-modal.modpack-name-label": {
|
||||
"message": "Mod Paketi Adı"
|
||||
},
|
||||
"app.export-modal.modpack-name-placeholder": {
|
||||
"message": "Mod paketi adı"
|
||||
},
|
||||
"app.export-modal.select-files-label": {
|
||||
"message": "Bu dışa aktarmaya hangi dosyaların dahil edileceğini yapılandırma"
|
||||
},
|
||||
"app.export-modal.version-number-label": {
|
||||
"message": "Sürüm numarası"
|
||||
},
|
||||
|
||||
@@ -170,18 +170,12 @@
|
||||
"app.export-modal.header": {
|
||||
"message": "Експортувати збірку"
|
||||
},
|
||||
"app.export-modal.include-file-accessibility-label": {
|
||||
"message": "Уключити «{file}»?"
|
||||
},
|
||||
"app.export-modal.modpack-name-label": {
|
||||
"message": "Назва збірки"
|
||||
},
|
||||
"app.export-modal.modpack-name-placeholder": {
|
||||
"message": "Назва збірки"
|
||||
},
|
||||
"app.export-modal.select-files-label": {
|
||||
"message": "Змініть файли, які додані до експорту"
|
||||
},
|
||||
"app.export-modal.version-number-label": {
|
||||
"message": "Номер версії"
|
||||
},
|
||||
|
||||
@@ -170,18 +170,12 @@
|
||||
"app.export-modal.header": {
|
||||
"message": "Xuất modpack"
|
||||
},
|
||||
"app.export-modal.include-file-accessibility-label": {
|
||||
"message": "Bao gồm \"{file}\"?"
|
||||
},
|
||||
"app.export-modal.modpack-name-label": {
|
||||
"message": "Tên modpack"
|
||||
},
|
||||
"app.export-modal.modpack-name-placeholder": {
|
||||
"message": "Tên modpack"
|
||||
},
|
||||
"app.export-modal.select-files-label": {
|
||||
"message": "Cấu hình các tệp nào được bao gồm trong quá trình xuất phiên bản này"
|
||||
},
|
||||
"app.export-modal.version-number-label": {
|
||||
"message": "Phiên bản"
|
||||
},
|
||||
|
||||
@@ -203,18 +203,12 @@
|
||||
"app.export-modal.header": {
|
||||
"message": "导出整合包"
|
||||
},
|
||||
"app.export-modal.include-file-accessibility-label": {
|
||||
"message": "包含 \"{file}\"?"
|
||||
},
|
||||
"app.export-modal.modpack-name-label": {
|
||||
"message": "整合包名称"
|
||||
},
|
||||
"app.export-modal.modpack-name-placeholder": {
|
||||
"message": "整合包名称"
|
||||
},
|
||||
"app.export-modal.select-files-label": {
|
||||
"message": "配置此导出中包含哪些文件"
|
||||
},
|
||||
"app.export-modal.version-number-label": {
|
||||
"message": "版本号"
|
||||
},
|
||||
|
||||
@@ -203,18 +203,12 @@
|
||||
"app.export-modal.header": {
|
||||
"message": "匯出模組包"
|
||||
},
|
||||
"app.export-modal.include-file-accessibility-label": {
|
||||
"message": "要包含「{file}」嗎?"
|
||||
},
|
||||
"app.export-modal.modpack-name-label": {
|
||||
"message": "模組包名稱"
|
||||
},
|
||||
"app.export-modal.modpack-name-placeholder": {
|
||||
"message": "模組包名稱"
|
||||
},
|
||||
"app.export-modal.select-files-label": {
|
||||
"message": "設定要包含在此匯出檔案中的檔案"
|
||||
},
|
||||
"app.export-modal.version-number-label": {
|
||||
"message": "版本號碼"
|
||||
},
|
||||
|
||||
@@ -114,6 +114,10 @@ const messages = defineMessages({
|
||||
id: 'app.skins.section.tiny-takeover',
|
||||
defaultMessage: 'Tiny Takeover',
|
||||
},
|
||||
chaosCubedSection: {
|
||||
id: 'app.skins.section.chaos-cubed',
|
||||
defaultMessage: 'Chaos Cubed',
|
||||
},
|
||||
rateLimitTitle: {
|
||||
id: 'app.skins.rate-limit.title',
|
||||
defaultMessage: 'Slow down!',
|
||||
@@ -447,6 +451,8 @@ function getDefaultSkinSectionTitle(section?: string) {
|
||||
return formatMessage(messages.mountsOfMayhemSection)
|
||||
case 'Tiny Takeover':
|
||||
return formatMessage(messages.tinyTakeoverSection)
|
||||
case 'Chaos Cubed':
|
||||
return formatMessage(messages.chaosCubedSection)
|
||||
case 'Default skins':
|
||||
return formatMessage(messages.defaultSkinsSection)
|
||||
default:
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
<template>
|
||||
<div class="h-full w-full pt-6">
|
||||
<div
|
||||
class="h-full w-full pt-6"
|
||||
:class="isContainedServerRoute ? 'box-border min-h-0 overflow-hidden' : ''"
|
||||
>
|
||||
<ServersManageRootLayout
|
||||
:server-id="serverId"
|
||||
:layout-mode="isContainedServerRoute ? 'contained' : 'page'"
|
||||
:reload-page="() => router.go(0)"
|
||||
:resolve-viewer="resolveViewer"
|
||||
:show-copy-id-action="themeStore.devMode"
|
||||
@@ -63,6 +67,8 @@ const queryClient = useQueryClient()
|
||||
const themeStore = useTheming()
|
||||
const breadcrumbs = useBreadcrumbs()
|
||||
|
||||
const isContainedServerRoute = computed(() => route.name === 'ServerManageOverview')
|
||||
|
||||
const serverId = computed(() => {
|
||||
const rawId = route.params.id
|
||||
return Array.isArray(rawId) ? rawId[0] : (rawId ?? '')
|
||||
|
||||
@@ -3,5 +3,5 @@ import { ServersManageOverviewPage } from '@modrinth/ui'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ServersManageOverviewPage />
|
||||
<ServersManageOverviewPage contained-console />
|
||||
</template>
|
||||
|
||||
@@ -676,7 +676,7 @@ const handleOptionsClick = async (args: { option: string; item: unknown }) => {
|
||||
break
|
||||
case 'copy_path': {
|
||||
if (instance.value) {
|
||||
const fullPath = await get_full_path(instance.value?.path)
|
||||
const fullPath = await get_full_path(instance.value.id)
|
||||
await navigator.clipboard.writeText(fullPath)
|
||||
}
|
||||
break
|
||||
|
||||
@@ -1443,7 +1443,7 @@ onMounted(() => {
|
||||
props.instance &&
|
||||
event.instance_id === props.instance.id &&
|
||||
event.event === 'synced' &&
|
||||
props.instance.install_stage !== 'pack_installing' &&
|
||||
props.instance.install_stage === 'installed' &&
|
||||
!isBulkOperating.value
|
||||
) {
|
||||
await initProjects()
|
||||
|
||||
@@ -618,10 +618,7 @@ const filterOptions = computed(() => {
|
||||
const hasSingleplayer = dedupedWorlds.value.some((x) => x.type === 'singleplayer')
|
||||
const hasServer = dedupedWorlds.value.some((x) => x.type === 'server')
|
||||
|
||||
const hasStatusFilter =
|
||||
selectedFilters.value.includes('online') || selectedFilters.value.includes('offline')
|
||||
|
||||
if (hasSingleplayer && hasServer && !hasStatusFilter) {
|
||||
if (hasSingleplayer && hasServer) {
|
||||
options.push({ id: 'singleplayer', label: formatMessage(commonMessages.singleplayerLabel) })
|
||||
}
|
||||
|
||||
@@ -633,13 +630,11 @@ const filterOptions = computed(() => {
|
||||
options.push({ id: 'vanilla', label: formatMessage(messages.vanillaFilter) })
|
||||
options.push({ id: 'modded', label: formatMessage(messages.moddedFilter) })
|
||||
}
|
||||
if (!selectedFilters.value.includes('singleplayer')) {
|
||||
const hasOnline = servers.some((x) => !!serverData.value[x.address]?.status)
|
||||
const hasOffline = servers.some((x) => !serverData.value[x.address]?.status)
|
||||
if (hasOnline && hasOffline) {
|
||||
options.push({ id: 'online', label: formatMessage(messages.onlineFilter) })
|
||||
options.push({ id: 'offline', label: formatMessage(messages.offlineFilter) })
|
||||
}
|
||||
const hasOnline = servers.some((x) => !!serverData.value[x.address]?.status)
|
||||
const hasOffline = servers.some((x) => !serverData.value[x.address]?.status)
|
||||
if (hasOnline && hasOffline) {
|
||||
options.push({ id: 'online', label: formatMessage(messages.onlineFilter) })
|
||||
options.push({ id: 'offline', label: formatMessage(messages.offlineFilter) })
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,16 +5,15 @@
|
||||
:game-versions="gameVersions"
|
||||
:versions="versions"
|
||||
:project="project"
|
||||
:show-environment-column="themeStore.featureFlags.show_version_environment_column"
|
||||
:version-link="(version) => buildProjectHref(`/project/${project.id}/version/${version.id}`)"
|
||||
>
|
||||
<template #actions="{ version }">
|
||||
<ButtonStyled circular type="transparent">
|
||||
<ButtonStyled circular type="transparent" color="green">
|
||||
<button
|
||||
v-tooltip="`Install`"
|
||||
:class="{
|
||||
'group-hover:!bg-brand group-hover:[&>svg]:!text-brand-inverted':
|
||||
!installed || version.id !== installedVersion,
|
||||
}"
|
||||
v-tooltip="
|
||||
!installed ? 'Install' : version.id !== installedVersion ? 'Swap version' : ''
|
||||
"
|
||||
:disabled="installing || (installed && version.id === installedVersion)"
|
||||
@click.stop="() => install(version.id)"
|
||||
>
|
||||
@@ -26,7 +25,6 @@
|
||||
<ButtonStyled circular type="transparent">
|
||||
<OverflowMenu
|
||||
v-if="false"
|
||||
class="group-hover:!bg-button-bg"
|
||||
:options="[
|
||||
{
|
||||
id: 'install-elsewhere',
|
||||
@@ -52,7 +50,6 @@
|
||||
<a
|
||||
v-else
|
||||
v-tooltip="`Open in browser`"
|
||||
class="group-hover:!bg-button-bg"
|
||||
:href="`https://modrinth.com/${project.project_type}/${project.slug}/version/${version.id}`"
|
||||
target="_blank"
|
||||
>
|
||||
@@ -77,6 +74,9 @@ import { useRoute } from 'vue-router'
|
||||
|
||||
import { SwapIcon } from '@/assets/icons/index.js'
|
||||
import { get_game_versions, get_loaders } from '@/helpers/tags.js'
|
||||
import { useTheming } from '@/store/theme.ts'
|
||||
|
||||
const themeStore = useTheming()
|
||||
|
||||
defineProps({
|
||||
project: {
|
||||
|
||||
@@ -247,6 +247,7 @@ export default new createRouter({
|
||||
name: 'Logs',
|
||||
component: Instance.Logs,
|
||||
meta: {
|
||||
renderMode: 'fixed',
|
||||
useRootContext: true,
|
||||
breadcrumb: [{ name: '?Instance', link: '/instance/{id}/' }, { name: 'Logs' }],
|
||||
},
|
||||
|
||||
@@ -8,6 +8,7 @@ export const DEFAULT_FEATURE_FLAGS = {
|
||||
worlds_tab: false,
|
||||
worlds_in_home: true,
|
||||
server_project_qa: false,
|
||||
show_version_environment_column: false,
|
||||
server_ram_as_bytes_always_on: false,
|
||||
always_show_app_controls: false,
|
||||
skip_non_essential_warnings: false,
|
||||
|
||||
@@ -414,7 +414,7 @@ components:
|
||||
- unknown
|
||||
- signature
|
||||
example: required-resource-pack
|
||||
# https://github.com/modrinth/labrinth/blob/master/src/routes/version_creation.rs#L27-L57
|
||||
# https://github.com/modrinth/code/blob/main/apps/labrinth/src/routes/v2/version_creation.rs#L32-L76
|
||||
CreatableVersion:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/BaseVersion'
|
||||
@@ -446,6 +446,11 @@ components:
|
||||
- client_or_server_prefers_both
|
||||
- unknown
|
||||
description: The environment that this version is for.
|
||||
file_types:
|
||||
type: object
|
||||
additionalProperties:
|
||||
$ref: '#/components/schemas/FileTypeEnum'
|
||||
description: A map of file parts to their associated file type, a file type is used for additional files such as sources jars.
|
||||
required:
|
||||
- file_parts
|
||||
- project_id
|
||||
|
||||
+30
-30
@@ -41,38 +41,38 @@ export const VISIBLE_PROJECT_STATUS_CHANGE_EVENT_STATUS_SET =
|
||||
|
||||
export const LIGHT_LEGEND_PALETTE = [
|
||||
'hsl(152, 100%, 34%)',
|
||||
'hsl(26, 100%, 42%)',
|
||||
'hsl(202, 100%, 35%)',
|
||||
'hsl(327, 45%, 64%)',
|
||||
'hsl(41, 100%, 45%)',
|
||||
'hsl(250, 60%, 33%)',
|
||||
'hsl(170, 43%, 47%)',
|
||||
'hsl(330, 60%, 33%)',
|
||||
'hsl(46, 100%, 36%)',
|
||||
'hsl(167, 100%, 30%)',
|
||||
'hsl(343, 38%, 45%)',
|
||||
'hsl(222, 100%, 28%)',
|
||||
'hsl(270, 62%, 60%)',
|
||||
'hsl(32, 100%, 37%)',
|
||||
'hsl(349, 57%, 51%)',
|
||||
'hsl(191, 43%, 37%)',
|
||||
'hsl(41, 79%, 46%)',
|
||||
'hsl(203, 76%, 64%)',
|
||||
'hsl(0, 93%, 62%)',
|
||||
'hsl(143, 66%, 29%)',
|
||||
'hsl(58, 89%, 25%)',
|
||||
'hsl(311, 64%, 49%)',
|
||||
'hsl(198, 91%, 32%)',
|
||||
'hsl(12, 88%, 27%)',
|
||||
'hsl(236, 61%, 60%)',
|
||||
'hsl(102, 59%, 74%)',
|
||||
'hsl(293, 76%, 79%)',
|
||||
'hsl(67, 99%, 41%)',
|
||||
'hsl(179, 100%, 50%)',
|
||||
'hsl(102, 100%, 61%)',
|
||||
'hsl(0, 100%, 32%)',
|
||||
]
|
||||
|
||||
export const DARK_LEGEND_PALETTE = [
|
||||
'hsl(145, 78%, 48%)',
|
||||
'hsl(41, 100%, 50%)',
|
||||
'hsl(202, 77%, 63%)',
|
||||
'hsl(323, 66%, 72%)',
|
||||
'hsl(56, 85%, 60%)',
|
||||
'hsl(255, 92%, 80%)',
|
||||
'hsl(12, 100%, 67%)',
|
||||
'hsl(176, 58%, 56%)',
|
||||
'hsl(60, 100%, 41%)',
|
||||
'hsl(165, 80%, 38%)',
|
||||
'hsl(341, 36%, 56%)',
|
||||
'hsl(226, 60%, 49%)',
|
||||
'hsl(252, 53%, 62%)',
|
||||
'hsl(75, 59%, 50%)',
|
||||
'hsl(195, 56%, 42%)',
|
||||
'hsl(30, 59%, 56%)',
|
||||
'hsl(41, 79%, 46%)',
|
||||
'hsl(203, 76%, 64%)',
|
||||
'hsl(0, 93%, 62%)',
|
||||
'hsl(143, 66%, 29%)',
|
||||
'hsl(58, 94%, 45%)',
|
||||
'hsl(311, 64%, 49%)',
|
||||
'hsl(198, 91%, 32%)',
|
||||
'hsl(12, 88%, 27%)',
|
||||
'hsl(236, 61%, 60%)',
|
||||
'hsl(102, 59%, 74%)',
|
||||
'hsl(293, 76%, 79%)',
|
||||
'hsl(61, 92%, 33%)',
|
||||
'hsl(179, 100%, 50%)',
|
||||
'hsl(102, 100%, 61%)',
|
||||
'hsl(0, 100%, 32%)',
|
||||
]
|
||||
|
||||
+44
-13
@@ -288,6 +288,7 @@ type PaletteRankEntry = {
|
||||
key: string
|
||||
label: string
|
||||
total: number
|
||||
excludedFromRank?: boolean
|
||||
}
|
||||
|
||||
function formatDatasetTooltip(projectName: string | undefined): string | undefined {
|
||||
@@ -330,9 +331,12 @@ function buildPaletteColorsByDownloadRank(
|
||||
const colorsByKey = new Map<string, string>()
|
||||
if (palette.length === 0) return colorsByKey
|
||||
|
||||
const sortedEntries = [...entries].sort(
|
||||
(a, b) => b.total - a.total || a.label.localeCompare(b.label) || a.key.localeCompare(b.key),
|
||||
)
|
||||
const compareEntries = (a: PaletteRankEntry, b: PaletteRankEntry) =>
|
||||
b.total - a.total || a.label.localeCompare(b.label) || a.key.localeCompare(b.key)
|
||||
const rankedEntries = entries.filter((entry) => !entry.excludedFromRank).sort(compareEntries)
|
||||
const excludedEntries = entries.filter((entry) => entry.excludedFromRank).sort(compareEntries)
|
||||
const sortedEntries = [...rankedEntries, ...excludedEntries]
|
||||
|
||||
sortedEntries.forEach((entry, index) => {
|
||||
colorsByKey.set(entry.key, getPaletteColorForIndex(index, palette))
|
||||
})
|
||||
@@ -340,6 +344,27 @@ function buildPaletteColorsByDownloadRank(
|
||||
return colorsByKey
|
||||
}
|
||||
|
||||
function isExcludedFromPaletteRank(breakdownValues: readonly string[]): boolean {
|
||||
return breakdownValues.some(
|
||||
(value) =>
|
||||
isUnknownAnalyticsBreakdownValue(value) || isNoDependentAnalyticsBreakdownValue(value),
|
||||
)
|
||||
}
|
||||
|
||||
function buildPaletteRankEntry(
|
||||
key: string,
|
||||
breakdownValues: readonly string[],
|
||||
total: number,
|
||||
formatLabel: (breakdownValues: readonly string[]) => string,
|
||||
): PaletteRankEntry {
|
||||
return {
|
||||
key,
|
||||
label: formatLabel(breakdownValues),
|
||||
total,
|
||||
excludedFromRank: isExcludedFromPaletteRank(breakdownValues),
|
||||
}
|
||||
}
|
||||
|
||||
export function getMetricValue(
|
||||
point: Labrinth.Analytics.v3.ProjectAnalytics,
|
||||
activeStat: AnalyticsDashboardStat,
|
||||
@@ -499,11 +524,14 @@ export function buildChartDatasets(
|
||||
})
|
||||
|
||||
const colorsByBreakdown = buildPaletteColorsByDownloadRank(
|
||||
Array.from(dataByBreakdown.keys()).map((breakdownKey) => ({
|
||||
key: breakdownKey,
|
||||
label: formatChartBreakdownLabels(breakdownValuesByKey.get(breakdownKey) ?? []),
|
||||
total: downloadTotalsByBreakdown.get(breakdownKey) ?? 0,
|
||||
})),
|
||||
Array.from(dataByBreakdown.keys()).map((breakdownKey) =>
|
||||
buildPaletteRankEntry(
|
||||
breakdownKey,
|
||||
breakdownValuesByKey.get(breakdownKey) ?? [],
|
||||
downloadTotalsByBreakdown.get(breakdownKey) ?? 0,
|
||||
formatChartBreakdownLabels,
|
||||
),
|
||||
),
|
||||
palette,
|
||||
)
|
||||
|
||||
@@ -675,11 +703,14 @@ export function buildChartDatasets(
|
||||
})
|
||||
|
||||
const colorsByBreakdown = buildPaletteColorsByDownloadRank(
|
||||
Array.from(dataByProjectBreakdown.keys()).map((breakdownKey) => ({
|
||||
key: breakdownKey,
|
||||
label: formatChartBreakdownLabels(breakdownValuesByKey.get(breakdownKey) ?? []),
|
||||
total: downloadTotalsByProjectBreakdown.get(breakdownKey) ?? 0,
|
||||
})),
|
||||
Array.from(dataByProjectBreakdown.keys()).map((breakdownKey) =>
|
||||
buildPaletteRankEntry(
|
||||
breakdownKey,
|
||||
breakdownValuesByKey.get(breakdownKey) ?? [],
|
||||
downloadTotalsByProjectBreakdown.get(breakdownKey) ?? 0,
|
||||
formatChartBreakdownLabels,
|
||||
),
|
||||
),
|
||||
palette,
|
||||
)
|
||||
|
||||
|
||||
@@ -193,6 +193,7 @@ async function setStatus(status: Labrinth.Projects.v2.ProjectStatus) {
|
||||
isLoadingStatusAction.value = true
|
||||
try {
|
||||
await client.labrinth.projects_v2.edit(props.item.project.id, { status })
|
||||
emit('refetch')
|
||||
|
||||
projectStatus.value = status
|
||||
} catch (err) {
|
||||
@@ -1206,7 +1207,7 @@ async function handleSubmitReview(verdict: 'safe' | 'unsafe') {
|
||||
</OverflowMenu>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled v-if="featureFlags.developerMode" type="outlined">
|
||||
<button @click="emit('showMaliciousSummary', unsafeFiles)">Debug Summary</button>
|
||||
<button @click="emit('showMaliciousSummary', unsafeFiles)">Debug</button>
|
||||
</ButtonStyled>
|
||||
</template>
|
||||
</ThreadView>
|
||||
|
||||
@@ -21,6 +21,7 @@ export const DEFAULT_FEATURE_FLAGS = validateValues({
|
||||
developerMode: false,
|
||||
demoMode: false,
|
||||
showVersionFilesInTable: false,
|
||||
showVersionEnvironmentColumn: false,
|
||||
showAdsWithPlus: false,
|
||||
alwaysShowChecklistAsPopup: true,
|
||||
testTaxForm: false,
|
||||
|
||||
@@ -914,18 +914,6 @@
|
||||
"dashboard.projects.project.review-environment-metadata": {
|
||||
"message": "يرجى مراجعة بيانات تعريف البيئة"
|
||||
},
|
||||
"dashboard.projects.sort.ascending": {
|
||||
"message": "صعوداً"
|
||||
},
|
||||
"dashboard.projects.sort.descending": {
|
||||
"message": "نزولا"
|
||||
},
|
||||
"dashboard.projects.sort.option.name": {
|
||||
"message": "الإسم"
|
||||
},
|
||||
"dashboard.projects.sort.option.status": {
|
||||
"message": "الحالة"
|
||||
},
|
||||
"dashboard.revenue.available-now": {
|
||||
"message": "متاح الآن"
|
||||
},
|
||||
|
||||
@@ -1346,9 +1346,6 @@
|
||||
"dashboard.overview.notifications.loading": {
|
||||
"message": "Načítání oznámení..."
|
||||
},
|
||||
"dashboard.projects.bulk-edit-hint": {
|
||||
"message": "Více projektů můžete upravit najednou jejich výběrem níže."
|
||||
},
|
||||
"dashboard.projects.bulk-edit.server-disabled": {
|
||||
"message": "Serverové projekty nepodporují hromadné úpravy"
|
||||
},
|
||||
@@ -1400,24 +1397,6 @@
|
||||
"dashboard.projects.project.moderator-message-aria": {
|
||||
"message": "Projekt má zprávu od moderátorů. Další informace zobrazíte v projektu."
|
||||
},
|
||||
"dashboard.projects.sort.ascending": {
|
||||
"message": "Vzestupně"
|
||||
},
|
||||
"dashboard.projects.sort.descending": {
|
||||
"message": "Sestupně"
|
||||
},
|
||||
"dashboard.projects.sort.option.name": {
|
||||
"message": "Jméno"
|
||||
},
|
||||
"dashboard.projects.sort.option.status": {
|
||||
"message": "Stav"
|
||||
},
|
||||
"dashboard.projects.sort.option.type": {
|
||||
"message": "Typ"
|
||||
},
|
||||
"dashboard.projects.table.icon": {
|
||||
"message": "Ikona"
|
||||
},
|
||||
"dashboard.projects.table.id": {
|
||||
"message": "ID"
|
||||
},
|
||||
|
||||
@@ -1634,9 +1634,6 @@
|
||||
"dashboard.overview.notifications.loading": {
|
||||
"message": "Benachrichtigungen werden geladen..."
|
||||
},
|
||||
"dashboard.projects.bulk-edit-hint": {
|
||||
"message": "Du kannst mehrere Projekte gleichzeitig bearbeiten, indem du sie unten auswählst."
|
||||
},
|
||||
"dashboard.projects.bulk-edit.server-disabled": {
|
||||
"message": "Serverprojekte understützen Massenbearbeitung nicht"
|
||||
},
|
||||
@@ -1712,24 +1709,6 @@
|
||||
"dashboard.projects.project.review-environment-metadata": {
|
||||
"message": "Bitte überprüfe die Umgebungsmetadaten"
|
||||
},
|
||||
"dashboard.projects.sort.ascending": {
|
||||
"message": "Aufsteigend"
|
||||
},
|
||||
"dashboard.projects.sort.descending": {
|
||||
"message": "Absteigend"
|
||||
},
|
||||
"dashboard.projects.sort.option.name": {
|
||||
"message": "Name"
|
||||
},
|
||||
"dashboard.projects.sort.option.status": {
|
||||
"message": "Status"
|
||||
},
|
||||
"dashboard.projects.sort.option.type": {
|
||||
"message": "Art"
|
||||
},
|
||||
"dashboard.projects.table.icon": {
|
||||
"message": "Icon"
|
||||
},
|
||||
"dashboard.projects.table.id": {
|
||||
"message": "ID"
|
||||
},
|
||||
|
||||
@@ -1634,9 +1634,6 @@
|
||||
"dashboard.overview.notifications.loading": {
|
||||
"message": "Benachrichtigungen werden geladen..."
|
||||
},
|
||||
"dashboard.projects.bulk-edit-hint": {
|
||||
"message": "Du kannst mehrere Projekte gleichzeitig bearbeiten, indem du sie unten auswählst."
|
||||
},
|
||||
"dashboard.projects.bulk-edit.server-disabled": {
|
||||
"message": "Serverprojekte unterstützen Massenbearbeitung nicht"
|
||||
},
|
||||
@@ -1712,24 +1709,6 @@
|
||||
"dashboard.projects.project.review-environment-metadata": {
|
||||
"message": "Bitte überprüfe die Umgebungsmetadaten"
|
||||
},
|
||||
"dashboard.projects.sort.ascending": {
|
||||
"message": "Aufsteigend"
|
||||
},
|
||||
"dashboard.projects.sort.descending": {
|
||||
"message": "Absteigend"
|
||||
},
|
||||
"dashboard.projects.sort.option.name": {
|
||||
"message": "Name"
|
||||
},
|
||||
"dashboard.projects.sort.option.status": {
|
||||
"message": "Status"
|
||||
},
|
||||
"dashboard.projects.sort.option.type": {
|
||||
"message": "Typ"
|
||||
},
|
||||
"dashboard.projects.table.icon": {
|
||||
"message": "Icon"
|
||||
},
|
||||
"dashboard.projects.table.id": {
|
||||
"message": "ID"
|
||||
},
|
||||
|
||||
@@ -1634,9 +1634,6 @@
|
||||
"dashboard.overview.notifications.loading": {
|
||||
"message": "Loading notifications..."
|
||||
},
|
||||
"dashboard.projects.bulk-edit-hint": {
|
||||
"message": "You can edit multiple projects at once by selecting them below."
|
||||
},
|
||||
"dashboard.projects.bulk-edit.server-disabled": {
|
||||
"message": "Server projects do not support bulk editing"
|
||||
},
|
||||
@@ -1712,23 +1709,11 @@
|
||||
"dashboard.projects.project.review-environment-metadata": {
|
||||
"message": "Please review environment metadata"
|
||||
},
|
||||
"dashboard.projects.sort.ascending": {
|
||||
"message": "Ascending"
|
||||
"dashboard.projects.selection-bar.aria-label": {
|
||||
"message": "Selected projects actions"
|
||||
},
|
||||
"dashboard.projects.sort.descending": {
|
||||
"message": "Descending"
|
||||
},
|
||||
"dashboard.projects.sort.option.name": {
|
||||
"message": "Name"
|
||||
},
|
||||
"dashboard.projects.sort.option.status": {
|
||||
"message": "Status"
|
||||
},
|
||||
"dashboard.projects.sort.option.type": {
|
||||
"message": "Type"
|
||||
},
|
||||
"dashboard.projects.table.icon": {
|
||||
"message": "Icon"
|
||||
"dashboard.projects.selection-bar.selected-count": {
|
||||
"message": "{count, plural, one {# project selected} other {# projects selected}}"
|
||||
},
|
||||
"dashboard.projects.table.id": {
|
||||
"message": "ID"
|
||||
@@ -1736,6 +1721,9 @@
|
||||
"dashboard.projects.table.name": {
|
||||
"message": "Name"
|
||||
},
|
||||
"dashboard.projects.table.select-all-bulk-editable": {
|
||||
"message": "Select all projects that support bulk editing"
|
||||
},
|
||||
"dashboard.projects.table.status": {
|
||||
"message": "Status"
|
||||
},
|
||||
|
||||
@@ -1634,9 +1634,6 @@
|
||||
"dashboard.overview.notifications.loading": {
|
||||
"message": "Cargando notificaciones..."
|
||||
},
|
||||
"dashboard.projects.bulk-edit-hint": {
|
||||
"message": "Puedes editar varios proyectos a la vez seleccionándolos a continuación."
|
||||
},
|
||||
"dashboard.projects.bulk-edit.server-disabled": {
|
||||
"message": "Los proyectos no son compatible con la edición masiva de servidores"
|
||||
},
|
||||
@@ -1712,24 +1709,6 @@
|
||||
"dashboard.projects.project.review-environment-metadata": {
|
||||
"message": "Por favor revisa los metadatos de entorno"
|
||||
},
|
||||
"dashboard.projects.sort.ascending": {
|
||||
"message": "Ascendente"
|
||||
},
|
||||
"dashboard.projects.sort.descending": {
|
||||
"message": "Descendente"
|
||||
},
|
||||
"dashboard.projects.sort.option.name": {
|
||||
"message": "Nombre"
|
||||
},
|
||||
"dashboard.projects.sort.option.status": {
|
||||
"message": "Estado"
|
||||
},
|
||||
"dashboard.projects.sort.option.type": {
|
||||
"message": "Tipo"
|
||||
},
|
||||
"dashboard.projects.table.icon": {
|
||||
"message": "Icono"
|
||||
},
|
||||
"dashboard.projects.table.id": {
|
||||
"message": "ID"
|
||||
},
|
||||
|
||||
@@ -1517,9 +1517,6 @@
|
||||
"dashboard.overview.notifications.loading": {
|
||||
"message": "Cargando notificaciones..."
|
||||
},
|
||||
"dashboard.projects.bulk-edit-hint": {
|
||||
"message": "Puedes editar múltiples proyectos a la vez seleccionándolos abajo."
|
||||
},
|
||||
"dashboard.projects.bulk-edit.server-disabled": {
|
||||
"message": "Proyectos de servidor no soportan edición masiva"
|
||||
},
|
||||
@@ -1592,24 +1589,6 @@
|
||||
"dashboard.projects.project.review-environment-metadata": {
|
||||
"message": "Por favor revise el entorno del metadata"
|
||||
},
|
||||
"dashboard.projects.sort.ascending": {
|
||||
"message": "Ascendiendo"
|
||||
},
|
||||
"dashboard.projects.sort.descending": {
|
||||
"message": "Descendiendo"
|
||||
},
|
||||
"dashboard.projects.sort.option.name": {
|
||||
"message": "Nombre"
|
||||
},
|
||||
"dashboard.projects.sort.option.status": {
|
||||
"message": "Estado"
|
||||
},
|
||||
"dashboard.projects.sort.option.type": {
|
||||
"message": "Tipo"
|
||||
},
|
||||
"dashboard.projects.table.icon": {
|
||||
"message": "Icono"
|
||||
},
|
||||
"dashboard.projects.table.id": {
|
||||
"message": "ID"
|
||||
},
|
||||
|
||||
@@ -611,24 +611,6 @@
|
||||
"dashboard.projects.project.icon-alt": {
|
||||
"message": "Kuvake projektille {title}"
|
||||
},
|
||||
"dashboard.projects.sort.ascending": {
|
||||
"message": "Nouseva"
|
||||
},
|
||||
"dashboard.projects.sort.descending": {
|
||||
"message": "Laskeva"
|
||||
},
|
||||
"dashboard.projects.sort.option.name": {
|
||||
"message": "Nimi"
|
||||
},
|
||||
"dashboard.projects.sort.option.status": {
|
||||
"message": "Tila"
|
||||
},
|
||||
"dashboard.projects.sort.option.type": {
|
||||
"message": "Tyyppi"
|
||||
},
|
||||
"dashboard.projects.table.icon": {
|
||||
"message": "Kuvake"
|
||||
},
|
||||
"dashboard.projects.table.id": {
|
||||
"message": "ID"
|
||||
},
|
||||
|
||||
@@ -998,18 +998,6 @@
|
||||
"dashboard.projects.project.review-environment-metadata": {
|
||||
"message": "Pakisuri sa metadata sa environment"
|
||||
},
|
||||
"dashboard.projects.sort.option.name": {
|
||||
"message": "Pangalan"
|
||||
},
|
||||
"dashboard.projects.sort.option.status": {
|
||||
"message": "Estado"
|
||||
},
|
||||
"dashboard.projects.sort.option.type": {
|
||||
"message": "Uri"
|
||||
},
|
||||
"dashboard.projects.table.icon": {
|
||||
"message": "Ikono"
|
||||
},
|
||||
"dashboard.projects.table.id": {
|
||||
"message": "ID"
|
||||
},
|
||||
|
||||
@@ -1634,9 +1634,6 @@
|
||||
"dashboard.overview.notifications.loading": {
|
||||
"message": "Chargement des notifications..."
|
||||
},
|
||||
"dashboard.projects.bulk-edit-hint": {
|
||||
"message": "Vous pouvez modifier plusieurs projets en même temps en les sélectionnant ci-dessous."
|
||||
},
|
||||
"dashboard.projects.bulk-edit.server-disabled": {
|
||||
"message": "Les projets du serveur ne prennent pas en charge l'édition en masse"
|
||||
},
|
||||
@@ -1712,24 +1709,6 @@
|
||||
"dashboard.projects.project.review-environment-metadata": {
|
||||
"message": "Merci de revoir les métadonnées environnementales"
|
||||
},
|
||||
"dashboard.projects.sort.ascending": {
|
||||
"message": "Ascendant"
|
||||
},
|
||||
"dashboard.projects.sort.descending": {
|
||||
"message": "Descendant"
|
||||
},
|
||||
"dashboard.projects.sort.option.name": {
|
||||
"message": "Nom"
|
||||
},
|
||||
"dashboard.projects.sort.option.status": {
|
||||
"message": "Statut"
|
||||
},
|
||||
"dashboard.projects.sort.option.type": {
|
||||
"message": "Type"
|
||||
},
|
||||
"dashboard.projects.table.icon": {
|
||||
"message": "Icône"
|
||||
},
|
||||
"dashboard.projects.table.id": {
|
||||
"message": "ID"
|
||||
},
|
||||
|
||||
@@ -1382,9 +1382,6 @@
|
||||
"dashboard.overview.notifications.loading": {
|
||||
"message": "Értesítések betöltése..."
|
||||
},
|
||||
"dashboard.projects.bulk-edit-hint": {
|
||||
"message": "Több projektet is szerkeszthetsz egyszerre, ha kiválasztod őket alább."
|
||||
},
|
||||
"dashboard.projects.empty": {
|
||||
"message": "Még nincsenek projektjeid. Kattints a fenti zöld gombra a kezdéshez."
|
||||
},
|
||||
@@ -1445,24 +1442,6 @@
|
||||
"dashboard.projects.project.review-environment-metadata": {
|
||||
"message": "Kérlek ellenőrizd a környezeti metaadatokat"
|
||||
},
|
||||
"dashboard.projects.sort.ascending": {
|
||||
"message": "Növekvő"
|
||||
},
|
||||
"dashboard.projects.sort.descending": {
|
||||
"message": "Csökkenő"
|
||||
},
|
||||
"dashboard.projects.sort.option.name": {
|
||||
"message": "Név"
|
||||
},
|
||||
"dashboard.projects.sort.option.status": {
|
||||
"message": "Állapot"
|
||||
},
|
||||
"dashboard.projects.sort.option.type": {
|
||||
"message": "Típus"
|
||||
},
|
||||
"dashboard.projects.table.icon": {
|
||||
"message": "Ikon"
|
||||
},
|
||||
"dashboard.projects.table.id": {
|
||||
"message": "Azonosító"
|
||||
},
|
||||
|
||||
@@ -1628,9 +1628,6 @@
|
||||
"dashboard.overview.notifications.loading": {
|
||||
"message": "Caricando le notifiche..."
|
||||
},
|
||||
"dashboard.projects.bulk-edit-hint": {
|
||||
"message": "Puoi modificare più progetti contemporaneamente selezionandoli qui sotto."
|
||||
},
|
||||
"dashboard.projects.bulk-edit.server-disabled": {
|
||||
"message": "Non puoi modificare più server contemporaneamente"
|
||||
},
|
||||
@@ -1703,24 +1700,6 @@
|
||||
"dashboard.projects.project.review-environment-metadata": {
|
||||
"message": "Si prega di rivedere i metadati d'ambiente"
|
||||
},
|
||||
"dashboard.projects.sort.ascending": {
|
||||
"message": "Crescente"
|
||||
},
|
||||
"dashboard.projects.sort.descending": {
|
||||
"message": "Decrescente"
|
||||
},
|
||||
"dashboard.projects.sort.option.name": {
|
||||
"message": "Nome"
|
||||
},
|
||||
"dashboard.projects.sort.option.status": {
|
||||
"message": "Stato"
|
||||
},
|
||||
"dashboard.projects.sort.option.type": {
|
||||
"message": "Tipo"
|
||||
},
|
||||
"dashboard.projects.table.icon": {
|
||||
"message": "Icona"
|
||||
},
|
||||
"dashboard.projects.table.id": {
|
||||
"message": "ID"
|
||||
},
|
||||
|
||||
@@ -1211,9 +1211,6 @@
|
||||
"dashboard.overview.notifications.loading": {
|
||||
"message": "通知を読み込み中…"
|
||||
},
|
||||
"dashboard.projects.bulk-edit-hint": {
|
||||
"message": "以下で選択することで、複数のプロジェクトを一括で編集できます。"
|
||||
},
|
||||
"dashboard.projects.bulk-edit.server-disabled": {
|
||||
"message": "サーバープロジェクトでは一括編集を利用できません"
|
||||
},
|
||||
@@ -1286,24 +1283,6 @@
|
||||
"dashboard.projects.project.review-environment-metadata": {
|
||||
"message": "環境メタデータを確認してください"
|
||||
},
|
||||
"dashboard.projects.sort.ascending": {
|
||||
"message": "昇順"
|
||||
},
|
||||
"dashboard.projects.sort.descending": {
|
||||
"message": "降順"
|
||||
},
|
||||
"dashboard.projects.sort.option.name": {
|
||||
"message": "名前"
|
||||
},
|
||||
"dashboard.projects.sort.option.status": {
|
||||
"message": "ステータス"
|
||||
},
|
||||
"dashboard.projects.sort.option.type": {
|
||||
"message": "種類"
|
||||
},
|
||||
"dashboard.projects.table.icon": {
|
||||
"message": "アイコン"
|
||||
},
|
||||
"dashboard.projects.table.id": {
|
||||
"message": "ID"
|
||||
},
|
||||
|
||||
@@ -998,9 +998,6 @@
|
||||
"dashboard.overview.notifications.loading": {
|
||||
"message": "알림 불러오는 중..."
|
||||
},
|
||||
"dashboard.projects.bulk-edit-hint": {
|
||||
"message": "아래에서 여러 프로젝트를 선택하면 한 번에 편집할 수 있습니다."
|
||||
},
|
||||
"dashboard.projects.bulk-edit.server-disabled": {
|
||||
"message": "서버 프로젝트는 일괄 편집을 지원하지 않습니다"
|
||||
},
|
||||
@@ -1070,24 +1067,6 @@
|
||||
"dashboard.projects.project.review-environment-metadata": {
|
||||
"message": "환경 메타데이터를 검토해 주세요"
|
||||
},
|
||||
"dashboard.projects.sort.ascending": {
|
||||
"message": "오름차순"
|
||||
},
|
||||
"dashboard.projects.sort.descending": {
|
||||
"message": "내림차순"
|
||||
},
|
||||
"dashboard.projects.sort.option.name": {
|
||||
"message": "이름"
|
||||
},
|
||||
"dashboard.projects.sort.option.status": {
|
||||
"message": "상태"
|
||||
},
|
||||
"dashboard.projects.sort.option.type": {
|
||||
"message": "유형"
|
||||
},
|
||||
"dashboard.projects.table.icon": {
|
||||
"message": "아이콘"
|
||||
},
|
||||
"dashboard.projects.table.id": {
|
||||
"message": "ID"
|
||||
},
|
||||
|
||||
@@ -1367,9 +1367,6 @@
|
||||
"dashboard.overview.notifications.loading": {
|
||||
"message": "Sedang memuat pemberitahuan..."
|
||||
},
|
||||
"dashboard.projects.bulk-edit-hint": {
|
||||
"message": "Anda boleh menyunting berbilang projek sekaligus dengan memilihnya di bawah."
|
||||
},
|
||||
"dashboard.projects.bulk-edit.server-disabled": {
|
||||
"message": "Projek pelayan tidak menyokong penyuntingan pukal"
|
||||
},
|
||||
@@ -1439,24 +1436,6 @@
|
||||
"dashboard.projects.project.review-environment-metadata": {
|
||||
"message": "Sila semak metadata persekitaran"
|
||||
},
|
||||
"dashboard.projects.sort.ascending": {
|
||||
"message": "Menaik"
|
||||
},
|
||||
"dashboard.projects.sort.descending": {
|
||||
"message": "Menurun"
|
||||
},
|
||||
"dashboard.projects.sort.option.name": {
|
||||
"message": "Nama"
|
||||
},
|
||||
"dashboard.projects.sort.option.status": {
|
||||
"message": "Status"
|
||||
},
|
||||
"dashboard.projects.sort.option.type": {
|
||||
"message": "Jenis"
|
||||
},
|
||||
"dashboard.projects.table.icon": {
|
||||
"message": "Ikon"
|
||||
},
|
||||
"dashboard.projects.table.id": {
|
||||
"message": "ID"
|
||||
},
|
||||
|
||||
@@ -1358,9 +1358,6 @@
|
||||
"dashboard.overview.notifications.loading": {
|
||||
"message": "Notificaties aan het laden..."
|
||||
},
|
||||
"dashboard.projects.bulk-edit-hint": {
|
||||
"message": "U kunt meerdere projecten tegelijkertijd bijwerken door ze hieronder te selecteren."
|
||||
},
|
||||
"dashboard.projects.bulk-edit.server-disabled": {
|
||||
"message": "Massa wijzigingen worden niet ondersteund door server projecten"
|
||||
},
|
||||
@@ -1412,12 +1409,6 @@
|
||||
"dashboard.projects.project.review-environment-metadata": {
|
||||
"message": "Controller project metadata alstublieft"
|
||||
},
|
||||
"dashboard.projects.sort.option.name": {
|
||||
"message": "Naam"
|
||||
},
|
||||
"dashboard.projects.sort.option.status": {
|
||||
"message": "Status"
|
||||
},
|
||||
"dashboard.projects.table.name": {
|
||||
"message": "Naam"
|
||||
},
|
||||
|
||||
@@ -851,9 +851,6 @@
|
||||
"dashboard.organizations.title": {
|
||||
"message": "Organiseringer"
|
||||
},
|
||||
"dashboard.projects.bulk-edit-hint": {
|
||||
"message": "Du kan redigere flere prosjekt samtidig ved å velge dem nedafor."
|
||||
},
|
||||
"dashboard.projects.bulk-edit.server-disabled": {
|
||||
"message": "Serverprosjekt støtter ikke massredigering"
|
||||
},
|
||||
|
||||
@@ -1634,9 +1634,6 @@
|
||||
"dashboard.overview.notifications.loading": {
|
||||
"message": "Ładowanie powiadomień..."
|
||||
},
|
||||
"dashboard.projects.bulk-edit-hint": {
|
||||
"message": "Możesz edytować kilka projektów naraz, zaznaczając je poniżej."
|
||||
},
|
||||
"dashboard.projects.bulk-edit.server-disabled": {
|
||||
"message": "Projekty serwerów nie wspierają grupowej edycji"
|
||||
},
|
||||
@@ -1709,24 +1706,6 @@
|
||||
"dashboard.projects.project.review-environment-metadata": {
|
||||
"message": "Sprawdź metadane środowiska"
|
||||
},
|
||||
"dashboard.projects.sort.ascending": {
|
||||
"message": "Rosnąco"
|
||||
},
|
||||
"dashboard.projects.sort.descending": {
|
||||
"message": "Malejąco"
|
||||
},
|
||||
"dashboard.projects.sort.option.name": {
|
||||
"message": "Nazwa"
|
||||
},
|
||||
"dashboard.projects.sort.option.status": {
|
||||
"message": "Status"
|
||||
},
|
||||
"dashboard.projects.sort.option.type": {
|
||||
"message": "Rodzaj"
|
||||
},
|
||||
"dashboard.projects.table.icon": {
|
||||
"message": "Ikona"
|
||||
},
|
||||
"dashboard.projects.table.id": {
|
||||
"message": "ID"
|
||||
},
|
||||
|
||||
@@ -1631,9 +1631,6 @@
|
||||
"dashboard.overview.notifications.loading": {
|
||||
"message": "Carregando notificações..."
|
||||
},
|
||||
"dashboard.projects.bulk-edit-hint": {
|
||||
"message": "Você pode editar vários projetos em simultâneo, selecionando-os abaixo."
|
||||
},
|
||||
"dashboard.projects.bulk-edit.server-disabled": {
|
||||
"message": "Os projetos de servidor não suportam edição em massa"
|
||||
},
|
||||
@@ -1709,24 +1706,6 @@
|
||||
"dashboard.projects.project.review-environment-metadata": {
|
||||
"message": "Por favor, revise os metadados do ambiente"
|
||||
},
|
||||
"dashboard.projects.sort.ascending": {
|
||||
"message": "Crescente"
|
||||
},
|
||||
"dashboard.projects.sort.descending": {
|
||||
"message": "Decrescente"
|
||||
},
|
||||
"dashboard.projects.sort.option.name": {
|
||||
"message": "Nome"
|
||||
},
|
||||
"dashboard.projects.sort.option.status": {
|
||||
"message": "Status"
|
||||
},
|
||||
"dashboard.projects.sort.option.type": {
|
||||
"message": "Tipo"
|
||||
},
|
||||
"dashboard.projects.table.icon": {
|
||||
"message": "Ícone"
|
||||
},
|
||||
"dashboard.projects.table.id": {
|
||||
"message": "ID"
|
||||
},
|
||||
|
||||
@@ -821,18 +821,6 @@
|
||||
"dashboard.projects.links.wiki-page.label": {
|
||||
"message": "Página da Wiki"
|
||||
},
|
||||
"dashboard.projects.sort.option.name": {
|
||||
"message": "Nome"
|
||||
},
|
||||
"dashboard.projects.sort.option.status": {
|
||||
"message": "Status"
|
||||
},
|
||||
"dashboard.projects.sort.option.type": {
|
||||
"message": "Tipo"
|
||||
},
|
||||
"dashboard.projects.table.icon": {
|
||||
"message": "Ícone"
|
||||
},
|
||||
"dashboard.projects.table.id": {
|
||||
"message": "ID"
|
||||
},
|
||||
|
||||
@@ -1628,9 +1628,6 @@
|
||||
"dashboard.overview.notifications.loading": {
|
||||
"message": "Загрузка уведомлений..."
|
||||
},
|
||||
"dashboard.projects.bulk-edit-hint": {
|
||||
"message": "Можно редактировать сразу несколько проектов, выбрав их ниже."
|
||||
},
|
||||
"dashboard.projects.bulk-edit.server-disabled": {
|
||||
"message": "Серверные проекты не поддерживают массовое редактирование"
|
||||
},
|
||||
@@ -1706,24 +1703,6 @@
|
||||
"dashboard.projects.project.review-environment-metadata": {
|
||||
"message": "Проверьте метаданные среды"
|
||||
},
|
||||
"dashboard.projects.sort.ascending": {
|
||||
"message": "По возрастанию"
|
||||
},
|
||||
"dashboard.projects.sort.descending": {
|
||||
"message": "По убыванию"
|
||||
},
|
||||
"dashboard.projects.sort.option.name": {
|
||||
"message": "Название"
|
||||
},
|
||||
"dashboard.projects.sort.option.status": {
|
||||
"message": "Статус"
|
||||
},
|
||||
"dashboard.projects.sort.option.type": {
|
||||
"message": "Тип"
|
||||
},
|
||||
"dashboard.projects.table.icon": {
|
||||
"message": "Иконка"
|
||||
},
|
||||
"dashboard.projects.table.id": {
|
||||
"message": "ID"
|
||||
},
|
||||
|
||||
@@ -1514,24 +1514,6 @@
|
||||
"dashboard.projects.project.icon-alt": {
|
||||
"message": "Ikon för {title}"
|
||||
},
|
||||
"dashboard.projects.sort.ascending": {
|
||||
"message": "Stigande"
|
||||
},
|
||||
"dashboard.projects.sort.descending": {
|
||||
"message": "Fallande"
|
||||
},
|
||||
"dashboard.projects.sort.option.name": {
|
||||
"message": "Namn"
|
||||
},
|
||||
"dashboard.projects.sort.option.status": {
|
||||
"message": "Status"
|
||||
},
|
||||
"dashboard.projects.sort.option.type": {
|
||||
"message": "Typ"
|
||||
},
|
||||
"dashboard.projects.table.icon": {
|
||||
"message": "Ikon"
|
||||
},
|
||||
"dashboard.projects.table.id": {
|
||||
"message": "ID"
|
||||
},
|
||||
|
||||
@@ -1574,9 +1574,6 @@
|
||||
"dashboard.overview.notifications.loading": {
|
||||
"message": "Bildirimler yükleniyor..."
|
||||
},
|
||||
"dashboard.projects.bulk-edit-hint": {
|
||||
"message": "Aşağıdan seçerek birden fazla projeyi düzenleyebilirsiniz."
|
||||
},
|
||||
"dashboard.projects.bulk-edit.server-disabled": {
|
||||
"message": "Sunucu projeleri toplu düzenlemeyi desteklemez"
|
||||
},
|
||||
@@ -1652,24 +1649,6 @@
|
||||
"dashboard.projects.project.review-environment-metadata": {
|
||||
"message": "Lütfen ortam meta verilerini inceleyin"
|
||||
},
|
||||
"dashboard.projects.sort.ascending": {
|
||||
"message": "Artan"
|
||||
},
|
||||
"dashboard.projects.sort.descending": {
|
||||
"message": "Azalan sırada sırala"
|
||||
},
|
||||
"dashboard.projects.sort.option.name": {
|
||||
"message": "İsim"
|
||||
},
|
||||
"dashboard.projects.sort.option.status": {
|
||||
"message": "Durum"
|
||||
},
|
||||
"dashboard.projects.sort.option.type": {
|
||||
"message": "Tip"
|
||||
},
|
||||
"dashboard.projects.table.icon": {
|
||||
"message": "Simge"
|
||||
},
|
||||
"dashboard.projects.table.id": {
|
||||
"message": "Kimlik"
|
||||
},
|
||||
|
||||
@@ -1301,9 +1301,6 @@
|
||||
"dashboard.overview.notifications.loading": {
|
||||
"message": "Завантаження сповіщень…"
|
||||
},
|
||||
"dashboard.projects.bulk-edit-hint": {
|
||||
"message": "Ви можете редагувати кілька проєктів одночасно, вибравши їх нижче."
|
||||
},
|
||||
"dashboard.projects.bulk-edit.server-disabled": {
|
||||
"message": "Проєкти сервера не підтримують масове редагування"
|
||||
},
|
||||
@@ -1376,24 +1373,6 @@
|
||||
"dashboard.projects.project.review-environment-metadata": {
|
||||
"message": "Перегляньте метадані середовища"
|
||||
},
|
||||
"dashboard.projects.sort.ascending": {
|
||||
"message": "Зростання"
|
||||
},
|
||||
"dashboard.projects.sort.descending": {
|
||||
"message": "Спадання"
|
||||
},
|
||||
"dashboard.projects.sort.option.name": {
|
||||
"message": "Назва"
|
||||
},
|
||||
"dashboard.projects.sort.option.status": {
|
||||
"message": "Статус"
|
||||
},
|
||||
"dashboard.projects.sort.option.type": {
|
||||
"message": "Тип"
|
||||
},
|
||||
"dashboard.projects.table.icon": {
|
||||
"message": "Значок"
|
||||
},
|
||||
"dashboard.projects.table.id": {
|
||||
"message": "ID"
|
||||
},
|
||||
|
||||
@@ -1478,9 +1478,6 @@
|
||||
"dashboard.overview.notifications.loading": {
|
||||
"message": "Đang tải thông báo..."
|
||||
},
|
||||
"dashboard.projects.bulk-edit-hint": {
|
||||
"message": "Bạn có thể sửa đổi nhiều dự án cùng một lúc bằng cách chọn chúng ở dưới."
|
||||
},
|
||||
"dashboard.projects.bulk-edit.server-disabled": {
|
||||
"message": "Dự án máy chủ không hỗ trợ sửa đổi nhiều mục cùng một lúc"
|
||||
},
|
||||
@@ -1556,24 +1553,6 @@
|
||||
"dashboard.projects.project.review-environment-metadata": {
|
||||
"message": "Vui lòng xem lại thông tin dữ liệu"
|
||||
},
|
||||
"dashboard.projects.sort.ascending": {
|
||||
"message": "Từ dưới lên"
|
||||
},
|
||||
"dashboard.projects.sort.descending": {
|
||||
"message": "Từ trên xuống"
|
||||
},
|
||||
"dashboard.projects.sort.option.name": {
|
||||
"message": "Tên"
|
||||
},
|
||||
"dashboard.projects.sort.option.status": {
|
||||
"message": "Trạng thái"
|
||||
},
|
||||
"dashboard.projects.sort.option.type": {
|
||||
"message": "Loại"
|
||||
},
|
||||
"dashboard.projects.table.icon": {
|
||||
"message": "B.tượng"
|
||||
},
|
||||
"dashboard.projects.table.id": {
|
||||
"message": "ID"
|
||||
},
|
||||
|
||||
@@ -1631,9 +1631,6 @@
|
||||
"dashboard.overview.notifications.loading": {
|
||||
"message": "加载通知…"
|
||||
},
|
||||
"dashboard.projects.bulk-edit-hint": {
|
||||
"message": "你可以通过下方复选框选择多个项目来一次性编辑。"
|
||||
},
|
||||
"dashboard.projects.bulk-edit.server-disabled": {
|
||||
"message": "服务器项目不支持批量编辑"
|
||||
},
|
||||
@@ -1709,24 +1706,6 @@
|
||||
"dashboard.projects.project.review-environment-metadata": {
|
||||
"message": "请复核运行环境元数据"
|
||||
},
|
||||
"dashboard.projects.sort.ascending": {
|
||||
"message": "正序"
|
||||
},
|
||||
"dashboard.projects.sort.descending": {
|
||||
"message": "降序"
|
||||
},
|
||||
"dashboard.projects.sort.option.name": {
|
||||
"message": "名称"
|
||||
},
|
||||
"dashboard.projects.sort.option.status": {
|
||||
"message": "状态"
|
||||
},
|
||||
"dashboard.projects.sort.option.type": {
|
||||
"message": "类型"
|
||||
},
|
||||
"dashboard.projects.table.icon": {
|
||||
"message": "图标"
|
||||
},
|
||||
"dashboard.projects.table.id": {
|
||||
"message": "ID"
|
||||
},
|
||||
|
||||
@@ -1634,9 +1634,6 @@
|
||||
"dashboard.overview.notifications.loading": {
|
||||
"message": "正在載入通知..."
|
||||
},
|
||||
"dashboard.projects.bulk-edit-hint": {
|
||||
"message": "你可以透過在下方選擇專案來同時編輯多個專案。"
|
||||
},
|
||||
"dashboard.projects.bulk-edit.server-disabled": {
|
||||
"message": "伺服器專案不支援大量編輯"
|
||||
},
|
||||
@@ -1712,24 +1709,6 @@
|
||||
"dashboard.projects.project.review-environment-metadata": {
|
||||
"message": "請審查環境詮釋資料"
|
||||
},
|
||||
"dashboard.projects.sort.ascending": {
|
||||
"message": "遞增"
|
||||
},
|
||||
"dashboard.projects.sort.descending": {
|
||||
"message": "遞減"
|
||||
},
|
||||
"dashboard.projects.sort.option.name": {
|
||||
"message": "名稱"
|
||||
},
|
||||
"dashboard.projects.sort.option.status": {
|
||||
"message": "狀態"
|
||||
},
|
||||
"dashboard.projects.sort.option.type": {
|
||||
"message": "類型"
|
||||
},
|
||||
"dashboard.projects.table.icon": {
|
||||
"message": "圖示"
|
||||
},
|
||||
"dashboard.projects.table.id": {
|
||||
"message": "ID"
|
||||
},
|
||||
|
||||
@@ -1017,7 +1017,7 @@
|
||||
</div>
|
||||
|
||||
<div class="normal-page__content">
|
||||
<div class="overflow-x-auto"><NavTabs :links="navLinks" replace class="mb-4" /></div>
|
||||
<div class="mb-3 overflow-x-auto"><NavTabs :links="navLinks" replace class="mb-1" /></div>
|
||||
<NuxtPage @on-download="triggerDownloadAnimation" @delete-version="deleteVersion" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -53,6 +53,7 @@
|
||||
:project="project"
|
||||
:versions="versionsWithDisplayUrl"
|
||||
:show-files="flags.showVersionFilesInTable"
|
||||
:show-environment-column="flags.showVersionEnvironmentColumn"
|
||||
:current-member="!!currentMember"
|
||||
:loaders="tags.loaders"
|
||||
:game-versions="tags.gameVersions"
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
:project="project"
|
||||
:versions="versions"
|
||||
:show-files="flags.showVersionFilesInTable"
|
||||
:show-environment-column="flags.showVersionEnvironmentColumn"
|
||||
:current-member="!!currentMember"
|
||||
:loaders="tags.loaders"
|
||||
:game-versions="tags.gameVersions"
|
||||
|
||||
@@ -157,181 +157,215 @@
|
||||
</div>
|
||||
</NewModal>
|
||||
<ModalCreation ref="modal_creation" />
|
||||
<section class="universal-card">
|
||||
<div class="header__row">
|
||||
<h2 class="header__title text-2xl">{{ formatMessage(messages.headTitle) }}</h2>
|
||||
<div class="input-group">
|
||||
<ButtonStyled color="brand">
|
||||
<button @click="$refs.modal_creation.show($event)">
|
||||
<PlusIcon />
|
||||
{{ formatMessage(commonMessages.createAProjectButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</div>
|
||||
<p v-if="projects.length < 1">
|
||||
{{ formatMessage(messages.noProjectsYet) }}
|
||||
</p>
|
||||
<template v-else>
|
||||
<p>{{ formatMessage(messages.bulkEditHint) }}</p>
|
||||
<div class="input-group">
|
||||
<ButtonStyled>
|
||||
<button :disabled="selectedProjects.length === 0" @click="$refs.editLinksModal.show()">
|
||||
<EditIcon />
|
||||
{{ formatMessage(messages.editLinksButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<div class="push-right">
|
||||
<div class="labeled-control-row">
|
||||
{{ formatMessage(commonMessages.sortByLabel) }}
|
||||
<Combobox
|
||||
v-model="sortBy"
|
||||
:searchable="false"
|
||||
class="small-select"
|
||||
:options="sortOptions"
|
||||
@update:model-value="projects = updateSort(projects, sortBy, descending)"
|
||||
/>
|
||||
<ButtonStyled circular>
|
||||
<button
|
||||
v-tooltip="formatMessage(descending ? messages.descending : messages.ascending)"
|
||||
@click="updateDescending()"
|
||||
>
|
||||
<SortDescIcon v-if="descending" />
|
||||
<SortAscIcon v-else />
|
||||
<section class="relative overflow-hidden rounded-2xl">
|
||||
<Table
|
||||
v-model:sort-column="sortColumn"
|
||||
v-model:sort-direction="sortDirection"
|
||||
:columns="projectTableColumns"
|
||||
:data="sortedProjects"
|
||||
row-key="id"
|
||||
table-min-width="50rem"
|
||||
table-layout="auto"
|
||||
>
|
||||
<template #header>
|
||||
<div class="flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
|
||||
<h2 class="m-0 text-2xl font-semibold text-contrast">
|
||||
{{ formatMessage(messages.headTitle) }}
|
||||
</h2>
|
||||
<div class="flex w-full flex-wrap items-center gap-2 md:w-auto">
|
||||
<ButtonStyled color="brand">
|
||||
<button @click="$refs.modal_creation.show($event)">
|
||||
<PlusIcon />
|
||||
{{ formatMessage(commonMessages.createAProjectButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid-table">
|
||||
<div class="grid-table__row grid-table__header">
|
||||
<div>
|
||||
<Checkbox
|
||||
:model-value="allBulkEditableProjectsSelected"
|
||||
@update:model-value="toggleAllBulkEditableProjects()"
|
||||
/>
|
||||
</div>
|
||||
<div>{{ formatMessage(messages.iconHeader) }}</div>
|
||||
<div>{{ formatMessage(messages.nameHeader) }}</div>
|
||||
<div>{{ formatMessage(messages.idHeader) }}</div>
|
||||
<div>{{ formatMessage(messages.typeHeader) }}</div>
|
||||
<div>{{ formatMessage(messages.statusHeader) }}</div>
|
||||
<div />
|
||||
</template>
|
||||
<template #empty-state>
|
||||
<div class="flex h-64 items-center justify-center text-secondary">
|
||||
{{ formatMessage(messages.noProjectsYet) }}
|
||||
</div>
|
||||
<div v-for="project in projects" :key="`project-${project.id}`" class="grid-table__row">
|
||||
<div>
|
||||
<Checkbox
|
||||
v-tooltip="getBulkEditDisabledTooltip(project)"
|
||||
:disabled="isProjectBulkEditDisabled(project)"
|
||||
:model-value="selectedProjects.includes(project)"
|
||||
@update:model-value="toggleProjectSelection(project)"
|
||||
</template>
|
||||
<template #header-select>
|
||||
<div v-tooltip="formatMessage(messages.selectAllBulkEditableProjects)">
|
||||
<Checkbox
|
||||
:model-value="allBulkEditableProjectsSelected"
|
||||
@update:model-value="toggleAllBulkEditableProjects()"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<template #cell-select="{ row: project }">
|
||||
<div>
|
||||
<Checkbox
|
||||
v-tooltip="getBulkEditDisabledTooltip(project)"
|
||||
:disabled="isProjectBulkEditDisabled(project)"
|
||||
:model-value="isProjectSelected(project)"
|
||||
@update:model-value="toggleProjectSelection(project)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<template #cell-name="{ row: project }">
|
||||
<nuxt-link class="project-name-cell" :to="getProjectUrl(project)">
|
||||
<span
|
||||
v-tooltip="project.title"
|
||||
class="flex size-6 shrink-0 items-center justify-center overflow-hidden rounded text-primary"
|
||||
>
|
||||
<img
|
||||
v-if="project.icon_url"
|
||||
:src="project.icon_url"
|
||||
:alt="formatMessage(messages.projectIconAlt, { title: project.title })"
|
||||
class="h-6 w-6 rounded object-cover"
|
||||
/>
|
||||
<BoxIcon v-else class="h-full w-full" />
|
||||
</span>
|
||||
<span class="project-title">
|
||||
<IssuesIcon
|
||||
v-if="project.moderator_message"
|
||||
:aria-label="formatMessage(messages.projectModeratorMessageAriaLabel)"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<nuxt-link
|
||||
tabindex="-1"
|
||||
:to="`/${getProjectTypeForUrl(project.project_type, project.loaders)}/${
|
||||
project.slug ? project.slug : project.id
|
||||
}`"
|
||||
>
|
||||
<Avatar
|
||||
:src="project.icon_url"
|
||||
aria-hidden="true"
|
||||
:alt="formatMessage(messages.projectIconAlt, { title: project.title })"
|
||||
no-shadow
|
||||
/>
|
||||
</nuxt-link>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<span class="project-title">
|
||||
<IssuesIcon
|
||||
v-if="project.moderator_message"
|
||||
:aria-label="formatMessage(messages.projectModeratorMessageAriaLabel)"
|
||||
/>
|
||||
|
||||
<nuxt-link
|
||||
class="hover-link wrap-as-needed"
|
||||
:to="`/${getProjectTypeForUrl(project.project_type, project.loaders)}/${
|
||||
project.slug ? project.slug : project.id
|
||||
}`"
|
||||
>
|
||||
{{ project.title }}
|
||||
</nuxt-link>
|
||||
<span class="project-title-link wrap-as-needed">
|
||||
{{ project.title }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<CopyCode :text="project.id" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{{ formatProjectType(getProjectTypeForUrl(project.project_type, project.loaders)) }}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<ProjectStatusBadge v-if="project.status" :status="project.status" />
|
||||
</div>
|
||||
|
||||
<div class="flex !flex-row items-center !justify-end gap-2">
|
||||
<ButtonStyled
|
||||
v-if="projectsWithMigrationWarning.includes(project.id)"
|
||||
circular
|
||||
color="orange"
|
||||
</span>
|
||||
</nuxt-link>
|
||||
</template>
|
||||
<template #cell-id="{ row: project }">
|
||||
<div class="flex items-center">
|
||||
<CopyCode :text="project.id" />
|
||||
</div>
|
||||
</template>
|
||||
<template #cell-type="{ row: project }">
|
||||
<div class="flex items-center">
|
||||
{{ getProjectDisplayType(project) }}
|
||||
</div>
|
||||
</template>
|
||||
<template #cell-status="{ row: project }">
|
||||
<div class="flex items-center">
|
||||
<ProjectStatusBadge v-if="project.status" :status="project.status" />
|
||||
</div>
|
||||
</template>
|
||||
<template #cell-actions="{ row: project }">
|
||||
<div class="flex !flex-row items-center !justify-end gap-2">
|
||||
<ButtonStyled
|
||||
v-if="projectsWithMigrationWarning.includes(project.id)"
|
||||
circular
|
||||
color="orange"
|
||||
>
|
||||
<nuxt-link
|
||||
v-tooltip="formatMessage(messages.reviewEnvironmentMetadata)"
|
||||
:to="`${getProjectUrl(project)}?showEnvironmentMigrationWarning=true`"
|
||||
>
|
||||
<nuxt-link
|
||||
v-tooltip="formatMessage(messages.reviewEnvironmentMetadata)"
|
||||
:to="`/${getProjectTypeForUrl(project.project_type, project.loaders)}/${
|
||||
project.slug ? project.slug : project.id
|
||||
}?showEnvironmentMigrationWarning=true`"
|
||||
>
|
||||
<TriangleAlertIcon />
|
||||
</nuxt-link>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled circular>
|
||||
<nuxt-link
|
||||
v-tooltip="formatMessage(commonMessages.settingsLabel)"
|
||||
:to="`/${getProjectTypeForUrl(project.project_type, project.loaders)}/${
|
||||
project.slug ? project.slug : project.id
|
||||
}/settings`"
|
||||
>
|
||||
<SettingsIcon />
|
||||
</nuxt-link>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
<TriangleAlertIcon />
|
||||
</nuxt-link>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled circular>
|
||||
<nuxt-link
|
||||
v-tooltip="formatMessage(commonMessages.settingsLabel)"
|
||||
:to="`${getProjectUrl(project)}/settings`"
|
||||
>
|
||||
<SettingsIcon />
|
||||
</nuxt-link>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</template>
|
||||
</Table>
|
||||
</section>
|
||||
<FloatingActionBar
|
||||
:shown="selectedProjects.length > 0"
|
||||
:aria-label="formatMessage(messages.selectionActionBarAriaLabel)"
|
||||
hide-when-modal-open
|
||||
>
|
||||
<div class="flex items-center gap-0.5">
|
||||
<div
|
||||
class="relative h-8 shrink-0"
|
||||
:style="{ width: `${selectedProjectIconStackWidth}px` }"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<div
|
||||
v-for="(project, index) in visibleSelectedProjects"
|
||||
:key="project.id"
|
||||
v-tooltip="project.title"
|
||||
class="absolute top-0 flex h-8 w-8 items-center justify-center overflow-hidden rounded-lg border-[1.5px] border-solid border-surface-3 bg-surface-4"
|
||||
:style="{
|
||||
left: `${index * SELECTED_PROJECT_ICON_STACK_OFFSET}px`,
|
||||
zIndex: visibleSelectedProjects.length - index,
|
||||
}"
|
||||
>
|
||||
<img
|
||||
v-if="project.icon_url"
|
||||
:src="project.icon_url"
|
||||
:alt="formatMessage(messages.projectIconAlt, { title: project.title })"
|
||||
class="h-full w-full rounded-lg object-cover"
|
||||
/>
|
||||
<BoxIcon v-else class="h-full w-full text-primary" />
|
||||
</div>
|
||||
<div
|
||||
v-if="selectedProjectOverflowCount > 0"
|
||||
class="absolute top-0 flex h-8 w-8 items-center justify-center rounded-lg border-[1.5px] border-solid border-surface-3 bg-surface-4 text-xs font-bold text-contrast"
|
||||
:style="{
|
||||
left: `${visibleSelectedProjects.length * SELECTED_PROJECT_ICON_STACK_OFFSET}px`,
|
||||
zIndex: 0,
|
||||
}"
|
||||
>
|
||||
+{{ selectedProjectOverflowCount }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</section>
|
||||
|
||||
<span class="px-3 py-2 text-base font-semibold tabular-nums text-contrast">
|
||||
{{ formatMessage(messages.selectedProjectsCount, { count: selectedProjects.length }) }}
|
||||
</span>
|
||||
<div class="mx-0.5 h-6 w-px bg-surface-5" />
|
||||
<ButtonStyled type="transparent">
|
||||
<button
|
||||
v-tooltip="formatMessage(commonMessages.clearButton)"
|
||||
class="!text-primary"
|
||||
@click="clearProjectSelection()"
|
||||
>
|
||||
<XIcon class="cq-show-icon hidden" />
|
||||
<span class="bar-label">{{ formatMessage(commonMessages.clearButton) }}</span>
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
|
||||
<div class="ml-auto flex items-center gap-0.5">
|
||||
<ButtonStyled color="brand">
|
||||
<button v-tooltip="formatMessage(messages.editLinksButton)" @click="showEditLinksModal()">
|
||||
<EditIcon />
|
||||
<span class="bar-label">{{ formatMessage(messages.editLinksButton) }}</span>
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</FloatingActionBar>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {
|
||||
BoxIcon,
|
||||
EditIcon,
|
||||
IssuesIcon,
|
||||
PlusIcon,
|
||||
SaveIcon,
|
||||
SettingsIcon,
|
||||
SortAscIcon,
|
||||
SortDescIcon,
|
||||
TrashIcon,
|
||||
TriangleAlertIcon,
|
||||
XIcon,
|
||||
} from '@modrinth/assets'
|
||||
import {
|
||||
Avatar,
|
||||
ButtonStyled,
|
||||
Checkbox,
|
||||
Combobox,
|
||||
commonMessages,
|
||||
CopyCode,
|
||||
defineMessages,
|
||||
FloatingActionBar,
|
||||
injectNotificationManager,
|
||||
IntlFormatted,
|
||||
NewModal,
|
||||
ProjectStatusBadge,
|
||||
StyledInput,
|
||||
Table,
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
import { formatProjectType } from '@modrinth/utils'
|
||||
@@ -360,6 +394,14 @@ const messages = defineMessages({
|
||||
id: 'dashboard.projects.links.button.edit',
|
||||
defaultMessage: 'Edit links',
|
||||
},
|
||||
selectedProjectsCount: {
|
||||
id: 'dashboard.projects.selection-bar.selected-count',
|
||||
defaultMessage: '{count, plural, one {# project selected} other {# projects selected}}',
|
||||
},
|
||||
selectionActionBarAriaLabel: {
|
||||
id: 'dashboard.projects.selection-bar.aria-label',
|
||||
defaultMessage: 'Selected projects actions',
|
||||
},
|
||||
editLinksHeader: {
|
||||
id: 'dashboard.projects.links.header.edit',
|
||||
defaultMessage: 'Edit links',
|
||||
@@ -434,34 +476,6 @@ const messages = defineMessages({
|
||||
id: 'dashboard.projects.empty',
|
||||
defaultMessage: "You don't have any projects yet. Click the green button above to begin.",
|
||||
},
|
||||
bulkEditHint: {
|
||||
id: 'dashboard.projects.bulk-edit-hint',
|
||||
defaultMessage: 'You can edit multiple projects at once by selecting them below.',
|
||||
},
|
||||
ascending: {
|
||||
id: 'dashboard.projects.sort.ascending',
|
||||
defaultMessage: 'Ascending',
|
||||
},
|
||||
descending: {
|
||||
id: 'dashboard.projects.sort.descending',
|
||||
defaultMessage: 'Descending',
|
||||
},
|
||||
sortOptionName: {
|
||||
id: 'dashboard.projects.sort.option.name',
|
||||
defaultMessage: 'Name',
|
||||
},
|
||||
sortOptionStatus: {
|
||||
id: 'dashboard.projects.sort.option.status',
|
||||
defaultMessage: 'Status',
|
||||
},
|
||||
sortOptionType: {
|
||||
id: 'dashboard.projects.sort.option.type',
|
||||
defaultMessage: 'Type',
|
||||
},
|
||||
iconHeader: {
|
||||
id: 'dashboard.projects.table.icon',
|
||||
defaultMessage: 'Icon',
|
||||
},
|
||||
nameHeader: {
|
||||
id: 'dashboard.projects.table.name',
|
||||
defaultMessage: 'Name',
|
||||
@@ -478,6 +492,10 @@ const messages = defineMessages({
|
||||
id: 'dashboard.projects.table.status',
|
||||
defaultMessage: 'Status',
|
||||
},
|
||||
selectAllBulkEditableProjects: {
|
||||
id: 'dashboard.projects.table.select-all-bulk-editable',
|
||||
defaultMessage: 'Select all projects that support bulk editing',
|
||||
},
|
||||
projectIconAlt: {
|
||||
id: 'dashboard.projects.project.icon-alt',
|
||||
defaultMessage: 'Icon for {title}',
|
||||
@@ -505,14 +523,9 @@ useHead({ title: () => `${formatMessage(messages.headTitle)} - Modrinth` })
|
||||
const user = await useUser()
|
||||
const projects = ref([])
|
||||
const projectsWithMigrationWarning = ref([])
|
||||
const selectedProjects = ref([])
|
||||
const sortBy = ref('Name')
|
||||
const sortOptions = computed(() => [
|
||||
{ value: 'Name', label: formatMessage(messages.sortOptionName) },
|
||||
{ value: 'Status', label: formatMessage(messages.sortOptionStatus) },
|
||||
{ value: 'Type', label: formatMessage(messages.sortOptionType) },
|
||||
])
|
||||
const descending = ref(false)
|
||||
const selectedProjectIds = ref([])
|
||||
const sortColumn = ref('name')
|
||||
const sortDirection = ref('asc')
|
||||
const editLinks = reactive({
|
||||
showAffected: false,
|
||||
source: { val: '', clear: false },
|
||||
@@ -522,7 +535,62 @@ const editLinks = reactive({
|
||||
})
|
||||
|
||||
const editLinksModal = ref(null)
|
||||
const modal_creation = ref(null)
|
||||
const sortCollator = new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' })
|
||||
const SELECTED_PROJECT_ICON_STACK_OFFSET = 24
|
||||
|
||||
const projectTableColumns = computed(() => [
|
||||
{
|
||||
key: 'select',
|
||||
width: '3rem',
|
||||
headerClass: '!text-center',
|
||||
cellClass: '!overflow-visible',
|
||||
},
|
||||
{
|
||||
key: 'name',
|
||||
label: formatMessage(messages.nameHeader),
|
||||
enableSorting: true,
|
||||
defaultSortDirection: 'asc',
|
||||
width: '22rem',
|
||||
},
|
||||
{
|
||||
key: 'id',
|
||||
label: formatMessage(messages.idHeader),
|
||||
width: '13rem',
|
||||
cellClass: '!overflow-visible',
|
||||
},
|
||||
{
|
||||
key: 'type',
|
||||
label: formatMessage(messages.typeHeader),
|
||||
enableSorting: true,
|
||||
width: '11rem',
|
||||
},
|
||||
{
|
||||
key: 'status',
|
||||
label: formatMessage(messages.statusHeader),
|
||||
enableSorting: true,
|
||||
width: '10rem',
|
||||
cellClass: '!overflow-visible',
|
||||
},
|
||||
{
|
||||
key: 'actions',
|
||||
width: '6rem',
|
||||
align: 'right',
|
||||
cellClass: '!overflow-visible',
|
||||
},
|
||||
])
|
||||
|
||||
const sortedProjects = computed(() => {
|
||||
const direction = sortDirection.value === 'desc' ? -1 : 1
|
||||
|
||||
return projects.value.slice().sort((left, right) => {
|
||||
const result = sortCollator.compare(
|
||||
getProjectSortValue(left, sortColumn.value),
|
||||
getProjectSortValue(right, sortColumn.value),
|
||||
)
|
||||
|
||||
return result * direction
|
||||
})
|
||||
})
|
||||
|
||||
function getLinkInputPlaceholder(clearLink, isDiscord = false) {
|
||||
if (clearLink) {
|
||||
@@ -545,16 +613,35 @@ const bulkEditableProjects = computed(() =>
|
||||
projects.value.filter((project) => !isProjectBulkEditDisabled(project)),
|
||||
)
|
||||
|
||||
const selectedProjects = computed(() =>
|
||||
projects.value.filter((project) => selectedProjectIds.value.includes(project.id)),
|
||||
)
|
||||
const visibleSelectedProjects = computed(() => selectedProjects.value.slice(0, 3))
|
||||
const selectedProjectOverflowCount = computed(() => Math.max(0, selectedProjects.value.length - 3))
|
||||
const selectedProjectIconStackWidth = computed(() => {
|
||||
if (selectedProjects.value.length === 0) return 0
|
||||
|
||||
return (
|
||||
32 +
|
||||
(visibleSelectedProjects.value.length - 1 + (selectedProjectOverflowCount.value > 0 ? 1 : 0)) *
|
||||
SELECTED_PROJECT_ICON_STACK_OFFSET
|
||||
)
|
||||
})
|
||||
|
||||
const allBulkEditableProjectsSelected = computed(
|
||||
() =>
|
||||
bulkEditableProjects.value.length > 0 &&
|
||||
bulkEditableProjects.value.every((project) => selectedProjects.value.includes(project)),
|
||||
bulkEditableProjects.value.every((project) => selectedProjectIds.value.includes(project.id)),
|
||||
)
|
||||
|
||||
function toggleAllBulkEditableProjects() {
|
||||
selectedProjects.value = allBulkEditableProjectsSelected.value
|
||||
selectedProjectIds.value = allBulkEditableProjectsSelected.value
|
||||
? []
|
||||
: bulkEditableProjects.value.slice()
|
||||
: bulkEditableProjects.value.map((project) => project.id)
|
||||
}
|
||||
|
||||
function isProjectSelected(project) {
|
||||
return selectedProjectIds.value.includes(project.id)
|
||||
}
|
||||
|
||||
function toggleProjectSelection(project) {
|
||||
@@ -562,12 +649,24 @@ function toggleProjectSelection(project) {
|
||||
return
|
||||
}
|
||||
|
||||
if (selectedProjects.value.includes(project)) {
|
||||
selectedProjects.value = selectedProjects.value.filter((it) => it !== project)
|
||||
if (isProjectSelected(project)) {
|
||||
selectedProjectIds.value = selectedProjectIds.value.filter((id) => id !== project.id)
|
||||
return
|
||||
}
|
||||
|
||||
selectedProjects.value = [...selectedProjects.value, project]
|
||||
selectedProjectIds.value = [...selectedProjectIds.value, project.id]
|
||||
}
|
||||
|
||||
function clearProjectSelection() {
|
||||
selectedProjectIds.value = []
|
||||
}
|
||||
|
||||
function showEditLinksModal() {
|
||||
if (selectedProjects.value.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
editLinksModal.value?.show()
|
||||
}
|
||||
|
||||
function getBulkEditDisabledTooltip(project) {
|
||||
@@ -578,40 +677,28 @@ function getBulkEditDisabledTooltip(project) {
|
||||
return ''
|
||||
}
|
||||
|
||||
function updateSort(list, sort, desc) {
|
||||
let sortedArray = list
|
||||
switch (sort) {
|
||||
case 'Name':
|
||||
sortedArray = list.slice().sort((a, b) => a.title.localeCompare(b.title))
|
||||
break
|
||||
case 'Status':
|
||||
sortedArray = list.slice().sort((a, b) => {
|
||||
if (a.status < b.status) return -1
|
||||
if (a.status > b.status) return 1
|
||||
return 0
|
||||
})
|
||||
break
|
||||
case 'Type':
|
||||
sortedArray = list.slice().sort((a, b) => {
|
||||
if (a.project_type < b.project_type) return -1
|
||||
if (a.project_type > b.project_type) return 1
|
||||
return 0
|
||||
})
|
||||
break
|
||||
function getProjectUrlType(project) {
|
||||
return getProjectTypeForUrl(project.project_type, project.loaders)
|
||||
}
|
||||
|
||||
function getProjectDisplayType(project) {
|
||||
return formatProjectType(getProjectUrlType(project))
|
||||
}
|
||||
|
||||
function getProjectUrl(project) {
|
||||
return `/${getProjectUrlType(project)}/${project.slug ? project.slug : project.id}`
|
||||
}
|
||||
|
||||
function getProjectSortValue(project, column) {
|
||||
switch (column) {
|
||||
case 'type':
|
||||
return getProjectDisplayType(project)
|
||||
case 'status':
|
||||
return project.status ?? ''
|
||||
case 'name':
|
||||
default:
|
||||
break
|
||||
return project.title ?? ''
|
||||
}
|
||||
if (desc) sortedArray = sortedArray.reverse()
|
||||
return sortedArray
|
||||
}
|
||||
|
||||
function resort() {
|
||||
projects.value = updateSort(projects.value, sortBy.value, descending.value)
|
||||
}
|
||||
|
||||
function updateDescending() {
|
||||
descending.value = !descending.value
|
||||
resort()
|
||||
}
|
||||
|
||||
async function bulkEditLinks() {
|
||||
@@ -635,7 +722,7 @@ async function bulkEditLinks() {
|
||||
text: formatMessage(messages.bulkEditSuccessText),
|
||||
type: 'success',
|
||||
})
|
||||
selectedProjects.value = []
|
||||
selectedProjectIds.value = []
|
||||
|
||||
editLinks.issues.val = ''
|
||||
editLinks.source.val = ''
|
||||
@@ -656,7 +743,7 @@ async function bulkEditLinks() {
|
||||
|
||||
await initUserProjects()
|
||||
if (user.value?.projects) {
|
||||
projects.value = updateSort(user.value.projects, 'Name', false)
|
||||
projects.value = user.value.projects.slice()
|
||||
|
||||
// minecraft_java_server type determined from component on projectV3
|
||||
projects.value = projects.value.map((project) => {
|
||||
@@ -677,126 +764,20 @@ if (user.value?.projects) {
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.grid-table {
|
||||
display: grid;
|
||||
grid-template-columns:
|
||||
min-content min-content minmax(min-content, 2fr)
|
||||
minmax(min-content, 1fr) minmax(min-content, 1fr) minmax(min-content, 1fr) min-content;
|
||||
border-radius: var(--size-rounded-sm);
|
||||
overflow: hidden;
|
||||
margin-top: var(--spacing-card-md);
|
||||
outline: 1px solid transparent;
|
||||
|
||||
.grid-table__row {
|
||||
display: contents;
|
||||
|
||||
> div {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
padding: var(--spacing-card-sm);
|
||||
|
||||
// Left edge of table
|
||||
&:first-child {
|
||||
padding-left: var(--spacing-card-bg);
|
||||
}
|
||||
|
||||
// Right edge of table
|
||||
&:last-child {
|
||||
padding-right: var(--spacing-card-bg);
|
||||
}
|
||||
}
|
||||
|
||||
&:nth-child(2n + 1) > div {
|
||||
background-color: var(--color-table-alternate-row);
|
||||
}
|
||||
|
||||
&.grid-table__header > div {
|
||||
background-color: var(--color-bg);
|
||||
font-weight: bold;
|
||||
color: var(--color-text-dark);
|
||||
padding-top: var(--spacing-card-bg);
|
||||
padding-bottom: var(--spacing-card-bg);
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 750px) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.grid-table__row {
|
||||
display: grid;
|
||||
grid-template: 'checkbox icon name type settings' 'checkbox icon id status settings';
|
||||
grid-template-columns:
|
||||
min-content min-content minmax(min-content, 2fr)
|
||||
minmax(min-content, 1fr) min-content;
|
||||
|
||||
:nth-child(1) {
|
||||
grid-area: checkbox;
|
||||
}
|
||||
|
||||
:nth-child(2) {
|
||||
grid-area: icon;
|
||||
}
|
||||
|
||||
:nth-child(3) {
|
||||
grid-area: name;
|
||||
}
|
||||
|
||||
:nth-child(4) {
|
||||
grid-area: id;
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
:nth-child(5) {
|
||||
grid-area: type;
|
||||
}
|
||||
|
||||
:nth-child(6) {
|
||||
grid-area: status;
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
:nth-child(7) {
|
||||
grid-area: settings;
|
||||
}
|
||||
}
|
||||
|
||||
.grid-table__header {
|
||||
grid-template: 'checkbox settings';
|
||||
grid-template-columns: min-content minmax(min-content, 1fr);
|
||||
|
||||
:nth-child(2),
|
||||
:nth-child(3),
|
||||
:nth-child(4),
|
||||
:nth-child(5),
|
||||
:nth-child(6) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 560px) {
|
||||
.grid-table__row {
|
||||
display: grid;
|
||||
grid-template: 'checkbox icon name settings' 'checkbox icon id settings' 'checkbox icon type settings' 'checkbox icon status settings';
|
||||
grid-template-columns: min-content min-content minmax(min-content, 1fr) min-content;
|
||||
|
||||
:nth-child(5) {
|
||||
padding-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.grid-table__header {
|
||||
grid-template: 'checkbox settings';
|
||||
grid-template-columns: min-content minmax(min-content, 1fr);
|
||||
}
|
||||
}
|
||||
.project-name-cell {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
min-height: 3.5rem;
|
||||
align-items: center;
|
||||
gap: var(--spacing-card-sm);
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.project-title {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
min-width: 0;
|
||||
gap: var(--spacing-card-xs);
|
||||
|
||||
svg {
|
||||
@@ -804,29 +785,11 @@ if (user.value?.projects) {
|
||||
}
|
||||
}
|
||||
|
||||
.status {
|
||||
margin-top: var(--spacing-card-xs);
|
||||
}
|
||||
|
||||
.hover-link:hover {
|
||||
.project-name-cell:hover .project-title-link,
|
||||
.project-name-cell:focus-visible .project-title-link {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.labeled-control-row {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
min-width: 0;
|
||||
align-items: center;
|
||||
gap: var(--spacing-card-md);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.small-select {
|
||||
width: -moz-fit-content;
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.label-button[data-active='true'] {
|
||||
--background-color: var(--color-red);
|
||||
--text-color: var(--color-brand-inverted);
|
||||
|
||||
@@ -183,24 +183,24 @@
|
||||
>
|
||||
<div class="flex flex-col gap-6">
|
||||
<template v-if="auth.user.has_totp && twoFactorStep === 0">
|
||||
<label for="two-factor-code">
|
||||
<div class="flex flex-col gap-2.5">
|
||||
<span class="text-md font-semibold text-contrast">{{
|
||||
formatMessage(messages.twoFactorEnterCodeLabel)
|
||||
}}</span>
|
||||
<StyledInput
|
||||
id="two-factor-code"
|
||||
v-model="twoFactorCode"
|
||||
:maxlength="11"
|
||||
:placeholder="formatMessage(messages.twoFactorCodePlaceholder)"
|
||||
@keyup.enter="removeTwoFactor()"
|
||||
/>
|
||||
<span class="label__description">{{
|
||||
formatMessage(messages.twoFactorEnterCodeDescription)
|
||||
}}</span>
|
||||
</label>
|
||||
<StyledInput
|
||||
id="two-factor-code"
|
||||
v-model="twoFactorCode"
|
||||
:maxlength="11"
|
||||
:placeholder="formatMessage(messages.twoFactorCodePlaceholder)"
|
||||
@keyup.enter="removeTwoFactor()"
|
||||
/>
|
||||
<p v-if="twoFactorIncorrect" class="known-errors m-0">
|
||||
{{ formatMessage(messages.twoFactorIncorrectError) }}
|
||||
</p>
|
||||
<p v-if="twoFactorIncorrect" class="known-errors m-0">
|
||||
{{ formatMessage(messages.twoFactorIncorrectError) }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex justify-end gap-2.5">
|
||||
<ButtonStyled>
|
||||
<button @click="$refs.manageTwoFactorModal.hide()">
|
||||
@@ -222,12 +222,13 @@
|
||||
<p class="m-0">
|
||||
<IntlFormatted :message-id="messages.twoFactorSetupScan">
|
||||
<template #authy-link="{ children }">
|
||||
<a href="https://authy.com/" target="_blank" rel="noreferrer">
|
||||
<a class="underline" href="https://authy.com/" target="_blank" rel="noreferrer">
|
||||
<component :is="() => children" />
|
||||
</a>
|
||||
</template>
|
||||
<template #microsoft-authenticator-link="{ children }">
|
||||
<a
|
||||
class="underline"
|
||||
href="https://www.microsoft.com/en-us/security/mobile-authenticator-app"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
@@ -252,25 +253,22 @@
|
||||
</p>
|
||||
</template>
|
||||
<template v-if="twoFactorStep === 1">
|
||||
<label for="verify-code">
|
||||
<div class="flex flex-col gap-2.5">
|
||||
<span class="text-md font-semibold text-contrast">{{
|
||||
formatMessage(messages.twoFactorVerifyCodeLabel)
|
||||
}}</span>
|
||||
<TwoFactorAuthCodeInput
|
||||
ref="twoFactorCodeInput"
|
||||
v-model="twoFactorCode"
|
||||
@keyup.enter="verifyTwoFactorCode()"
|
||||
/>
|
||||
<span class="label__description">{{
|
||||
formatMessage(messages.twoFactorVerifyCodeDescription)
|
||||
}}</span>
|
||||
</label>
|
||||
<StyledInput
|
||||
id="verify-code"
|
||||
v-model="twoFactorCode"
|
||||
:maxlength="6"
|
||||
autocomplete="one-time-code"
|
||||
:placeholder="formatMessage(messages.twoFactorCodePlaceholder)"
|
||||
@keyup.enter="verifyTwoFactorCode()"
|
||||
/>
|
||||
<p v-if="twoFactorIncorrect" class="known-errors m-0">
|
||||
{{ formatMessage(messages.twoFactorIncorrectError) }}
|
||||
</p>
|
||||
<p v-if="twoFactorIncorrect" class="known-errors m-0">
|
||||
{{ formatMessage(messages.twoFactorIncorrectError) }}
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
<template v-if="twoFactorStep === 2">
|
||||
<p class="m-0">{{ formatMessage(messages.twoFactorBackupCodesIntro) }}</p>
|
||||
@@ -478,6 +476,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { nextTick, watch } from 'vue'
|
||||
import {
|
||||
CheckIcon,
|
||||
DownloadIcon,
|
||||
@@ -503,6 +502,7 @@ import {
|
||||
NewModal,
|
||||
StyledInput,
|
||||
Table,
|
||||
TwoFactorAuthCodeInput,
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
import KeyIcon from 'assets/icons/auth/key.svg'
|
||||
@@ -878,9 +878,10 @@ const manageTwoFactorModal = ref()
|
||||
const twoFactorSecret = ref(null)
|
||||
const twoFactorFlow = ref(null)
|
||||
const twoFactorStep = ref(0)
|
||||
const twoFactorCodeInput = ref()
|
||||
async function showTwoFactorModal() {
|
||||
twoFactorStep.value = 0
|
||||
twoFactorCode.value = null
|
||||
twoFactorCode.value = ''
|
||||
twoFactorIncorrect.value = false
|
||||
if (auth.value.user.has_totp) {
|
||||
manageTwoFactorModal.value.show()
|
||||
@@ -890,7 +891,6 @@ async function showTwoFactorModal() {
|
||||
twoFactorSecret.value = null
|
||||
twoFactorFlow.value = null
|
||||
backupCodes.value = []
|
||||
manageTwoFactorModal.value.show()
|
||||
|
||||
startLoading()
|
||||
try {
|
||||
@@ -908,11 +908,22 @@ async function showTwoFactorModal() {
|
||||
})
|
||||
}
|
||||
stopLoading()
|
||||
manageTwoFactorModal.value.show()
|
||||
}
|
||||
|
||||
const twoFactorIncorrect = ref(false)
|
||||
const twoFactorCode = ref(null)
|
||||
const twoFactorCode = ref('')
|
||||
const backupCodes = ref([])
|
||||
|
||||
watch(twoFactorStep, async (step) => {
|
||||
if (step !== 1) {
|
||||
return
|
||||
}
|
||||
|
||||
await nextTick()
|
||||
twoFactorCodeInput.value?.focus()
|
||||
})
|
||||
|
||||
async function verifyTwoFactorCode() {
|
||||
startLoading()
|
||||
try {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use crate::validate::{
|
||||
SupportedGameVersions, ValidationError, ValidationResult, filter_out_packs,
|
||||
SupportedGameVersions, ValidationError, ValidationResult,
|
||||
validate_pack_formats,
|
||||
};
|
||||
use std::io::Cursor;
|
||||
use zip::ZipArchive;
|
||||
@@ -29,8 +30,6 @@ impl super::Validator for FabricValidator {
|
||||
));
|
||||
}
|
||||
|
||||
filter_out_packs(archive)?;
|
||||
|
||||
Ok(ValidationResult::Pass)
|
||||
Ok(validate_pack_formats(archive))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use crate::validate::{
|
||||
SupportedGameVersions, ValidationError, ValidationResult, filter_out_packs,
|
||||
SupportedGameVersions, ValidationError, ValidationResult,
|
||||
validate_pack_formats,
|
||||
};
|
||||
use chrono::DateTime;
|
||||
use std::io::Cursor;
|
||||
@@ -36,9 +37,7 @@ impl super::Validator for ForgeValidator {
|
||||
));
|
||||
}
|
||||
|
||||
filter_out_packs(archive)?;
|
||||
|
||||
Ok(ValidationResult::Pass)
|
||||
Ok(validate_pack_formats(archive))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,8 +73,6 @@ impl super::Validator for LegacyForgeValidator {
|
||||
));
|
||||
};
|
||||
|
||||
filter_out_packs(archive)?;
|
||||
|
||||
Ok(ValidationResult::Pass)
|
||||
Ok(validate_pack_formats(archive))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use crate::validate::{
|
||||
SupportedGameVersions, ValidationError, ValidationResult, filter_out_packs,
|
||||
SupportedGameVersions, ValidationError, ValidationResult,
|
||||
validate_pack_formats,
|
||||
};
|
||||
use std::io::Cursor;
|
||||
use zip::ZipArchive;
|
||||
@@ -29,8 +30,6 @@ impl super::Validator for LiteLoaderValidator {
|
||||
));
|
||||
}
|
||||
|
||||
filter_out_packs(archive)?;
|
||||
|
||||
Ok(ValidationResult::Pass)
|
||||
Ok(validate_pack_formats(archive))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -326,9 +326,10 @@ fn game_version_supported(
|
||||
}
|
||||
}
|
||||
|
||||
pub fn filter_out_packs(
|
||||
archive: &mut ZipArchive<Cursor<bytes::Bytes>>,
|
||||
) -> Result<ValidationResult, ValidationError> {
|
||||
#[must_use]
|
||||
pub fn validate_pack_formats(
|
||||
archive: &mut ZipArchive<Cursor<Bytes>>,
|
||||
) -> ValidationResult {
|
||||
if (archive.by_name("modlist.html").is_ok()
|
||||
&& archive.by_name("manifest.json").is_ok())
|
||||
|| archive
|
||||
@@ -338,10 +339,10 @@ pub fn filter_out_packs(
|
||||
.file_names()
|
||||
.any(|x| x.starts_with("override/mods/") && x.ends_with(".jar"))
|
||||
{
|
||||
return Ok(ValidationResult::Warning(
|
||||
"Invalid modpack file. You must upload a valid .MRPACK file.",
|
||||
));
|
||||
return ValidationResult::Warning(
|
||||
"Invalid modpack file. Modpacks must be uploaded in the .mrpack format, not as a ZIP file.",
|
||||
);
|
||||
}
|
||||
|
||||
Ok(ValidationResult::Pass)
|
||||
ValidationResult::Pass
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use crate::validate::{
|
||||
SupportedGameVersions, ValidationError, ValidationResult, filter_out_packs,
|
||||
SupportedGameVersions, ValidationError, ValidationResult,
|
||||
validate_pack_formats,
|
||||
};
|
||||
use std::io::Cursor;
|
||||
use zip::ZipArchive;
|
||||
@@ -33,8 +34,6 @@ impl super::Validator for NeoForgeValidator {
|
||||
));
|
||||
}
|
||||
|
||||
filter_out_packs(archive)?;
|
||||
|
||||
Ok(ValidationResult::Pass)
|
||||
Ok(validate_pack_formats(archive))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use crate::validate::{
|
||||
SupportedGameVersions, ValidationError, ValidationResult, filter_out_packs,
|
||||
SupportedGameVersions, ValidationError, ValidationResult,
|
||||
validate_pack_formats,
|
||||
};
|
||||
use chrono::DateTime;
|
||||
use std::io::Cursor;
|
||||
@@ -34,8 +35,6 @@ impl super::Validator for QuiltValidator {
|
||||
));
|
||||
}
|
||||
|
||||
filter_out_packs(archive)?;
|
||||
|
||||
Ok(ValidationResult::Pass)
|
||||
Ok(validate_pack_formats(archive))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use crate::validate::{
|
||||
SupportedGameVersions, ValidationError, ValidationResult, filter_out_packs,
|
||||
SupportedGameVersions, ValidationError, ValidationResult,
|
||||
validate_pack_formats,
|
||||
};
|
||||
use std::io::Cursor;
|
||||
use zip::ZipArchive;
|
||||
@@ -29,8 +30,6 @@ impl super::Validator for RiftValidator {
|
||||
));
|
||||
}
|
||||
|
||||
filter_out_packs(archive)?;
|
||||
|
||||
Ok(ValidationResult::Pass)
|
||||
Ok(validate_pack_formats(archive))
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+12
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n\t\t\t\tUPDATE instance_content_entries\n\t\t\t\tSET file_id = ?, modified_at = ?\n\t\t\t\tWHERE instance_id = ? AND file_id = ?\n\t\t\t\t",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 4
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "028eb6e4c6e87388ea7a371bd7b41e2b10c828ab93140f6d9091fbc76e03cdec"
|
||||
}
|
||||
Generated
-20
@@ -1,20 +0,0 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n\t\t\t\t\t\t\t\t\t\t\t\t\tSELECT id\n\t\t\t\t\t\t\t\t\t\t\t\t\tFROM instances\n\t\t\t\t\t\t\t\t\t\t\t\t\tWHERE path = ?\n\t\t\t\t\t\t\t\t\t\t\t\t\t",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "id",
|
||||
"ordinal": 0,
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "5363aa346fdead6a331a5a209d0505315b9cbb5b842216db5585cc2f7f739180"
|
||||
}
|
||||
Generated
+12
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n\t\t\t\tDELETE FROM instance_files\n\t\t\t\tWHERE id = ?\n\t\t\t\t",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "7a912eb3fd9b7da37cce1d8153241aba899e679a6248c8a1de21b7b65155843e"
|
||||
}
|
||||
Generated
+20
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n\t\tSELECT id\n\t\tFROM instance_files\n\t\tWHERE instance_id = ? AND relative_path = ?\n\t\t",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "id",
|
||||
"ordinal": 0,
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 2
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "b4885df7ebd5300533179c0ed98cbca8ca982489597f413638c6af8ccbbe2bfa"
|
||||
}
|
||||
Generated
+12
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n\t\t\t\tDELETE FROM instance_content_entries\n\t\t\t\tWHERE id IN (\n\t\t\t\t\tSELECT target_entry.id\n\t\t\t\t\tFROM instance_content_entries target_entry\n\t\t\t\t\tWHERE target_entry.instance_id = ?\n\t\t\t\t\t\tAND target_entry.file_id = ?\n\t\t\t\t\t\tAND EXISTS (\n\t\t\t\t\t\t\tSELECT 1\n\t\t\t\t\t\t\tFROM instance_content_entries source_entry\n\t\t\t\t\t\t\tWHERE source_entry.instance_id = target_entry.instance_id\n\t\t\t\t\t\t\t\tAND source_entry.content_set_id = target_entry.content_set_id\n\t\t\t\t\t\t\t\tAND source_entry.file_id = ?\n\t\t\t\t\t\t)\n\t\t\t\t)\n\t\t\t\t",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 3
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "edab0097706c394608dbc8c65670159a6d568cb4d408876d874d218d83bc8caf"
|
||||
}
|
||||
@@ -13,8 +13,7 @@ use crate::util::io::{self, IOError};
|
||||
use async_zip::tokio::write::ZipFileWriter;
|
||||
use async_zip::{Compression, ZipEntryBuilder};
|
||||
use path_util::SafeRelativeUtf8UnixPathBuf;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::iter::FromIterator;
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
use tokio::fs::File;
|
||||
use tokio::io::AsyncReadExt;
|
||||
@@ -56,12 +55,12 @@ pub async fn export_mrpack(
|
||||
let version_id = version_id.unwrap_or("1.0.0".to_string());
|
||||
let mut packfile =
|
||||
create_mrpack_json(&metadata, version_id, description).await?;
|
||||
let included_candidates_set = HashSet::<_>::from_iter(
|
||||
included_export_candidates.iter().map(|x| x.as_str()),
|
||||
);
|
||||
packfile
|
||||
.files
|
||||
.retain(|f| included_candidates_set.contains(f.path.as_str()));
|
||||
packfile.files.retain(|f| {
|
||||
is_export_candidate_included(
|
||||
f.path.as_str(),
|
||||
&included_export_candidates,
|
||||
)
|
||||
});
|
||||
|
||||
let mut path_list = Vec::new();
|
||||
add_all_recursive_folder_paths(&instance_base_path, &mut path_list).await?;
|
||||
@@ -80,9 +79,10 @@ pub async fn export_mrpack(
|
||||
let relative_path = pack_get_relative_path(&instance_base_path, &path)?;
|
||||
|
||||
if packfile.files.iter().any(|f| f.path == relative_path)
|
||||
|| !included_candidates_set
|
||||
.iter()
|
||||
.any(|x| relative_path.starts_with(&**x))
|
||||
|| !is_export_candidate_included(
|
||||
relative_path.as_str(),
|
||||
&included_export_candidates,
|
||||
)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
@@ -112,6 +112,18 @@ pub async fn export_mrpack(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn is_export_candidate_included(
|
||||
path: &str,
|
||||
included_export_candidates: &[String],
|
||||
) -> bool {
|
||||
included_export_candidates.iter().any(|candidate| {
|
||||
path == candidate
|
||||
|| path
|
||||
.strip_prefix(candidate)
|
||||
.is_some_and(|suffix| suffix.starts_with('/'))
|
||||
})
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
pub async fn get_pack_export_candidates(
|
||||
instance_id: &str,
|
||||
|
||||
@@ -16,14 +16,11 @@ const CHASE_THE_SKIES_SKIN_PACK_SECTION: &str = "Chase the Skies";
|
||||
const THE_COPPER_AGE_SKIN_PACK_SECTION: &str = "The Copper Age";
|
||||
const MOUNTS_OF_MAYHEM_SKIN_PACK_SECTION: &str = "Mounts of Mayhem";
|
||||
const TINY_TAKEOVER_SKIN_PACK_SECTION: &str = "Tiny Takeover";
|
||||
const CHAOS_CUBED_SKIN_PACK_SECTION: &str = "Chaos Cubed";
|
||||
|
||||
/// A list of bundled Minecraft skins to make available to the user.
|
||||
pub static DEFAULT_SKINS: LazyLock<Vec<Skin>> = LazyLock::new(|| {
|
||||
//
|
||||
// The Mojang skins below are available in the vanilla Minecraft launcher, and were found
|
||||
// by reverse engineering the behavior of the Minecraft launcher. The textures are
|
||||
// publicly available at `https://textures.minecraft.net/texture/<texture_key>`.
|
||||
//
|
||||
// These skins have `local-` prefix in their texture_key as they dont exist on Mojang's API.
|
||||
vec![Skin {
|
||||
texture_key: Arc::from("local-cb3974d16e88a6bd1fe91b0bd58aa5a89abc01f66aed0efc732b84091a523fc9"),
|
||||
name: Some(Arc::from("Mr. Pack - Asexual")),
|
||||
@@ -132,6 +129,11 @@ pub static DEFAULT_SKINS: LazyLock<Vec<Skin>> = LazyLock::new(|| {
|
||||
source: SkinSource::Default,
|
||||
is_equipped: false,
|
||||
},
|
||||
//
|
||||
// The Mojang skins below are available in the vanilla Minecraft launcher, and were found
|
||||
// by reverse engineering the behavior of the Minecraft launcher. The textures are
|
||||
// publicly available at `https://textures.minecraft.net/texture/<texture_key>`.
|
||||
//
|
||||
Skin {
|
||||
texture_key: Arc::from("46acd06e8483b176e8ea39fc12fe105eb3a2a4970f5100057e9d84d4b60bdfa7"),
|
||||
name: Some(Arc::from("Alex")),
|
||||
@@ -782,5 +784,43 @@ pub static DEFAULT_SKINS: LazyLock<Vec<Skin>> = LazyLock::new(|| {
|
||||
).unwrap()),
|
||||
source: SkinSource::Default,
|
||||
is_equipped: false,
|
||||
}]
|
||||
},
|
||||
// Chaos Cubed skin pack
|
||||
Skin {
|
||||
texture_key: Arc::from("fa4d0a00cfabcad04659b991176e2b7872c661d54e85808bc12aa59e10dde326"),
|
||||
name: Some(Arc::from("Brawler")),
|
||||
section: Some(Arc::from(CHAOS_CUBED_SKIN_PACK_SECTION)),
|
||||
variant: MinecraftSkinVariant::Classic,
|
||||
cape_id: None,
|
||||
texture: Arc::from(Url::try_from(
|
||||
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAMAAACdt4HsAAAAbFBMVEUAAADOzs7///+FKypkHSGmPDl0JCXCWlPbxZ7t3rs3Nlbz7nhZFhbd3WW7h0txIyXYqE7TvExwIiWWNDLWlnxmaFqPZU/MsIv5/eFAOzDWb1Ls85Dw96zPhFz0+8Pm5ua/v78pKEkbGjwTECkB9t8tAAAAAXRSTlMAQObYZgAAA2JJREFUeNrtluGaoiAUhkHAUtOsZptxKii9/3vc73Agjdqyp7/7jgd0itcjAiEifWTgP5Gw3iqwXYuUVDAwdwIVmCFYgoeCtt59Itg1TdOa14Jl4E5gTN2ajwQtUpghKAKPMqjN7gPBzgB1L+hfsFYRFgTW6/WWhsV6lqCpiLpp6opovEBriigol1fKW4HWaC9A7SUCwKCJzQaFCO2LwjvKokgMSLSpxQ114zOgt6K3gttDkJmiMBkEtwatyZC25wzakEHph15ZgBIDkQ0Dx6CBqsSESmlPA7xgWIICfH1RuQT4ZxRs6jo8eoSu681m07btpt0IHvzFBBIMgLMQc+iBlPIsAnmuFAcjAicgz849FGTZ+IEK3Av+nBy++CoDCApwL0B7ZJCd/5FBJkVAsyC/EwCXOfl0LmildBAoOs/z1Yri9HtifvlERC4JucGdtSJMjgOXVOUnGKYxFUyW0h4CfBuF1maBig4f9iezNvuxsZ48AhTRABULFgvEjYBv/BsTSDKIAshSQRwRY8Fl2oksiJ2IgiIcHLSakJT1SkSGhNWtgC4RK51JmSHOHpxPHuEmgwt9ezUR5LhUeI3LyDdAJSJKG6A0o9SdAI1JMs40LyhuO1HrawY6vEYKHFGlXUa48AjOTTqRuocyiK8R/aQWC3SYwUGDSMOTnyVBpQPns4jITGaAPvPmHG0VBBSG36QP5yiBJIN3ORyOVszCGNzeiJSjEHMFXKRYOD4THA+HtwTp81PgQG3tK8NisUg6EAmEOFIt3mUvRIc4Hjk68S4dDgS9x4N9IuCBxcHzL5xb9KA9IlCnnVlZW42CXsq+zMpelnR2PX8yZlS1tx3PGWOMlLGdr6/n4glV1VV3c4NBHc7Ff96jB7T2pPuCmZOKBbQCTX/WcelmT2vOAIwbC94XzFtYWECjId0XzFra+gTeDxBc01zgFYFrkXJJoHbT4NnI8bAvrr+yF9APw7gnCDWvB/uOwz4WhOZgwG1xjClQByK6DvFMAAWnwPuBseB1kcL6GFeEVMAMcT8Q9wcixa8Dnd1XyhBqSIj7gbg/EA/BgpBmEPdOvBu48i1eoQllADJDer59EZgjGAit43vgRwibA+fkeYZA8+qouBOd420BIJMTr6Ae41Uxk3RHD28OHmbwF7FfmVsbvSBAAAAAAElFTkSuQmCC"
|
||||
).unwrap()),
|
||||
source: SkinSource::Default,
|
||||
is_equipped: false,
|
||||
},
|
||||
Skin {
|
||||
texture_key: Arc::from("bfa251327ce1fc617dc90879dbfb77dabf151381d5d40f261c7f16e2d147d942"),
|
||||
name: Some(Arc::from("Builder")),
|
||||
section: Some(Arc::from(CHAOS_CUBED_SKIN_PACK_SECTION)),
|
||||
variant: MinecraftSkinVariant::Classic,
|
||||
cape_id: None,
|
||||
texture: Arc::from(Url::try_from(
|
||||
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAMAAACdt4HsAAAAnFBMVEUAAAC7h0vDlUzTvEzYqE7w96rq6ZH8/cj0+8NmP0RxTE7Zp0JAJCQzGhp/VFTo4pL09Z7GlC/z1oDCrFrXvWbd3WVAOzDcyoyPZU/e4JDs85CXYhXYv2CoeWX///+FKypkHSGWS0Lz7ngrtXoqpnIol2koYEckeVcmiGDGb2+qVVPCWlNWMiaASiEnExIaCwpeOyxNKR+RXiynczg4pncGAAAAAXRSTlMAQObYZgAAA6JJREFUeNrNlu16oyoQgEGDmNo9bb5Me7ofbaqimBiB+7+3M4NgXE1Omic/dl8KjLbzOnloEOI5OpSDjOCcLZeMczLi6wLDVitm7hAkUMF9Ag6C5A6B+aqgtUwFbyDg9wgMf3vj5rpgv2+B/X4i4DxZwDBNvAJfr9br9Yrz73yRsO+c28s1Y2u2SGC4KmDL1XK5XjG2YIxzGFbrJdxi0ZJzxpZWoBR2j70+ESWLJFkxwhkHGCdslcCtKMIxSSIv2Csn2o8FEWMsikiPu4ETzr3g+DNo2+DncSwgDQFgQFzYNDjjhBfHplXNUfnlw7g9Nn0jjwSAAXHh4zc7f3vEC/ibVh0bXLwfP3ABj41qm5OB4B/D4MAQMnEGD17AA/G5yq+/Ung9rGD6EYiroK/MAGVZCuKQsgCkDBzEUQP6oDUZYwylQhTFQKDUGUFV1bXRFwT5oIK6E9RTQYUVmPOCPC9PpYr9XtRnBcYczCgZoYhGpJR1LVEgawgAAcDNqq4Aewt/MxIECMwgQD5LIcpPaSmzT2iQUwGY/vHxcUZgEBR8Zn3LB3FWW5xAyrMCreHH5Ya7XY6GPn6Y//Ng2/zBtpGA9gL/9CzcnSqB+CmOn7E/x/HT0/Pz03kBDHnWtfd8l/VxtstUGAS2B0GInXgCGgAo0CAIgi6n/FXmoc//leVh2apQYVeuDyugFLIdRmTQMB+agIYxjm2rWugdEBMPpVgA1RZwlQW0d4GjbeLdjoUaQTwt4tUQFOJ8Oxy054AQD/4LdwghyjyfWx4AmAaxcbh8TTylyBHIL4QADbPMIYexYawNrhGMAEZkSByR+7hfwP6kIEb+JQ4Wk1t52b6mL70gSsmtpNttmt4j2CInwe3rEaVphGm94K9lNuo3Qymhw357BeFmttnMgmAGPQxvL6Lb+Sj1LyIyIIOtl/xponj8vsTN5bdzgT7oLwvwkxaFECdBVeE+RC7DxgKsICcOyK8MVHCToMzz3wUH8/8VxNFrHDNMRgygLVL6N3tVA/KS4CV93b6kIAgsXmAMvstlZ0A+Ln8ncV+ICNEGmAgcUl4UbC1TgZ6PDgaXBJHdF7yAngT+TNCfC66htRV49OBIoDD+miBAKGC0Ow60riu3HWyghxhPBYgxbi217l/bXVejbYGeF1AswFUwwu4HsDHMNuFsFoYBGeMe6FHDYwHSFdd/REMssCFkTiBE2ZEjpT1YGAc6yDVEIVyyNQkNGFR04+QL9R9m6Quz/GRY4QAAAABJRU5ErkJggg=="
|
||||
).unwrap()),
|
||||
source: SkinSource::Default,
|
||||
is_equipped: false,
|
||||
},
|
||||
Skin {
|
||||
texture_key: Arc::from("2d89c01ed54a6fa08d534c07200ccddda426183e642d17cef648d90716f4aa92"),
|
||||
name: Some(Arc::from("Rancher")),
|
||||
section: Some(Arc::from(CHAOS_CUBED_SKIN_PACK_SECTION)),
|
||||
variant: MinecraftSkinVariant::Classic,
|
||||
cape_id: None,
|
||||
texture: Arc::from(Url::try_from(
|
||||
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAMAAACdt4HsAAAAw1BMVEUAAADs85Dcyoze4JDt+sbz97b3+tDq6ZFAOzBSRDdkTj6PZU/n26Xizpnk16TZt4TQoXDXtILIjl69gli2d1DHmGzMpXnSsIHQrX7BjmXOqXzaxJPi1KHdyZfZtIvs5rPewZXgwJbu6rbizZ/jz6Hs4rDjy57n1abWs4np3azl1qXLm3bPo3zdxJfcwpVqt4b7/uzn1qfSmn/TrIT6/eTgtJAmiGAqpnLWpIG+eVrMknFjuIEol2kkeVf09Z7w96z0+8MoxpQlAAAAAXRSTlMAQObYZgAABHJJREFUeNqllgt7mkoQhlkuahJE5KIiqLGxibm1pz3nKKC4+/9/Vb+BhZDNQ63pJ8ss6LzOzl61WrY9nI6Gtu0k8XQeO46mSEgxdmKnE2OaKmfshp47tpNZFE7CSfIBII30/AgYhn7gee5wET5D0aILcKosjKKR6wHgj17/JcC318sBYy/wfXf0+kzaXg4YIgJqwtP25fn5Zft4McCZIXfhzFl9275st3eri5OIbpxTN65Wj5vN42r1m26EGGs7jkZDxybAlKyDZ4DoPT2LM9Lsseu57sy2J9Et+t+2Z64XYjzY49D18VyH3mW1hev5vhcN49vlerm8HQ4jzwuQzKGL1360qH/IFcdTDZh7fhD40WgaBet1MJmOArxAd05dn8bFXHHg8vlUW23uBsvlcjKd//N9vf7+33Qa4TEYj+YzjAsvnNfddlK68WZABvc4DBB7tIhv11AcL6L1ehmEizj0EUkYNwDFDq7J4J5MtrcP93dJ8nD/8+f9Q5Lc3d/fbSeOMwlnYTRzpAPrslqSPG02X7+sVl++/vhR2c3mKUkcJ55P48RBpwsh0PEwJ7LyGe8Hg3o8XEE6pA4azssb06RMKE+LTFP0EVCUElzXDUPXeQPomWaWFbmm6hoyjDYggwohDB0IvRVBDxF0AN5HkEOIAG3ggrUAZQSpEv611FUpAdWAtkySJQvU/vebm6uB1NW16PVETxTHI/xRh2ePJMjTMEwU6z2gCkASKAITAIjyL3omACDgnWFRlqpCkHYPIIIWwDLBIJlcmJYQlkXJF0hmI4I0gGOlohQqcBAWCJZpMWH2AQABIF3RG+BQA1AI0LcEIxcLAKsE9HHR0Njtijc1gMPxAO3hCvf9/iAMAPp9Cx8m+oYA0ECd05dH/Ha32++pdDaBG30ADHzgbBiA9A1cnNz3iJbcqd4ATOoceSMjcLEq2YyhAEZ553ma/g+laZ5nGUZJA6Dfwg/CHXWkW/BqCAOkMybrR9KhzlgrgqpTyrtO4rhkyhmrCokXpQ4oVb7fAIp4KSEYZ1LllOZ5lmdpWpQtyAo0QRWHg3ZGAtK6hEl/HkBXlxgYfwdg/CyAUxM+qyqVotHlAFycOLJ8CoDCqLB3CdeVSodYBwBuBqvGK8YUKt1JFhDdGW5nxkyzcX5Wzcb5J6rC4qwO7zPZZRxF2ssBXDoyVKnUrWgn4Gz3kKO0ojsvHTkpW87rDHxyrKuTSd3uTSjDWqKsC13TWQKwzr3b1rHlN4Bej3cvKM12/waAPwBp/nsAZ+8AahPSdgSCq4Du84KpSJTiUMe6MCiFM4Pc9dWNR05rLm0H4KoJYaBuPDSVa2eqK+uBBFRnhhKgbjzkVK8NVIcbfcdoJSCjy022VqErosUARXSuC8WxBhxKgNQOKkjaOZEXnRMq7Y+73eEAWLOxd88FOR/UJtTuYJWIc7ORq8e5LJcnA3k20FSJypkxadXjnDwWHOqDQdeK1BT1OCePBVLHJolYHgbNmtguuqKM2lCkaZajkn886v8CMs39Yh2w0KoAAAAASUVORK5CYII="
|
||||
).unwrap()),
|
||||
source: SkinSource::Default,
|
||||
is_equipped: false,
|
||||
},
|
||||
]
|
||||
});
|
||||
|
||||
@@ -619,6 +619,83 @@ pub(crate) async fn rename_instance_file(
|
||||
) -> crate::Result<Option<InstanceFile>> {
|
||||
let enabled = i64::from(enabled);
|
||||
let modified_at = Utc::now().timestamp();
|
||||
let mut tx = pool.begin().await?;
|
||||
|
||||
let source_id = sqlx::query_scalar!(
|
||||
"
|
||||
SELECT id
|
||||
FROM instance_files
|
||||
WHERE instance_id = ? AND relative_path = ?
|
||||
",
|
||||
instance_id,
|
||||
old_relative_path,
|
||||
)
|
||||
.fetch_optional(&mut *tx)
|
||||
.await?;
|
||||
let target_id = sqlx::query_scalar!(
|
||||
"
|
||||
SELECT id
|
||||
FROM instance_files
|
||||
WHERE instance_id = ? AND relative_path = ?
|
||||
",
|
||||
instance_id,
|
||||
new_relative_path,
|
||||
)
|
||||
.fetch_optional(&mut *tx)
|
||||
.await?;
|
||||
|
||||
if let (Some(source_id), Some(target_id)) =
|
||||
(source_id.as_deref(), target_id.as_deref())
|
||||
&& source_id != target_id
|
||||
{
|
||||
sqlx::query!(
|
||||
"
|
||||
DELETE FROM instance_content_entries
|
||||
WHERE id IN (
|
||||
SELECT target_entry.id
|
||||
FROM instance_content_entries target_entry
|
||||
WHERE target_entry.instance_id = ?
|
||||
AND target_entry.file_id = ?
|
||||
AND EXISTS (
|
||||
SELECT 1
|
||||
FROM instance_content_entries source_entry
|
||||
WHERE source_entry.instance_id = target_entry.instance_id
|
||||
AND source_entry.content_set_id = target_entry.content_set_id
|
||||
AND source_entry.file_id = ?
|
||||
)
|
||||
)
|
||||
",
|
||||
instance_id,
|
||||
target_id,
|
||||
source_id,
|
||||
)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
|
||||
sqlx::query!(
|
||||
"
|
||||
UPDATE instance_content_entries
|
||||
SET file_id = ?, modified_at = ?
|
||||
WHERE instance_id = ? AND file_id = ?
|
||||
",
|
||||
source_id,
|
||||
modified_at,
|
||||
instance_id,
|
||||
target_id,
|
||||
)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
|
||||
sqlx::query!(
|
||||
"
|
||||
DELETE FROM instance_files
|
||||
WHERE id = ?
|
||||
",
|
||||
target_id,
|
||||
)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
}
|
||||
|
||||
sqlx::query!(
|
||||
"
|
||||
@@ -638,9 +715,11 @@ pub(crate) async fn rename_instance_file(
|
||||
instance_id,
|
||||
old_relative_path,
|
||||
)
|
||||
.execute(pool)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
|
||||
tx.commit().await?;
|
||||
|
||||
get_instance_file_by_relative_path(instance_id, new_relative_path, pool)
|
||||
.await
|
||||
}
|
||||
|
||||
@@ -106,6 +106,7 @@ pub(crate) async fn create_instance(
|
||||
tx.commit().await?;
|
||||
|
||||
crate::state::instances::watcher::watch_instance_folder(
|
||||
&instance.id,
|
||||
&instance.path,
|
||||
&state.file_watcher,
|
||||
&state.directories,
|
||||
|
||||
@@ -209,29 +209,18 @@ pub(crate) async fn list_content(
|
||||
.await?;
|
||||
let imported_modpack_scope = is_imported_modpack_scope(&link);
|
||||
let linked_modpack_source_kind = linked_modpack_source_kind(&link);
|
||||
let mut failed_modpack_identifier_lookup = false;
|
||||
let modpack_ids = if imported_modpack_scope {
|
||||
None
|
||||
} else {
|
||||
match linked_modpack_ids(&link) {
|
||||
Some((_, version_id)) => match get_modpack_identifiers(
|
||||
&version_id,
|
||||
&resolved.content_set,
|
||||
&state.pool,
|
||||
&state.api_semaphore,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(ids) => Some(ids),
|
||||
Err(err) => {
|
||||
tracing::warn!(
|
||||
"Failed to fetch modpack identifiers: {}",
|
||||
err
|
||||
);
|
||||
failed_modpack_identifier_lookup = true;
|
||||
None
|
||||
}
|
||||
},
|
||||
Some((_, version_id)) => {
|
||||
get_cached_modpack_identifiers(
|
||||
&version_id,
|
||||
&state.pool,
|
||||
&state.api_semaphore,
|
||||
)
|
||||
.await?
|
||||
}
|
||||
None => None,
|
||||
}
|
||||
};
|
||||
@@ -243,10 +232,9 @@ pub(crate) async fn list_content(
|
||||
}
|
||||
} else if let Some(ids) = modpack_ids.as_ref() {
|
||||
ContentFilter::ExcludeModpack(ids)
|
||||
} else if failed_modpack_identifier_lookup {
|
||||
} else if let Some(source_kind) = linked_modpack_source_kind {
|
||||
ContentFilter::ExcludeSourceKind {
|
||||
source_kind: linked_modpack_source_kind
|
||||
.unwrap_or(ContentSourceKind::ModrinthModpack),
|
||||
source_kind,
|
||||
exclude_untracked: true,
|
||||
}
|
||||
} else {
|
||||
@@ -1182,6 +1170,28 @@ impl ModpackIdentifiers {
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_cached_modpack_identifiers(
|
||||
version_id: &str,
|
||||
pool: &SqlitePool,
|
||||
fetch_semaphore: &FetchSemaphore,
|
||||
) -> crate::Result<Option<ModpackIdentifiers>> {
|
||||
let Some(cached) =
|
||||
CachedEntry::get_modpack_files(version_id, pool, fetch_semaphore)
|
||||
.await?
|
||||
else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
if cached.project_ids.is_empty() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
Ok(Some(ModpackIdentifiers {
|
||||
hashes: cached.file_hashes.into_iter().collect(),
|
||||
project_ids: cached.project_ids.into_iter().collect(),
|
||||
}))
|
||||
}
|
||||
|
||||
async fn get_modpack_identifiers(
|
||||
version_id: &str,
|
||||
content_set: &ContentSet,
|
||||
|
||||
@@ -7,15 +7,20 @@ use crate::state::{
|
||||
use crate::worlds::WorldType;
|
||||
use notify::{RecommendedWatcher, RecursiveMode};
|
||||
use notify_debouncer_mini::{DebounceEventResult, Debouncer, new_debouncer};
|
||||
use std::time::Duration;
|
||||
use std::{collections::HashMap, sync::Arc, time::Duration};
|
||||
use tokio::sync::{RwLock, mpsc::channel};
|
||||
|
||||
use super::adapters::sqlite::instance_rows;
|
||||
|
||||
pub type FileWatcher = RwLock<Debouncer<RecommendedWatcher>>;
|
||||
pub struct FileWatcher {
|
||||
watcher: RwLock<Debouncer<RecommendedWatcher>>,
|
||||
instance_ids: Arc<RwLock<HashMap<String, String>>>,
|
||||
}
|
||||
|
||||
pub async fn init_watcher() -> crate::Result<FileWatcher> {
|
||||
let (tx, mut rx) = channel(1);
|
||||
let instance_ids = Arc::new(RwLock::new(HashMap::new()));
|
||||
let event_instance_ids = instance_ids.clone();
|
||||
|
||||
let file_watcher = new_debouncer(
|
||||
Duration::from_secs_f32(1.0),
|
||||
@@ -32,88 +37,85 @@ pub async fn init_watcher() -> crate::Result<FileWatcher> {
|
||||
|
||||
match res {
|
||||
Ok(events) => {
|
||||
let instance_ids = event_instance_ids.read().await;
|
||||
let mut visited_instances = Vec::new();
|
||||
|
||||
events.iter().for_each(|e| {
|
||||
let mut instance_path = None;
|
||||
for e in &events {
|
||||
let mut instance_path = None;
|
||||
|
||||
let mut found = false;
|
||||
for component in e.path.components() {
|
||||
if found {
|
||||
instance_path = Some(component.as_os_str());
|
||||
break;
|
||||
}
|
||||
let mut found = false;
|
||||
for component in e.path.components() {
|
||||
if found {
|
||||
instance_path = Some(component.as_os_str());
|
||||
break;
|
||||
}
|
||||
|
||||
if component.as_os_str()
|
||||
== crate::state::dirs::INSTANCES_FOLDER_NAME
|
||||
{
|
||||
found = true;
|
||||
}
|
||||
}
|
||||
if component.as_os_str()
|
||||
== crate::state::dirs::INSTANCES_FOLDER_NAME
|
||||
{
|
||||
found = true;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(instance_path) = instance_path {
|
||||
let instance_path_str =
|
||||
instance_path.to_string_lossy().to_string();
|
||||
let first_file_name = e
|
||||
.path
|
||||
.components()
|
||||
.skip_while(|x| x.as_os_str() != instance_path)
|
||||
.nth(1)
|
||||
.map(|x| x.as_os_str());
|
||||
if first_file_name
|
||||
.as_ref()
|
||||
.is_some_and(|x| *x == "crash-reports")
|
||||
&& e.path
|
||||
.extension()
|
||||
.as_ref()
|
||||
.is_some_and(|x| *x == "txt")
|
||||
{
|
||||
crash_task(instance_path_str);
|
||||
} else if !visited_instances.contains(&instance_path)
|
||||
{
|
||||
let event = if first_file_name
|
||||
.as_ref()
|
||||
.is_some_and(|x| *x == "servers.dat")
|
||||
{
|
||||
Some(InstancePayloadType::ServersUpdated)
|
||||
} else if first_file_name.as_ref().is_some_and(|x| {
|
||||
*x == "saves"
|
||||
&& e.path
|
||||
.file_name()
|
||||
.as_ref()
|
||||
.is_some_and(|x| *x == "level.dat")
|
||||
}) {
|
||||
tracing::info!(
|
||||
"World updated: {}",
|
||||
e.path.display()
|
||||
);
|
||||
let world = e
|
||||
.path
|
||||
.parent()
|
||||
.unwrap()
|
||||
.file_name()
|
||||
.unwrap()
|
||||
.to_string_lossy()
|
||||
.to_string();
|
||||
if !e.path.is_file() {
|
||||
let instance_path_str = instance_path_str.clone();
|
||||
let world = world.clone();
|
||||
tokio::spawn(async move {
|
||||
if let Ok(state) = State::get().await {
|
||||
let instance_id = sqlx::query_scalar!(
|
||||
"
|
||||
SELECT id
|
||||
FROM instances
|
||||
WHERE path = ?
|
||||
",
|
||||
instance_path_str,
|
||||
)
|
||||
.fetch_optional(&state.pool)
|
||||
.await;
|
||||
let Ok(Some(instance_id)) = instance_id else {
|
||||
return;
|
||||
};
|
||||
if let Err(e) = attached_world_data::AttachedWorldData::remove_for_world(
|
||||
if let Some(instance_path) = instance_path {
|
||||
let instance_path_str =
|
||||
instance_path.to_string_lossy().to_string();
|
||||
let Some(instance_id) =
|
||||
instance_ids.get(&instance_path_str).cloned()
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let first_file_name = e
|
||||
.path
|
||||
.components()
|
||||
.skip_while(|x| x.as_os_str() != instance_path)
|
||||
.nth(1)
|
||||
.map(|x| x.as_os_str());
|
||||
if first_file_name
|
||||
.as_ref()
|
||||
.is_some_and(|x| *x == "crash-reports")
|
||||
&& e.path
|
||||
.extension()
|
||||
.as_ref()
|
||||
.is_some_and(|x| *x == "txt")
|
||||
{
|
||||
crash_task(instance_id);
|
||||
} else if !visited_instances.contains(&instance_id)
|
||||
{
|
||||
let event = if first_file_name
|
||||
.as_ref()
|
||||
.is_some_and(|x| *x == "servers.dat")
|
||||
{
|
||||
Some(InstancePayloadType::ServersUpdated)
|
||||
} else if first_file_name.as_ref().is_some_and(
|
||||
|x| {
|
||||
*x == "saves"
|
||||
&& e.path
|
||||
.file_name()
|
||||
.as_ref()
|
||||
.is_some_and(|x| {
|
||||
*x == "level.dat"
|
||||
})
|
||||
},
|
||||
) {
|
||||
tracing::info!(
|
||||
"World updated: {}",
|
||||
e.path.display()
|
||||
);
|
||||
let world = e
|
||||
.path
|
||||
.parent()
|
||||
.unwrap()
|
||||
.file_name()
|
||||
.unwrap()
|
||||
.to_string_lossy()
|
||||
.to_string();
|
||||
if !e.path.is_file() {
|
||||
let instance_id = instance_id.clone();
|
||||
let world = world.clone();
|
||||
tokio::spawn(async move {
|
||||
if let Ok(state) = State::get().await
|
||||
&& let Err(e) = attached_world_data::AttachedWorldData::remove_for_world(
|
||||
&instance_id,
|
||||
WorldType::Singleplayer,
|
||||
&world,
|
||||
@@ -121,68 +123,69 @@ pub async fn init_watcher() -> crate::Result<FileWatcher> {
|
||||
).await {
|
||||
tracing::warn!("Failed to remove AttachedWorldData for '{world}': {e}")
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
Some(InstancePayloadType::WorldUpdated { world })
|
||||
} else if first_file_name
|
||||
.as_ref()
|
||||
.is_none_or(|x| *x != "saves")
|
||||
{
|
||||
Some(InstancePayloadType::Synced)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
if let Some(event) = event {
|
||||
tokio::spawn(async move {
|
||||
let _ = emit_instance(
|
||||
&instance_path_str,
|
||||
event,
|
||||
)
|
||||
.await;
|
||||
});
|
||||
visited_instances.push(instance_path);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
Some(InstancePayloadType::WorldUpdated {
|
||||
world,
|
||||
})
|
||||
} else if first_file_name
|
||||
.as_ref()
|
||||
.is_none_or(|x| *x != "saves")
|
||||
{
|
||||
Some(InstancePayloadType::Synced)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
if let Some(event) = event {
|
||||
let emit_instance_id = instance_id.clone();
|
||||
tokio::spawn(async move {
|
||||
let _ = emit_instance(
|
||||
&emit_instance_id,
|
||||
event,
|
||||
)
|
||||
.await;
|
||||
});
|
||||
visited_instances.push(instance_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(error) => tracing::warn!("Unable to watch file: {error}"),
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Ok(RwLock::new(file_watcher))
|
||||
Ok(FileWatcher {
|
||||
watcher: RwLock::new(file_watcher),
|
||||
instance_ids,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) async fn watch_instances_init(
|
||||
watcher: &FileWatcher,
|
||||
dirs: &DirectoryInfo,
|
||||
pool: &sqlx::SqlitePool,
|
||||
) {
|
||||
let Ok(mut instances_dir) = tokio::fs::read_dir(dirs.instances_dir()).await
|
||||
else {
|
||||
let Ok(instances) = instance_rows::list_instances(pool).await else {
|
||||
return;
|
||||
};
|
||||
|
||||
while let Ok(Some(instance_dir)) = instances_dir.next_entry().await {
|
||||
let file_name = instance_dir.file_name();
|
||||
let file_name = file_name.to_string_lossy();
|
||||
if file_name.starts_with(".DS_Store") {
|
||||
continue;
|
||||
}
|
||||
|
||||
watch_instance_folder(&file_name, watcher, dirs).await;
|
||||
for instance in instances {
|
||||
watch_instance_folder(&instance.id, &instance.path, watcher, dirs)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn watch_instance_folder(
|
||||
instance_id: &str,
|
||||
instance_path: &str,
|
||||
watcher: &FileWatcher,
|
||||
dirs: &DirectoryInfo,
|
||||
) {
|
||||
let instance_path = dirs.instances_dir().join(instance_path);
|
||||
let full_instance_path = dirs.instances_dir().join(instance_path);
|
||||
|
||||
let Ok(metadata) = tokio::fs::metadata(&instance_path).await else {
|
||||
let Ok(metadata) = tokio::fs::metadata(&full_instance_path).await else {
|
||||
return;
|
||||
};
|
||||
|
||||
@@ -195,7 +198,7 @@ pub(crate) async fn watch_instance_folder(
|
||||
.map(|x| x.get_folder())
|
||||
.chain(["crash-reports", "saves"])
|
||||
{
|
||||
let full_path = instance_path.join(sub_path);
|
||||
let full_path = full_instance_path.join(sub_path);
|
||||
|
||||
let meta = tokio::fs::symlink_metadata(&full_path).await;
|
||||
let exists = meta.is_ok();
|
||||
@@ -215,10 +218,11 @@ pub(crate) async fn watch_instance_folder(
|
||||
to_watch.push(full_path);
|
||||
}
|
||||
|
||||
let mut watcher = watcher.write().await;
|
||||
let mut debouncer = watcher.watcher.write().await;
|
||||
for full_path in &to_watch {
|
||||
if let Err(e) =
|
||||
watcher.watcher().watch(full_path, RecursiveMode::Recursive)
|
||||
if let Err(e) = debouncer
|
||||
.watcher()
|
||||
.watch(full_path, RecursiveMode::Recursive)
|
||||
{
|
||||
tracing::error!(
|
||||
"Failed to watch directory for watcher {full_path:?}: {e}"
|
||||
@@ -227,22 +231,29 @@ pub(crate) async fn watch_instance_folder(
|
||||
}
|
||||
}
|
||||
|
||||
if let Err(e) = watcher
|
||||
if let Err(e) = debouncer
|
||||
.watcher()
|
||||
.watch(&instance_path, RecursiveMode::NonRecursive)
|
||||
.watch(&full_instance_path, RecursiveMode::NonRecursive)
|
||||
{
|
||||
tracing::error!(
|
||||
"Failed to watch root instance directory for watcher {instance_path:?}: {e}"
|
||||
"Failed to watch root instance directory for watcher {full_instance_path:?}: {e}"
|
||||
);
|
||||
}
|
||||
|
||||
watcher
|
||||
.instance_ids
|
||||
.write()
|
||||
.await
|
||||
.insert(instance_path.to_string(), instance_id.to_string());
|
||||
}
|
||||
|
||||
fn crash_task(path: String) {
|
||||
fn crash_task(instance_id: String) {
|
||||
tokio::task::spawn(async move {
|
||||
let res = async {
|
||||
let state = State::get().await?;
|
||||
let Some(instance) =
|
||||
instance_rows::get_instance_by_path(&path, &state.pool).await?
|
||||
instance_rows::get_instance_by_id(&instance_id, &state.pool)
|
||||
.await?
|
||||
else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
@@ -107,6 +107,7 @@ impl State {
|
||||
instances::watcher::watch_instances_init(
|
||||
&state.file_watcher,
|
||||
&state.directories,
|
||||
&state.pool,
|
||||
)
|
||||
.await;
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ const MAX_MODERN_STATUS_PACKET_LENGTH: usize =
|
||||
MAX_MINECRAFT_STATUS_STRING_LENGTH + 4;
|
||||
const MAX_LEGACY_STATUS_UTF16_LENGTH: usize =
|
||||
MAX_MINECRAFT_STATUS_STRING_LENGTH;
|
||||
const SERVER_STATUS_TIMEOUT: Duration = Duration::from_secs(5);
|
||||
|
||||
/// Ensures the length of a packet as stated by a server is not longer than a
|
||||
/// hard-coded limit.
|
||||
@@ -87,7 +88,7 @@ pub async fn get_server_status(
|
||||
protocol => modern::status(address, original_address, protocol.map(|v| v.version)).await,
|
||||
}
|
||||
} => res,
|
||||
_ = tokio::time::sleep(Duration::from_secs(30)) => Err(ErrorKind::OtherError(
|
||||
_ = tokio::time::sleep(SERVER_STATUS_TIMEOUT) => Err(ErrorKind::OtherError(
|
||||
format!("Ping of {}:{} timed out", original_address.0, original_address.1)
|
||||
).into())
|
||||
}
|
||||
|
||||
@@ -10,6 +10,55 @@ export type VersionEntry = {
|
||||
}
|
||||
|
||||
const VERSIONS: VersionEntry[] = [
|
||||
{
|
||||
date: `2026-06-29T23:56:37+00:00`,
|
||||
product: 'app',
|
||||
version: '0.15.4',
|
||||
body: `## Fixed
|
||||
- Fixes another issue causing the app to freeze up when going into an instance page.`,
|
||||
},
|
||||
{
|
||||
date: `2026-06-29T22:52:04+00:00`,
|
||||
product: 'app',
|
||||
version: '0.15.3',
|
||||
body: `## Fixed
|
||||
- Fixed hanging on larger legacy instances when visiting the Instance page.`,
|
||||
},
|
||||
{
|
||||
date: `2026-06-29T20:44:35+00:00`,
|
||||
product: 'web',
|
||||
body: `## Changed
|
||||
- Updated analytics graph colors to be more accessible.
|
||||
- Updated project versions table UI and mobile view so columns overflow nicely.
|
||||
- Updates projects page in dashboard to use new table and action bar.
|
||||
- Updated translations`,
|
||||
},
|
||||
{
|
||||
date: `2026-06-29T20:44:35+00:00`,
|
||||
product: 'app',
|
||||
version: '0.15.2',
|
||||
body: `## Added
|
||||
- Added the ["Chaos Cubed"](https://minecraft.wiki/w/Chaos_Cubed_(skin_pack)) official skin pack to the Skin selector page.
|
||||
|
||||
## Changed
|
||||
- Redesigned the modpack export modal to align with the rest of the instance pages.
|
||||
- Updated project versions table UI and mobile view so columns overflow nicely.
|
||||
- Updated translations
|
||||
|
||||
## Fixed
|
||||
- Fixed an issue where sometimes the app would desynchronise from the file system when disabling, enabling or removing mods from the Content tab.
|
||||
- Fixed issue where server pinging in the Worlds tab of the instance page would be stuck in a loading state for too long.
|
||||
- Fixed the Logs page in Modrinth App overflowing past the window instead of keeping the console contained.
|
||||
- Fixed the Modrinth Hosting server panel in Modrinth App overflowing past the window instead of keeping the console contained.
|
||||
- Fixed issue where sometimes disabling linked modpack content would not work. Thanks [@creeperkatze](@creeperkatze)!
|
||||
- Fixed issue where unlinking a locally imported mrpack from an instance causes the content to never show up in the content list.`,
|
||||
},
|
||||
{
|
||||
date: `2026-06-29T20:44:35+00:00`,
|
||||
product: 'hosting',
|
||||
body: `## Changed
|
||||
- Updated translations`,
|
||||
},
|
||||
{
|
||||
date: `2026-06-26T16:33:23+00:00`,
|
||||
product: 'app',
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user