1 parent
146b88afce
commit
a4615c7b73
7 files changed
+492
-11
No files matched your search
@@ -0,0 +1,187 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import {
|
||||||
|
defineMessages,
|
||||||
|
injectNotificationManager,
|
||||||
|
NewModal,
|
||||||
|
StyledInput,
|
||||||
|
useVIntl,
|
||||||
|
} from '@modrinth/ui'
|
||||||
|
import { computed, ref } from 'vue'
|
||||||
|
|
||||||
|
import { type SkinSiteBugIssueType, skinSiteCreateBugIssue } from '@/helpers/skin-site-api'
|
||||||
|
|
||||||
|
const emit = defineEmits<{ (e: 'created', id: number): void }>()
|
||||||
|
|
||||||
|
const { formatMessage } = useVIntl()
|
||||||
|
const { handleError, addNotification } = injectNotificationManager()
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
title: { id: 'app.skin-site.bug-create.title', defaultMessage: 'New issue' },
|
||||||
|
type: { id: 'app.skin-site.bug-create.type', defaultMessage: 'Type' },
|
||||||
|
titleField: { id: 'app.skin-site.bug-create.title-field', defaultMessage: 'Title' },
|
||||||
|
titlePlaceholder: {
|
||||||
|
id: 'app.skin-site.bug-create.title-placeholder',
|
||||||
|
defaultMessage: 'A short summary of the problem',
|
||||||
|
},
|
||||||
|
description: { id: 'app.skin-site.bug-create.description', defaultMessage: 'Description' },
|
||||||
|
descriptionPlaceholder: {
|
||||||
|
id: 'app.skin-site.bug-create.description-placeholder',
|
||||||
|
defaultMessage: 'What happened, and how can it be reproduced?',
|
||||||
|
},
|
||||||
|
privateLabel: { id: 'app.skin-site.bug-create.private', defaultMessage: 'Private issue' },
|
||||||
|
privateHint: {
|
||||||
|
id: 'app.skin-site.bug-create.private-hint',
|
||||||
|
defaultMessage: 'Only you and the maintainers can see it.',
|
||||||
|
},
|
||||||
|
images: { id: 'app.skin-site.bug-create.images', defaultMessage: 'Images' },
|
||||||
|
imagesHint: {
|
||||||
|
id: 'app.skin-site.bug-create.images-hint',
|
||||||
|
defaultMessage: 'Optional, up to 5 images.',
|
||||||
|
},
|
||||||
|
submit: { id: 'app.skin-site.bug-create.submit', defaultMessage: 'Create issue' },
|
||||||
|
submitting: { id: 'app.skin-site.bug-create.submitting', defaultMessage: 'Creating…' },
|
||||||
|
created: { id: 'app.skin-site.bug-create.created', defaultMessage: 'Issue created.' },
|
||||||
|
cancel: { id: 'app.skin-site.bug-create.cancel', defaultMessage: 'Cancel' },
|
||||||
|
typeBug: { id: 'app.skin-site.bugs.type.bug', defaultMessage: 'Bug' },
|
||||||
|
typeFeature: { id: 'app.skin-site.bugs.type.feature', defaultMessage: 'Feature' },
|
||||||
|
typeImprovement: { id: 'app.skin-site.bugs.type.improvement', defaultMessage: 'Improvement' },
|
||||||
|
})
|
||||||
|
|
||||||
|
const modal = ref<InstanceType<typeof NewModal>>()
|
||||||
|
const type = ref<SkinSiteBugIssueType>('BUG')
|
||||||
|
const title = ref('')
|
||||||
|
const description = ref('')
|
||||||
|
const isPrivate = ref(false)
|
||||||
|
const images = ref<File[]>([])
|
||||||
|
const working = ref(false)
|
||||||
|
|
||||||
|
const TYPE_OPTIONS: SkinSiteBugIssueType[] = ['BUG', 'FEATURE', 'IMPROVEMENT']
|
||||||
|
|
||||||
|
function typeLabel(t: SkinSiteBugIssueType): string {
|
||||||
|
if (t === 'FEATURE') return formatMessage(messages.typeFeature)
|
||||||
|
if (t === 'IMPROVEMENT') return formatMessage(messages.typeImprovement)
|
||||||
|
return formatMessage(messages.typeBug)
|
||||||
|
}
|
||||||
|
|
||||||
|
const canSubmit = computed(
|
||||||
|
() => title.value.trim().length > 0 && description.value.trim().length > 0 && !working.value,
|
||||||
|
)
|
||||||
|
|
||||||
|
function reset() {
|
||||||
|
type.value = 'BUG'
|
||||||
|
title.value = ''
|
||||||
|
description.value = ''
|
||||||
|
isPrivate.value = false
|
||||||
|
images.value = []
|
||||||
|
working.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
function show() {
|
||||||
|
reset()
|
||||||
|
modal.value?.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
function onFiles(event: Event) {
|
||||||
|
const input = event.target as HTMLInputElement
|
||||||
|
images.value = Array.from(input.files ?? []).slice(0, 5)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submit() {
|
||||||
|
if (!canSubmit.value) return
|
||||||
|
working.value = true
|
||||||
|
try {
|
||||||
|
const result = await skinSiteCreateBugIssue(
|
||||||
|
{
|
||||||
|
type: type.value,
|
||||||
|
title: title.value.trim(),
|
||||||
|
description: description.value.trim(),
|
||||||
|
private: isPrivate.value,
|
||||||
|
},
|
||||||
|
images.value,
|
||||||
|
)
|
||||||
|
if (!result.success) throw new Error(result.errorMessage)
|
||||||
|
addNotification({ type: 'success', title: formatMessage(messages.created) })
|
||||||
|
modal.value?.hide()
|
||||||
|
emit('created', result.payload.id)
|
||||||
|
} catch (error) {
|
||||||
|
handleError(error)
|
||||||
|
} finally {
|
||||||
|
working.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function cancel() {
|
||||||
|
modal.value?.hide()
|
||||||
|
reset()
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({ show })
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<NewModal ref="modal" :header="formatMessage(messages.title)" @hide="reset">
|
||||||
|
<div class="flex flex-col gap-4">
|
||||||
|
<label class="flex flex-col gap-1">
|
||||||
|
<span class="font-semibold text-contrast">{{ formatMessage(messages.type) }}</span>
|
||||||
|
<select
|
||||||
|
v-model="type"
|
||||||
|
class="rounded-lg border border-solid border-surface-5 bg-bg-raised px-3 py-2 text-sm text-contrast"
|
||||||
|
>
|
||||||
|
<option v-for="t in TYPE_OPTIONS" :key="t" :value="t">{{ typeLabel(t) }}</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="flex flex-col gap-1">
|
||||||
|
<span class="font-semibold text-contrast">{{ formatMessage(messages.titleField) }}</span>
|
||||||
|
<StyledInput
|
||||||
|
v-model="title"
|
||||||
|
type="text"
|
||||||
|
:placeholder="formatMessage(messages.titlePlaceholder)"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="flex flex-col gap-1">
|
||||||
|
<span class="font-semibold text-contrast">{{ formatMessage(messages.description) }}</span>
|
||||||
|
<textarea
|
||||||
|
v-model="description"
|
||||||
|
rows="5"
|
||||||
|
class="w-full resize-y rounded-lg border border-solid border-surface-5 bg-bg-raised px-3 py-2 text-sm text-contrast outline-none focus:border-brand"
|
||||||
|
:placeholder="formatMessage(messages.descriptionPlaceholder)"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="flex items-start gap-2">
|
||||||
|
<input v-model="isPrivate" type="checkbox" class="mt-1" />
|
||||||
|
<span class="flex flex-col">
|
||||||
|
<span class="text-sm font-semibold text-contrast">
|
||||||
|
{{ formatMessage(messages.privateLabel) }}
|
||||||
|
</span>
|
||||||
|
<span class="text-xs text-secondary">{{ formatMessage(messages.privateHint) }}</span>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="flex flex-col gap-1">
|
||||||
|
<span class="font-semibold text-contrast">{{ formatMessage(messages.images) }}</span>
|
||||||
|
<input type="file" accept="image/*" multiple @change="onFiles" />
|
||||||
|
<span class="text-xs text-secondary">{{ formatMessage(messages.imagesHint) }}</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<div class="flex justify-end gap-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="rounded-full bg-button-bg px-4 py-2 text-sm font-semibold text-contrast hover:brightness-110"
|
||||||
|
@click="cancel"
|
||||||
|
>
|
||||||
|
{{ formatMessage(messages.cancel) }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="rounded-full bg-brand px-4 py-2 text-sm font-semibold text-contrast disabled:opacity-50"
|
||||||
|
:disabled="!canSubmit"
|
||||||
|
@click="submit"
|
||||||
|
>
|
||||||
|
{{ working ? formatMessage(messages.submitting) : formatMessage(messages.submit) }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</NewModal>
|
||||||
|
</template>
|
||||||
@@ -27,6 +27,11 @@ const messages = defineMessages({
|
|||||||
all: { id: 'app.skin-site.bugs.all', defaultMessage: 'All' },
|
all: { id: 'app.skin-site.bugs.all', defaultMessage: 'All' },
|
||||||
more: { id: 'app.skin-site.bugs.more', defaultMessage: 'Load more' },
|
more: { id: 'app.skin-site.bugs.more', defaultMessage: 'Load more' },
|
||||||
openOnly: { id: 'app.skin-site.bugs.open-only', defaultMessage: 'Open only' },
|
openOnly: { id: 'app.skin-site.bugs.open-only', defaultMessage: 'Open only' },
|
||||||
|
create: { id: 'app.skin-site.bugs.create', defaultMessage: 'New issue' },
|
||||||
|
notifications: {
|
||||||
|
id: 'app.skin-site.bugs.notifications',
|
||||||
|
defaultMessage: 'Notifications',
|
||||||
|
},
|
||||||
comments: { id: 'app.skin-site.bugs.comments', defaultMessage: '{count} comments' },
|
comments: { id: 'app.skin-site.bugs.comments', defaultMessage: '{count} comments' },
|
||||||
typeBug: { id: 'app.skin-site.bugs.type.bug', defaultMessage: 'Bug' },
|
typeBug: { id: 'app.skin-site.bugs.type.bug', defaultMessage: 'Bug' },
|
||||||
typeFeature: { id: 'app.skin-site.bugs.type.feature', defaultMessage: 'Feature' },
|
typeFeature: { id: 'app.skin-site.bugs.type.feature', defaultMessage: 'Feature' },
|
||||||
@@ -42,7 +47,10 @@ const messages = defineMessages({
|
|||||||
priorityNormal: { id: 'app.skin-site.bugs.priority.normal', defaultMessage: 'Normal' },
|
priorityNormal: { id: 'app.skin-site.bugs.priority.normal', defaultMessage: 'Normal' },
|
||||||
priorityLow: { id: 'app.skin-site.bugs.priority.low', defaultMessage: 'Low' },
|
priorityLow: { id: 'app.skin-site.bugs.priority.low', defaultMessage: 'Low' },
|
||||||
})
|
})
|
||||||
const emit = defineEmits<{ (e: 'back'): void; (e: 'open', id: number): void }>()
|
const emit = defineEmits<{
|
||||||
|
(e: 'back' | 'create' | 'notifications'): void
|
||||||
|
(e: 'open', id: number): void
|
||||||
|
}>()
|
||||||
|
|
||||||
const items = ref<SkinSiteBugIssue[]>([])
|
const items = ref<SkinSiteBugIssue[]>([])
|
||||||
const hasMore = ref(false)
|
const hasMore = ref(false)
|
||||||
@@ -164,9 +172,27 @@ onMounted(() => load(true))
|
|||||||
← {{ formatMessage(messages.back) }}
|
← {{ formatMessage(messages.back) }}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<header class="flex flex-col gap-1">
|
<header class="flex flex-wrap items-start gap-3">
|
||||||
|
<div class="flex flex-col gap-1">
|
||||||
<h1 class="m-0 text-xl font-bold text-contrast">{{ formatMessage(messages.title) }}</h1>
|
<h1 class="m-0 text-xl font-bold text-contrast">{{ formatMessage(messages.title) }}</h1>
|
||||||
<p class="m-0 text-sm text-secondary">{{ formatMessage(messages.subtitle) }}</p>
|
<p class="m-0 text-sm text-secondary">{{ formatMessage(messages.subtitle) }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="ml-auto flex items-center gap-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="rounded-full bg-button-bg px-3 py-1 text-xs font-semibold text-secondary hover:brightness-110"
|
||||||
|
@click="emit('notifications')"
|
||||||
|
>
|
||||||
|
{{ formatMessage(messages.notifications) }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="rounded-full bg-brand px-4 py-2 text-sm font-semibold text-contrast hover:brightness-110"
|
||||||
|
@click="emit('create')"
|
||||||
|
>
|
||||||
|
{{ formatMessage(messages.create) }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div class="flex flex-wrap items-center gap-2">
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
|
|||||||
@@ -0,0 +1,140 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { SpinnerIcon } from '@modrinth/assets'
|
||||||
|
import { defineMessages, injectNotificationManager, useVIntl } from '@modrinth/ui'
|
||||||
|
import { onMounted, ref } from 'vue'
|
||||||
|
|
||||||
|
import {
|
||||||
|
type SkinSiteBugNotification,
|
||||||
|
skinSiteBugNotifications,
|
||||||
|
skinSiteMarkBugNotificationRead,
|
||||||
|
} from '@/helpers/skin-site-api'
|
||||||
|
|
||||||
|
const emit = defineEmits<{ (e: 'back'): void; (e: 'open', id: number): void }>()
|
||||||
|
|
||||||
|
const { formatMessage } = useVIntl()
|
||||||
|
const { handleError } = injectNotificationManager()
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
title: { id: 'app.skin-site.bug-notifications.title', defaultMessage: 'Notifications' },
|
||||||
|
back: { id: 'app.skin-site.bug-notifications.back', defaultMessage: 'Back to home' },
|
||||||
|
subtitle: {
|
||||||
|
id: 'app.skin-site.bug-notifications.subtitle',
|
||||||
|
defaultMessage: 'Activity on issues you reported or follow.',
|
||||||
|
},
|
||||||
|
loading: { id: 'app.skin-site.bug-notifications.loading', defaultMessage: 'Loading…' },
|
||||||
|
failed: {
|
||||||
|
id: 'app.skin-site.bug-notifications.failed',
|
||||||
|
defaultMessage: 'Could not load notifications.',
|
||||||
|
},
|
||||||
|
empty: { id: 'app.skin-site.bug-notifications.empty', defaultMessage: 'No new notifications.' },
|
||||||
|
item: {
|
||||||
|
id: 'app.skin-site.bug-notifications.item',
|
||||||
|
defaultMessage: '{name} updated issue #{id}',
|
||||||
|
},
|
||||||
|
markAll: {
|
||||||
|
id: 'app.skin-site.bug-notifications.mark-all',
|
||||||
|
defaultMessage: 'Mark all as read',
|
||||||
|
},
|
||||||
|
markedAll: {
|
||||||
|
id: 'app.skin-site.bug-notifications.marked-all',
|
||||||
|
defaultMessage: 'All notifications marked as read.',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const items = ref<SkinSiteBugNotification[]>([])
|
||||||
|
const loading = ref(true)
|
||||||
|
const failed = ref(false)
|
||||||
|
const marking = ref(false)
|
||||||
|
|
||||||
|
function formatDateTime(ts: number): string {
|
||||||
|
try {
|
||||||
|
return new Date(ts).toLocaleString()
|
||||||
|
} catch {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function load() {
|
||||||
|
loading.value = true
|
||||||
|
failed.value = false
|
||||||
|
try {
|
||||||
|
const result = await skinSiteBugNotifications()
|
||||||
|
if (!result.success) throw new Error(result.errorMessage)
|
||||||
|
items.value = result.payload ?? []
|
||||||
|
} catch (error) {
|
||||||
|
failed.value = true
|
||||||
|
handleError(error)
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function markAllRead() {
|
||||||
|
if (marking.value || !items.value.length) return
|
||||||
|
marking.value = true
|
||||||
|
try {
|
||||||
|
await skinSiteMarkBugNotificationRead()
|
||||||
|
items.value = []
|
||||||
|
} catch (error) {
|
||||||
|
handleError(error)
|
||||||
|
} finally {
|
||||||
|
marking.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => void load())
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div class="flex flex-col gap-4">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="flex w-fit items-center gap-1 rounded-full bg-button-bg px-3 py-1 text-xs font-semibold text-contrast hover:brightness-110"
|
||||||
|
@click="emit('back')"
|
||||||
|
>
|
||||||
|
← {{ formatMessage(messages.back) }}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<header class="flex flex-wrap items-center gap-3">
|
||||||
|
<div class="flex flex-col gap-1">
|
||||||
|
<h1 class="m-0 text-xl font-bold text-contrast">{{ formatMessage(messages.title) }}</h1>
|
||||||
|
<p class="m-0 text-sm text-secondary">{{ formatMessage(messages.subtitle) }}</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
v-if="items.length"
|
||||||
|
type="button"
|
||||||
|
class="ml-auto rounded-full bg-button-bg px-3 py-1 text-xs font-semibold text-secondary hover:brightness-110 disabled:opacity-50"
|
||||||
|
:disabled="marking"
|
||||||
|
@click="markAllRead"
|
||||||
|
>
|
||||||
|
{{ formatMessage(messages.markAll) }}
|
||||||
|
</button>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div v-if="loading" class="flex items-center gap-2 text-sm text-secondary">
|
||||||
|
<SpinnerIcon class="size-4 animate-spin" />
|
||||||
|
{{ formatMessage(messages.loading) }}
|
||||||
|
</div>
|
||||||
|
<p v-else-if="failed" class="m-0 text-sm text-secondary">
|
||||||
|
{{ formatMessage(messages.failed) }}
|
||||||
|
</p>
|
||||||
|
<p
|
||||||
|
v-else-if="!items.length"
|
||||||
|
class="m-0 rounded-xl border border-solid border-surface-5 bg-bg-raised p-4 text-sm text-secondary"
|
||||||
|
>
|
||||||
|
{{ formatMessage(messages.empty) }}
|
||||||
|
</p>
|
||||||
|
<div v-else class="flex flex-col gap-2">
|
||||||
|
<div
|
||||||
|
v-for="item in items"
|
||||||
|
:key="item.id"
|
||||||
|
class="flex cursor-pointer flex-wrap items-center gap-3 rounded-xl border border-solid border-surface-5 bg-bg-raised p-3 hover:border-brand"
|
||||||
|
@click="emit('open', item.issueId)"
|
||||||
|
>
|
||||||
|
<span class="min-w-0 flex-1 truncate text-sm text-contrast">
|
||||||
|
{{ formatMessage(messages.item, { name: item.actorName, id: item.issueId }) }}
|
||||||
|
</span>
|
||||||
|
<span class="shrink-0 text-xs text-secondary">{{ formatDateTime(item.createdAt) }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -5,8 +5,10 @@ import { ref } from 'vue'
|
|||||||
|
|
||||||
import SkinSiteAccountCard from './SkinSiteAccountCard.vue'
|
import SkinSiteAccountCard from './SkinSiteAccountCard.vue'
|
||||||
import SkinSiteAnnouncements from './SkinSiteAnnouncements.vue'
|
import SkinSiteAnnouncements from './SkinSiteAnnouncements.vue'
|
||||||
|
import SkinSiteBugCreateModal from './SkinSiteBugCreateModal.vue'
|
||||||
import SkinSiteBugDetailView from './SkinSiteBugDetailView.vue'
|
import SkinSiteBugDetailView from './SkinSiteBugDetailView.vue'
|
||||||
import SkinSiteBugListView from './SkinSiteBugListView.vue'
|
import SkinSiteBugListView from './SkinSiteBugListView.vue'
|
||||||
|
import SkinSiteBugNotificationsView from './SkinSiteBugNotificationsView.vue'
|
||||||
import SkinSiteDonationCard from './SkinSiteDonationCard.vue'
|
import SkinSiteDonationCard from './SkinSiteDonationCard.vue'
|
||||||
import SkinSiteDownloadsView from './SkinSiteDownloadsView.vue'
|
import SkinSiteDownloadsView from './SkinSiteDownloadsView.vue'
|
||||||
import SkinSiteLoginModal from './SkinSiteLoginModal.vue'
|
import SkinSiteLoginModal from './SkinSiteLoginModal.vue'
|
||||||
@@ -32,9 +34,9 @@ const messages = defineMessages({
|
|||||||
const YGGDRASIL_URL = 'https://skin.starlight.cool/yggdrasil'
|
const YGGDRASIL_URL = 'https://skin.starlight.cool/yggdrasil'
|
||||||
const loginModal = ref<InstanceType<typeof SkinSiteLoginModal>>()
|
const loginModal = ref<InstanceType<typeof SkinSiteLoginModal>>()
|
||||||
const profileModal = ref<InstanceType<typeof SkinSiteProfileModal>>()
|
const profileModal = ref<InstanceType<typeof SkinSiteProfileModal>>()
|
||||||
const view = ref<'home' | 'players' | 'downloads' | 'map' | 'review' | 'bugs' | 'bug-detail'>(
|
const view = ref<
|
||||||
'home',
|
'home' | 'players' | 'downloads' | 'map' | 'review' | 'bugs' | 'bug-detail' | 'bug-notifications'
|
||||||
)
|
>('home')
|
||||||
const selectedBugId = ref<number | null>(null)
|
const selectedBugId = ref<number | null>(null)
|
||||||
|
|
||||||
function openBug(id: number) {
|
function openBug(id: number) {
|
||||||
@@ -47,6 +49,16 @@ function backToBugList() {
|
|||||||
view.value = 'bugs'
|
view.value = 'bugs'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const bugCreateModal = ref<InstanceType<typeof SkinSiteBugCreateModal>>()
|
||||||
|
|
||||||
|
function openBugNotifications() {
|
||||||
|
view.value = 'bug-notifications'
|
||||||
|
}
|
||||||
|
|
||||||
|
function onBugCreated(id: number) {
|
||||||
|
openBug(id)
|
||||||
|
}
|
||||||
|
|
||||||
async function copyAddress() {
|
async function copyAddress() {
|
||||||
try {
|
try {
|
||||||
await navigator.clipboard.writeText(YGGDRASIL_URL)
|
await navigator.clipboard.writeText(YGGDRASIL_URL)
|
||||||
@@ -64,7 +76,18 @@ async function copyAddress() {
|
|||||||
<SkinSiteDownloadsView v-else-if="view === 'downloads'" @back="view = 'home'" />
|
<SkinSiteDownloadsView v-else-if="view === 'downloads'" @back="view = 'home'" />
|
||||||
<SkinSiteMapView v-else-if="view === 'map'" @back="view = 'home'" />
|
<SkinSiteMapView v-else-if="view === 'map'" @back="view = 'home'" />
|
||||||
<SkinSiteReviewView v-else-if="view === 'review'" @back="view = 'home'" />
|
<SkinSiteReviewView v-else-if="view === 'review'" @back="view = 'home'" />
|
||||||
<SkinSiteBugListView v-else-if="view === 'bugs'" @back="view = 'home'" @open="openBug" />
|
<SkinSiteBugListView
|
||||||
|
v-else-if="view === 'bugs'"
|
||||||
|
@back="view = 'home'"
|
||||||
|
@open="openBug"
|
||||||
|
@create="bugCreateModal?.show()"
|
||||||
|
@notifications="openBugNotifications"
|
||||||
|
/>
|
||||||
|
<SkinSiteBugNotificationsView
|
||||||
|
v-else-if="view === 'bug-notifications'"
|
||||||
|
@back="view = 'bugs'"
|
||||||
|
@open="openBug"
|
||||||
|
/>
|
||||||
<SkinSiteBugDetailView
|
<SkinSiteBugDetailView
|
||||||
v-else-if="view === 'bug-detail' && selectedBugId !== null"
|
v-else-if="view === 'bug-detail' && selectedBugId !== null"
|
||||||
:issue-id="selectedBugId"
|
:issue-id="selectedBugId"
|
||||||
@@ -111,5 +134,6 @@ async function copyAddress() {
|
|||||||
|
|
||||||
<SkinSiteLoginModal ref="loginModal" />
|
<SkinSiteLoginModal ref="loginModal" />
|
||||||
<SkinSiteProfileModal ref="profileModal" />
|
<SkinSiteProfileModal ref="profileModal" />
|
||||||
|
<SkinSiteBugCreateModal ref="bugCreateModal" @created="onBugCreated" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -133,6 +133,8 @@ interface SendOptions {
|
|||||||
method?: 'GET' | 'POST' | 'PUT' | 'DELETE'
|
method?: 'GET' | 'POST' | 'PUT' | 'DELETE'
|
||||||
type?: RequestType
|
type?: RequestType
|
||||||
body?: unknown
|
body?: unknown
|
||||||
|
/** Send a multipart form body (e.g. issue creation with images). */
|
||||||
|
form?: FormData
|
||||||
/** Set to false to skip the Authorization header (e.g. login). */
|
/** Set to false to skip the Authorization header (e.g. login). */
|
||||||
auth?: boolean
|
auth?: boolean
|
||||||
}
|
}
|
||||||
@@ -141,13 +143,14 @@ export async function skinSiteRequest<T>(
|
|||||||
path: string,
|
path: string,
|
||||||
options: SendOptions = {},
|
options: SendOptions = {},
|
||||||
): Promise<SkinSiteResponse<T>> {
|
): Promise<SkinSiteResponse<T>> {
|
||||||
const { method = 'GET', type = 'starlight', body, auth = true } = options
|
const { method = 'GET', type = 'starlight', body, form, auth = true } = options
|
||||||
const url = `${SKIN_SITE_ORIGIN}${REQUEST_TYPE_PATH[type]}/${path.replace(/^\/+/, '')}`
|
const url = `${SKIN_SITE_ORIGIN}${REQUEST_TYPE_PATH[type]}/${path.replace(/^\/+/, '')}`
|
||||||
|
|
||||||
const headers: Record<string, string> = {
|
const headers: Record<string, string> = {
|
||||||
Accept: 'application/json',
|
Accept: 'application/json',
|
||||||
}
|
}
|
||||||
if (body !== undefined) headers['Content-Type'] = 'application/json'
|
if (body !== undefined) headers['Content-Type'] = 'application/json'
|
||||||
|
// FormData sets its own multipart boundary; never set Content-Type for it.
|
||||||
if (auth) {
|
if (auth) {
|
||||||
const token = getSkinSiteToken()
|
const token = getSkinSiteToken()
|
||||||
if (token) headers['Authorization'] = `Bearer ${token}`
|
if (token) headers['Authorization'] = `Bearer ${token}`
|
||||||
@@ -158,7 +161,7 @@ export async function skinSiteRequest<T>(
|
|||||||
response = await tauriFetch(url, {
|
response = await tauriFetch(url, {
|
||||||
method,
|
method,
|
||||||
headers,
|
headers,
|
||||||
body: body !== undefined ? JSON.stringify(body) : undefined,
|
body: form ?? (body !== undefined ? JSON.stringify(body) : undefined),
|
||||||
})
|
})
|
||||||
} catch {
|
} catch {
|
||||||
return handleException(0)
|
return handleException(0)
|
||||||
@@ -750,10 +753,14 @@ export function skinSiteBugComments(
|
|||||||
export function skinSiteCreateBugComment(
|
export function skinSiteCreateBugComment(
|
||||||
issueId: number,
|
issueId: number,
|
||||||
content: string,
|
content: string,
|
||||||
|
images: File[] = [],
|
||||||
): Promise<SkinSiteResponse<SkinSiteBugComment>> {
|
): Promise<SkinSiteResponse<SkinSiteBugComment>> {
|
||||||
|
const form = new FormData()
|
||||||
|
form.append('payload', JSON.stringify({ content }))
|
||||||
|
for (const image of images) form.append('images', image)
|
||||||
return skinSiteRequest<SkinSiteBugComment>(`bug/${issueId}/comments`, {
|
return skinSiteRequest<SkinSiteBugComment>(`bug/${issueId}/comments`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: { content },
|
form,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -768,3 +775,40 @@ export function skinSiteBugUnreadCount(): Promise<SkinSiteResponse<number>> {
|
|||||||
export function skinSiteMarkBugNotificationsRead(): Promise<SkinSiteResponse<string>> {
|
export function skinSiteMarkBugNotificationsRead(): Promise<SkinSiteResponse<string>> {
|
||||||
return skinSiteRequest<string>('bug/notifications/read', { method: 'POST' })
|
return skinSiteRequest<string>('bug/notifications/read', { method: 'POST' })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface SkinSiteBugCreateRequest {
|
||||||
|
type: SkinSiteBugIssueType
|
||||||
|
title: string
|
||||||
|
description: string
|
||||||
|
private?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export function skinSiteCreateBugIssue(
|
||||||
|
request: SkinSiteBugCreateRequest,
|
||||||
|
images: File[] = [],
|
||||||
|
): Promise<SkinSiteResponse<SkinSiteBugIssue>> {
|
||||||
|
const form = new FormData()
|
||||||
|
form.append('payload', JSON.stringify(request))
|
||||||
|
for (const image of images) form.append('images', image)
|
||||||
|
return skinSiteRequest<SkinSiteBugIssue>('bug', { method: 'POST', form })
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SkinSiteBugNotification {
|
||||||
|
id: number
|
||||||
|
issueId: number
|
||||||
|
actorName: string
|
||||||
|
createdAt: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export function skinSiteBugNotifications(): Promise<SkinSiteResponse<SkinSiteBugNotification[]>> {
|
||||||
|
return skinSiteRequest<SkinSiteBugNotification[]>('bug/notifications')
|
||||||
|
}
|
||||||
|
|
||||||
|
export function skinSiteMarkBugNotificationRead(
|
||||||
|
issueId?: number,
|
||||||
|
): Promise<SkinSiteResponse<string>> {
|
||||||
|
return skinSiteRequest<string>('bug/notifications/read', {
|
||||||
|
method: 'POST',
|
||||||
|
body: issueId === undefined ? {} : { issueId },
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -11737,5 +11737,35 @@
|
|||||||
"app.skin-site.bug-detail.comment-deleted": {"message": "Comment deleted."},
|
"app.skin-site.bug-detail.comment-deleted": {"message": "Comment deleted."},
|
||||||
"app.skin-site.bug-detail.delete": {"message": "Delete"},
|
"app.skin-site.bug-detail.delete": {"message": "Delete"},
|
||||||
"app.skin-site.bug-detail.deleting": {"message": "Deleting…"},
|
"app.skin-site.bug-detail.deleting": {"message": "Deleting…"},
|
||||||
"app.skin-site.bug-detail.system": {"message": "System"}
|
"app.skin-site.bug-detail.system": {"message": "System"},
|
||||||
|
"app.skin-site.bug-create.title": {"message": "New issue"},
|
||||||
|
"app.skin-site.bug-create.type": {"message": "Type"},
|
||||||
|
"app.skin-site.bug-create.title-field": {"message": "Title"},
|
||||||
|
"app.skin-site.bug-create.title-placeholder": {"message": "A short summary of the problem"},
|
||||||
|
"app.skin-site.bug-create.description": {"message": "Description"},
|
||||||
|
"app.skin-site.bug-create.description-placeholder": {"message": "What happened, and how can it be reproduced?"},
|
||||||
|
"app.skin-site.bug-create.private": {"message": "Private issue"},
|
||||||
|
"app.skin-site.bug-create.private-hint": {"message": "Only you and the maintainers can see it."},
|
||||||
|
"app.skin-site.bug-create.images": {"message": "Images"},
|
||||||
|
"app.skin-site.bug-create.images-hint": {"message": "Optional, up to 5 images."},
|
||||||
|
"app.skin-site.bug-create.submit": {"message": "Create issue"},
|
||||||
|
"app.skin-site.bug-create.submitting": {"message": "Creating…"},
|
||||||
|
"app.skin-site.bug-create.created": {"message": "Issue created."},
|
||||||
|
"app.skin-site.bug-create.cancel": {"message": "Cancel"},
|
||||||
|
"app.skin-site.bug-notifications.title": {"message": "Notifications"},
|
||||||
|
"app.skin-site.bug-notifications.back": {"message": "Back to home"},
|
||||||
|
"app.skin-site.bug-notifications.subtitle": {"message": "Activity on issues you reported or follow."},
|
||||||
|
"app.skin-site.bug-notifications.loading": {"message": "Loading…"},
|
||||||
|
"app.skin-site.bug-notifications.failed": {"message": "Could not load notifications."},
|
||||||
|
"app.skin-site.bug-notifications.empty": {"message": "No new notifications."},
|
||||||
|
"app.skin-site.bug-notifications.item": {"message": "{name} updated issue #{id}"},
|
||||||
|
"app.skin-site.bug-notifications.mark-all": {"message": "Mark all as read"},
|
||||||
|
"app.skin-site.bug-notifications.marked-all": {"message": "All notifications marked as read."},
|
||||||
|
"app.skin-site.bugs.create": {"message": "New issue"},
|
||||||
|
"app.skin-site.bugs.notifications": {"message": "Notifications"},
|
||||||
|
"app.skin-site.home.title": {"message": "StarLight Skin Site"},
|
||||||
|
"app.skin-site.home.subtitle": {"message": "The wardrobe for StarLight Server players."},
|
||||||
|
"app.skin-site.home.address": {"message": "Skin site address"},
|
||||||
|
"app.skin-site.home.copied": {"message": "Address copied."},
|
||||||
|
"app.skin-site.home.copy": {"message": "Copy"}
|
||||||
}
|
}
|
||||||
@@ -11568,5 +11568,35 @@
|
|||||||
"app.skin-site.bug-detail.comment-deleted": {"message": "评论已删除。"},
|
"app.skin-site.bug-detail.comment-deleted": {"message": "评论已删除。"},
|
||||||
"app.skin-site.bug-detail.delete": {"message": "删除"},
|
"app.skin-site.bug-detail.delete": {"message": "删除"},
|
||||||
"app.skin-site.bug-detail.deleting": {"message": "删除中…"},
|
"app.skin-site.bug-detail.deleting": {"message": "删除中…"},
|
||||||
"app.skin-site.bug-detail.system": {"message": "系统"}
|
"app.skin-site.bug-detail.system": {"message": "系统"},
|
||||||
|
"app.skin-site.bug-create.title": {"message": "新建问题"},
|
||||||
|
"app.skin-site.bug-create.type": {"message": "类型"},
|
||||||
|
"app.skin-site.bug-create.title-field": {"message": "标题"},
|
||||||
|
"app.skin-site.bug-create.title-placeholder": {"message": "简要描述问题"},
|
||||||
|
"app.skin-site.bug-create.description": {"message": "描述"},
|
||||||
|
"app.skin-site.bug-create.description-placeholder": {"message": "发生了什么?如何复现?"},
|
||||||
|
"app.skin-site.bug-create.private": {"message": "私密问题"},
|
||||||
|
"app.skin-site.bug-create.private-hint": {"message": "仅你和维护者可见。"},
|
||||||
|
"app.skin-site.bug-create.images": {"message": "图片"},
|
||||||
|
"app.skin-site.bug-create.images-hint": {"message": "可选,最多 5 张图片。"},
|
||||||
|
"app.skin-site.bug-create.submit": {"message": "创建问题"},
|
||||||
|
"app.skin-site.bug-create.submitting": {"message": "创建中…"},
|
||||||
|
"app.skin-site.bug-create.created": {"message": "问题已创建。"},
|
||||||
|
"app.skin-site.bug-create.cancel": {"message": "取消"},
|
||||||
|
"app.skin-site.bug-notifications.title": {"message": "通知"},
|
||||||
|
"app.skin-site.bug-notifications.back": {"message": "返回主页"},
|
||||||
|
"app.skin-site.bug-notifications.subtitle": {"message": "你提交或关注的问题动态。"},
|
||||||
|
"app.skin-site.bug-notifications.loading": {"message": "加载中…"},
|
||||||
|
"app.skin-site.bug-notifications.failed": {"message": "无法加载通知。"},
|
||||||
|
"app.skin-site.bug-notifications.empty": {"message": "暂无新通知。"},
|
||||||
|
"app.skin-site.bug-notifications.item": {"message": "{name} 更新了问题 #{id}"},
|
||||||
|
"app.skin-site.bug-notifications.mark-all": {"message": "全部标为已读"},
|
||||||
|
"app.skin-site.bug-notifications.marked-all": {"message": "已将全部通知标为已读。"},
|
||||||
|
"app.skin-site.bugs.create": {"message": "新建问题"},
|
||||||
|
"app.skin-site.bugs.notifications": {"message": "通知"},
|
||||||
|
"app.skin-site.home.title": {"message": "星光皮肤站"},
|
||||||
|
"app.skin-site.home.subtitle": {"message": "星光服务器玩家的衣柜。"},
|
||||||
|
"app.skin-site.home.address": {"message": "皮肤站地址"},
|
||||||
|
"app.skin-site.home.copied": {"message": "地址已复制。"},
|
||||||
|
"app.skin-site.home.copy": {"message": "复制"}
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user