Compare commits
23
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d2c32ed14f | ||
|
|
4b9b9ebf47 | ||
|
|
457dfd8c30 | ||
|
|
8ec5cf104c | ||
|
|
306328367e | ||
|
|
3901749d93 | ||
|
|
cee0c3bbd4 | ||
|
|
63ca20db04 | ||
|
|
0388e339db | ||
|
|
d4554b0f4c | ||
|
|
d5471bb36c | ||
|
|
f238eeae0e | ||
|
|
ae9a762439 | ||
|
|
8480927b99 | ||
|
|
24f5761317 | ||
|
|
3d003f9b31 | ||
|
|
1b487da5c2 | ||
|
|
b310018c38 | ||
|
|
fbde37f5a0 | ||
|
|
e6f9987694 | ||
|
|
5a08a1ed3a | ||
|
|
e51abeb5ad | ||
|
|
4c13f849eb |
No files matched your search
@@ -764,13 +764,13 @@ const messages = defineMessages({
|
|||||||
defaultMessage: 'Library',
|
defaultMessage: 'Library',
|
||||||
},
|
},
|
||||||
multiplayer: {
|
multiplayer: {
|
||||||
screenshots: {
|
|
||||||
id: 'app.navigation.screenshots',
|
|
||||||
defaultMessage: 'Screenshots',
|
|
||||||
},
|
|
||||||
id: 'app.navigation.multiplayer',
|
id: 'app.navigation.multiplayer',
|
||||||
defaultMessage: 'Multiplayer',
|
defaultMessage: 'Multiplayer',
|
||||||
},
|
},
|
||||||
|
screenshots: {
|
||||||
|
id: 'app.navigation.screenshots',
|
||||||
|
defaultMessage: 'Screenshots',
|
||||||
|
},
|
||||||
downloads: {
|
downloads: {
|
||||||
id: 'app.navigation.downloads',
|
id: 'app.navigation.downloads',
|
||||||
defaultMessage: 'Downloads',
|
defaultMessage: 'Downloads',
|
||||||
@@ -1260,7 +1260,7 @@ async function replayOnboarding(mode) {
|
|||||||
|
|
||||||
async function finishOnboarding() {
|
async function finishOnboarding() {
|
||||||
const wasReplay = onboardingReplay.value
|
const wasReplay = onboardingReplay.value
|
||||||
const settings = onboardingSettings.value ?? (await getSettings())
|
const settings = await getSettings()
|
||||||
if (!onboardingReplay.value) {
|
if (!onboardingReplay.value) {
|
||||||
if (onboardingMode.value === 'instance') {
|
if (onboardingMode.value === 'instance') {
|
||||||
settings.onboarding_instance_tour_completed = true
|
settings.onboarding_instance_tour_completed = true
|
||||||
|
|||||||
@@ -32,6 +32,143 @@ export const ANNOUNCEMENT_CHANGE_TYPES: readonly AnnouncementChangeType[] = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
export const launcherAnnouncements: readonly LauncherAnnouncement[] = [
|
export const launcherAnnouncements: readonly LauncherAnnouncement[] = [
|
||||||
|
{
|
||||||
|
id: 'launcher-1.0.1-beta10',
|
||||||
|
version: '1.0.1-beta10',
|
||||||
|
publishedAt: '2026-10-02',
|
||||||
|
title: {
|
||||||
|
'en-US': 'Starlight Launcher 1.0.1-beta10',
|
||||||
|
'zh-CN': 'Starlight Launcher 1.0.1-beta10',
|
||||||
|
},
|
||||||
|
changes: {
|
||||||
|
added: [
|
||||||
|
{
|
||||||
|
'en-US': 'Reworked the instance-page tour into four steps covering content, files, worlds, and logs.',
|
||||||
|
'zh-CN': '重做实例页导览,分为内容、文件、世界、日志四步。',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'en-US': 'When the install location is a drive root, the installer now creates a Starlight subfolder automatically.',
|
||||||
|
'zh-CN': '安装位置选到盘符根目录时,安装器会自动创建 Starlight 子文件夹。',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
fixed: [
|
||||||
|
{
|
||||||
|
'en-US': 'Fixed the launcher UI scale reverting after a restart.',
|
||||||
|
'zh-CN': '修复启动器界面缩放重启后恢复原状的问题。',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'en-US': 'Fixed developer feature flags not saving.',
|
||||||
|
'zh-CN': '修复开发者功能开关无法保存的问题。',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'en-US': 'Fixed instance group edits not saving for instances without an applied content set.',
|
||||||
|
'zh-CN': '修复没有应用内容集的实例无法保存分组修改的问题。',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'en-US': 'Fixed the launcher crashing on startup when feature flags were out of sync between the frontend and backend.',
|
||||||
|
'zh-CN': '修复前后端功能开关不同步导致启动器启动崩溃的问题。',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'en-US': 'Fixed being unable to copy text inside the embedded skin site.',
|
||||||
|
'zh-CN': '修复内嵌皮肤站中无法复制文本的问题。',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'en-US': 'Fixed the onboarding prompt not dismissing when clicking anywhere.',
|
||||||
|
'zh-CN': '修复导览提示点击任意位置无法关闭的问题。',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'launcher-1.0.1-beta9',
|
||||||
|
version: '1.0.1-beta9',
|
||||||
|
publishedAt: '2026-10-02',
|
||||||
|
title: {
|
||||||
|
'en-US': 'Starlight Launcher 1.0.1-beta9',
|
||||||
|
'zh-CN': 'Starlight Launcher 1.0.1-beta9',
|
||||||
|
},
|
||||||
|
changes: {
|
||||||
|
fixed: [
|
||||||
|
{
|
||||||
|
'en-US':
|
||||||
|
'Fixed a crash on the minimal home layout caused by an unrecognized multiplayer feature flag (missing backend enum variant).',
|
||||||
|
'zh-CN': '修复多人联机功能开关缺少后端枚举变体、导致精简主页报错的问题。',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'launcher-1.0.1-beta8',
|
||||||
|
version: '1.0.1-beta8',
|
||||||
|
publishedAt: '2026-10-02',
|
||||||
|
title: {
|
||||||
|
'en-US': 'Starlight Launcher 1.0.1-beta8',
|
||||||
|
'zh-CN': 'Starlight Launcher 1.0.1-beta8',
|
||||||
|
},
|
||||||
|
changes: {
|
||||||
|
fixed: [
|
||||||
|
{
|
||||||
|
'en-US':
|
||||||
|
'Fixed the sidebar navigation entries breaking after the screenshot center was added.',
|
||||||
|
'zh-CN': '修复添加截图中心后侧边栏导航项损坏的问题。',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'en-US':
|
||||||
|
'Fixed being unable to install the StarLight hosted pack even after signing in (missing command permission).',
|
||||||
|
'zh-CN': '修复登录后仍无法安装 StarLight 实例的问题(命令权限缺失)。',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'en-US':
|
||||||
|
'Fixed the launcher UI scaling not applying when changed (missing command permission).',
|
||||||
|
'zh-CN': '修复修改启动器界面缩放后不生效的问题(命令权限缺失)。',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'en-US':
|
||||||
|
'Fixed several screenshot center actions failing due to missing command permissions.',
|
||||||
|
'zh-CN': '修复截图中心部分操作因命令权限缺失而失败的问题。',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'launcher-1.0.1-beta7',
|
||||||
|
version: '1.0.1-beta7',
|
||||||
|
publishedAt: '2026-10-02',
|
||||||
|
title: {
|
||||||
|
'en-US': 'Starlight Launcher 1.0.1-beta7',
|
||||||
|
'zh-CN': 'Starlight Launcher 1.0.1-beta7',
|
||||||
|
},
|
||||||
|
changes: {
|
||||||
|
added: [
|
||||||
|
{
|
||||||
|
'en-US':
|
||||||
|
'Added a screenshot center: browse screenshots across every instance, group them (custom groups, by instance, or by date), preview, edit (crop, rotate, annotate, censor), and export as ZIP.',
|
||||||
|
'zh-CN':
|
||||||
|
'新增截图中心:跨实例浏览截图,支持自定义/按实例/按日期分组,可预览、编辑(裁剪、标注、打码等)并导出为 ZIP。',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'en-US':
|
||||||
|
'Added the multiplayer (Terracotta) entry to the sidebar, behind a feature flag.',
|
||||||
|
'zh-CN': '侧边栏新增多人联机(陶瓦)入口,由功能开关控制。',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
changed: [
|
||||||
|
{
|
||||||
|
'en-US':
|
||||||
|
'Launcher UI scaling is now persisted in the backend and applied via native webview zoom, so it scales embedded pages correctly and keeps onboarding anchors aligned.',
|
||||||
|
'zh-CN':
|
||||||
|
'启动器界面缩放改为后端持久化,并通过原生 webview 缩放应用,可正确缩放内嵌页面且导览锚点不再偏移。',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
fixed: [
|
||||||
|
{
|
||||||
|
'en-US':
|
||||||
|
'Fixed the onboarding tour skipping its first step when the target anchor was missing.',
|
||||||
|
'zh-CN': '修复导览首步在目标锚点缺失时被自动跳过的问题。',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'launcher-1.0.1-beta6',
|
id: 'launcher-1.0.1-beta6',
|
||||||
version: '1.0.1-beta6',
|
version: '1.0.1-beta6',
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ const messages = defineMessages({
|
|||||||
<template>
|
<template>
|
||||||
<iframe
|
<iframe
|
||||||
:src="skinSiteFrameUrl"
|
:src="skinSiteFrameUrl"
|
||||||
|
allow="clipboard-write; clipboard-read"
|
||||||
:title="formatMessage(messages.frameTitle)"
|
:title="formatMessage(messages.frameTitle)"
|
||||||
class="block h-full min-h-0 w-full border-0"
|
class="block h-full min-h-0 w-full border-0"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ import {
|
|||||||
ButtonStyled,
|
ButtonStyled,
|
||||||
commonMessages,
|
commonMessages,
|
||||||
defineMessages,
|
defineMessages,
|
||||||
|
injectNotificationManager,
|
||||||
NewModal,
|
NewModal,
|
||||||
RadioButtons,
|
RadioButtons,
|
||||||
StyledInput,
|
StyledInput,
|
||||||
@@ -72,6 +73,7 @@ import { edit, list } from '@/helpers/instance'
|
|||||||
import type { GameInstance } from '@/helpers/types'
|
import type { GameInstance } from '@/helpers/types'
|
||||||
|
|
||||||
const { formatMessage } = useVIntl()
|
const { formatMessage } = useVIntl()
|
||||||
|
const { handleError } = injectNotificationManager()
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
instanceIds: string[]
|
instanceIds: string[]
|
||||||
@@ -148,7 +150,7 @@ async function deleteGroup(group: string) {
|
|||||||
for (const instance of allInstances.value) {
|
for (const instance of allInstances.value) {
|
||||||
if (instance.groups.includes(group)) {
|
if (instance.groups.includes(group)) {
|
||||||
const newGroups = instance.groups.filter((g) => g !== group)
|
const newGroups = instance.groups.filter((g) => g !== group)
|
||||||
await edit(instance.id, { groups: newGroups }).catch(() => {})
|
await edit(instance.id, { groups: newGroups }).catch(handleError)
|
||||||
instance.groups = newGroups
|
instance.groups = newGroups
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -167,7 +169,7 @@ async function confirm() {
|
|||||||
const groups = selectedGroup.value ? [selectedGroup.value.trim().substring(0, 32)] : []
|
const groups = selectedGroup.value ? [selectedGroup.value.trim().substring(0, 32)] : []
|
||||||
|
|
||||||
for (const instanceId of props.instanceIds) {
|
for (const instanceId of props.instanceIds) {
|
||||||
await edit(instanceId, { groups }).catch(() => {})
|
await edit(instanceId, { groups }).catch(handleError)
|
||||||
}
|
}
|
||||||
|
|
||||||
emit('applied')
|
emit('applied')
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ const progressWidth = computed(() => `${Math.min(100, (props.current / props.tot
|
|||||||
<h2 :id="`onboarding-title-${step.id}`">{{ formatMessage(step.title) }}</h2>
|
<h2 :id="`onboarding-title-${step.id}`">{{ formatMessage(step.title) }}</h2>
|
||||||
</div>
|
</div>
|
||||||
<ButtonStyled circular type="transparent">
|
<ButtonStyled circular type="transparent">
|
||||||
<button :aria-label="formatMessage(onboardingMessages.skip)" @click.stop="$emit('skip')">
|
<button data-onboarding-skip :aria-label="formatMessage(onboardingMessages.skip)" @click.stop="$emit('skip')">
|
||||||
<XIcon />
|
<XIcon />
|
||||||
</button>
|
</button>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
|
|||||||
@@ -252,6 +252,42 @@ export const onboardingMessages = defineMessages({
|
|||||||
id: 'app.onboarding.instance-tabs.description',
|
id: 'app.onboarding.instance-tabs.description',
|
||||||
defaultMessage: 'Use these tabs for content, files, screenshots, worlds, and logs. Tidy chaos.',
|
defaultMessage: 'Use these tabs for content, files, screenshots, worlds, and logs. Tidy chaos.',
|
||||||
},
|
},
|
||||||
|
instanceContentTabTitle: {
|
||||||
|
id: 'app.onboarding.instance-content-tab.title',
|
||||||
|
defaultMessage: 'Content',
|
||||||
|
},
|
||||||
|
instanceContentTabDescription: {
|
||||||
|
id: 'app.onboarding.instance-content-tab.description',
|
||||||
|
defaultMessage:
|
||||||
|
'Manage this instance’s mods, resource packs, and shaders here. Search, enable or disable entries, check for updates, and browse for more.',
|
||||||
|
},
|
||||||
|
instanceFilesTabTitle: {
|
||||||
|
id: 'app.onboarding.instance-files-tab.title',
|
||||||
|
defaultMessage: 'Files',
|
||||||
|
},
|
||||||
|
instanceFilesTabDescription: {
|
||||||
|
id: 'app.onboarding.instance-files-tab.description',
|
||||||
|
defaultMessage:
|
||||||
|
'Browse the instance folder and open configs or logs in the built-in editor without leaving the launcher.',
|
||||||
|
},
|
||||||
|
instanceWorldsTabTitle: {
|
||||||
|
id: 'app.onboarding.instance-worlds-tab.title',
|
||||||
|
defaultMessage: 'Worlds',
|
||||||
|
},
|
||||||
|
instanceWorldsTabDescription: {
|
||||||
|
id: 'app.onboarding.instance-worlds-tab.description',
|
||||||
|
defaultMessage:
|
||||||
|
'Manage saved worlds: back them up, rename, edit world settings, and open the world folder.',
|
||||||
|
},
|
||||||
|
instanceLogsTabTitle: {
|
||||||
|
id: 'app.onboarding.instance-logs-tab.title',
|
||||||
|
defaultMessage: 'Logs',
|
||||||
|
},
|
||||||
|
instanceLogsTabDescription: {
|
||||||
|
id: 'app.onboarding.instance-logs-tab.description',
|
||||||
|
defaultMessage:
|
||||||
|
'Read the instance’s live logs to trace crashes and errors without digging through files.',
|
||||||
|
},
|
||||||
labTitle: {
|
labTitle: {
|
||||||
id: 'app.onboarding.lab.title',
|
id: 'app.onboarding.lab.title',
|
||||||
defaultMessage: 'Useful tools, built in',
|
defaultMessage: 'Useful tools, built in',
|
||||||
@@ -509,15 +545,29 @@ export const onboardingTours: Record<OnboardingMode, OnboardingStep[]> = {
|
|||||||
onboardingMessages.instanceActionsTitle,
|
onboardingMessages.instanceActionsTitle,
|
||||||
onboardingMessages.instanceActionsDescription,
|
onboardingMessages.instanceActionsDescription,
|
||||||
),
|
),
|
||||||
step(
|
inspect(
|
||||||
'instance-tabs',
|
'instance-content-tab',
|
||||||
'inspect',
|
'instance-tab-content',
|
||||||
copy(
|
onboardingMessages.instanceContentTabTitle,
|
||||||
onboardingMessages.instanceTabsTitle,
|
onboardingMessages.instanceContentTabDescription,
|
||||||
onboardingMessages.instanceTabsDescription,
|
),
|
||||||
onboardingMessages.finishArea,
|
inspect(
|
||||||
),
|
'instance-files-tab',
|
||||||
{ targetId: 'instance-tabs' },
|
'instance-tab-files',
|
||||||
|
onboardingMessages.instanceFilesTabTitle,
|
||||||
|
onboardingMessages.instanceFilesTabDescription,
|
||||||
|
),
|
||||||
|
inspect(
|
||||||
|
'instance-worlds-tab',
|
||||||
|
'instance-tab-worlds',
|
||||||
|
onboardingMessages.instanceWorldsTabTitle,
|
||||||
|
onboardingMessages.instanceWorldsTabDescription,
|
||||||
|
),
|
||||||
|
inspect(
|
||||||
|
'instance-logs-tab',
|
||||||
|
'instance-tab-logs',
|
||||||
|
onboardingMessages.instanceLogsTabTitle,
|
||||||
|
onboardingMessages.instanceLogsTabDescription,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -279,8 +279,11 @@ export function useOnboardingTour(
|
|||||||
function handleDocumentClick(event: MouseEvent) {
|
function handleDocumentClick(event: MouseEvent) {
|
||||||
if (!visible.value) return
|
if (!visible.value) return
|
||||||
const clickedElement = event.target instanceof Element ? event.target : null
|
const clickedElement = event.target instanceof Element ? event.target : null
|
||||||
if (clickedElement?.closest('[data-onboarding-overlay-ui]')) return
|
|
||||||
|
|
||||||
|
// 跳过/关闭按钮由其自身处理,不在全局点击里推进
|
||||||
|
if (clickedElement?.closest('[data-onboarding-skip]')) return
|
||||||
|
|
||||||
|
// inspect 步:点击任意位置(含导览气泡本身)都继续下一步
|
||||||
if (step.value.interaction === 'inspect') {
|
if (step.value.interaction === 'inspect') {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
event.stopImmediatePropagation()
|
event.stopImmediatePropagation()
|
||||||
@@ -288,6 +291,9 @@ export function useOnboardingTour(
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 非 inspect 步:点在导览 UI 自身时不处理
|
||||||
|
if (clickedElement?.closest('[data-onboarding-overlay-ui]')) return
|
||||||
|
|
||||||
if (!step.value.targetId || !['navigate', 'activate'].includes(step.value.interaction)) return
|
if (!step.value.targetId || !['navigate', 'activate'].includes(step.value.interaction)) return
|
||||||
if (clickedElement && handleBranchClick(clickedElement)) return
|
if (clickedElement && handleBranchClick(clickedElement)) return
|
||||||
if (!targetElement.value?.contains(event.target as Node)) return
|
if (!targetElement.value?.contains(event.target as Node)) return
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ import {
|
|||||||
import {
|
import {
|
||||||
Avatar,
|
Avatar,
|
||||||
Button,
|
Button,
|
||||||
type ButtonMenuOption,
|
|
||||||
type ComboboxOption,
|
type ComboboxOption,
|
||||||
commonMessages,
|
commonMessages,
|
||||||
ConfirmModal,
|
ConfirmModal,
|
||||||
@@ -1038,7 +1037,6 @@ function activateScreenshot(screenshot: InstanceScreenshot, event: MouseEvent |
|
|||||||
(candidate) => getSelectionKey(candidate) === getSelectionKey(screenshot),
|
(candidate) => getSelectionKey(candidate) === getSelectionKey(screenshot),
|
||||||
)
|
)
|
||||||
if (index >= 0) {
|
if (index >= 0) {
|
||||||
screenshotOptionsMenu.value?.close()
|
|
||||||
imageViewer.value?.show(index)
|
imageViewer.value?.show(index)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1055,44 +1053,34 @@ function requestDelete(screenshot: InstanceScreenshot, fromPreview = false) {
|
|||||||
|
|
||||||
function showScreenshotOptions(screenshot: InstanceScreenshot, event: MouseEvent) {
|
function showScreenshotOptions(screenshot: InstanceScreenshot, event: MouseEvent) {
|
||||||
screenshotOptionsTarget.value = screenshot
|
screenshotOptionsTarget.value = screenshot
|
||||||
const options: ButtonMenuOption[] = [
|
const options = [
|
||||||
...(event.type === 'contextmenu'
|
...(event.type === 'contextmenu' ? [{ name: 'edit' }, { name: 'copy' }] : []),
|
||||||
? [
|
{ name: 'open' },
|
||||||
{
|
{ name: 'go-to-instance' },
|
||||||
id: 'edit',
|
|
||||||
label: formatMessage(messages.edit),
|
|
||||||
icon: EditIcon,
|
|
||||||
action: () => editScreenshot(screenshot),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'copy',
|
|
||||||
label: formatMessage(messages.copy),
|
|
||||||
icon: ClipboardCopyIcon,
|
|
||||||
action: () => void copyScreenshot(screenshot),
|
|
||||||
},
|
|
||||||
]
|
|
||||||
: []),
|
|
||||||
{
|
|
||||||
id: 'open',
|
|
||||||
label: formatMessage(messages.showInFolder),
|
|
||||||
icon: FolderOpenIcon,
|
|
||||||
action: () => void openScreenshot(screenshot),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'go-to-instance',
|
|
||||||
label: formatMessage(messages.goToInstance),
|
|
||||||
action: () => void goToInstance(screenshot),
|
|
||||||
},
|
|
||||||
{ type: 'divider' },
|
{ type: 'divider' },
|
||||||
{
|
{ name: 'delete', color: 'danger' },
|
||||||
id: 'delete',
|
|
||||||
label: formatMessage(commonMessages.deleteLabel),
|
|
||||||
icon: TrashIcon,
|
|
||||||
tone: 'red',
|
|
||||||
action: () => requestDelete(screenshot),
|
|
||||||
},
|
|
||||||
]
|
]
|
||||||
screenshotOptionsMenu.value?.open(event, options)
|
screenshotOptionsMenu.value?.showMenu(event, screenshot, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleScreenshotOptionClicked({ item, option }: { item: InstanceScreenshot; option: string }) {
|
||||||
|
switch (option) {
|
||||||
|
case 'edit':
|
||||||
|
editScreenshot(item)
|
||||||
|
break
|
||||||
|
case 'copy':
|
||||||
|
void copyScreenshot(item)
|
||||||
|
break
|
||||||
|
case 'open':
|
||||||
|
void openScreenshot(item)
|
||||||
|
break
|
||||||
|
case 'go-to-instance':
|
||||||
|
void goToInstance(item)
|
||||||
|
break
|
||||||
|
case 'delete':
|
||||||
|
requestDelete(item)
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function goToInstance(screenshot: InstanceScreenshot) {
|
function goToInstance(screenshot: InstanceScreenshot) {
|
||||||
@@ -1128,7 +1116,6 @@ function editScreenshot(screenshot: InstanceScreenshot) {
|
|||||||
(candidate) => getSelectionKey(candidate) === getSelectionKey(screenshot),
|
(candidate) => getSelectionKey(candidate) === getSelectionKey(screenshot),
|
||||||
)
|
)
|
||||||
if (index >= 0) {
|
if (index >= 0) {
|
||||||
screenshotOptionsMenu.value?.close()
|
|
||||||
void imageViewer.value?.edit(index)
|
void imageViewer.value?.edit(index)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1426,7 +1413,23 @@ onBeforeUnmount(() => {
|
|||||||
:markdown="false"
|
:markdown="false"
|
||||||
@proceed="deleteCustomGroup"
|
@proceed="deleteCustomGroup"
|
||||||
/>
|
/>
|
||||||
<ContextMenu ref="screenshotOptionsMenu" :label="formatMessage(commonMessages.actionsLabel)">
|
<ContextMenu
|
||||||
|
ref="screenshotOptionsMenu"
|
||||||
|
:label="formatMessage(commonMessages.actionsLabel)"
|
||||||
|
@option-clicked="handleScreenshotOptionClicked"
|
||||||
|
>
|
||||||
|
<template #edit>
|
||||||
|
<EditIcon />
|
||||||
|
{{ formatMessage(messages.edit) }}
|
||||||
|
</template>
|
||||||
|
<template #copy>
|
||||||
|
<ClipboardCopyIcon />
|
||||||
|
{{ formatMessage(messages.copy) }}
|
||||||
|
</template>
|
||||||
|
<template #open>
|
||||||
|
<FolderOpenIcon />
|
||||||
|
{{ formatMessage(messages.showInFolder) }}
|
||||||
|
</template>
|
||||||
<template #go-to-instance>
|
<template #go-to-instance>
|
||||||
<Avatar
|
<Avatar
|
||||||
:src="getInstanceIconUrl(screenshotOptionsInstance?.icon_path)"
|
:src="getInstanceIconUrl(screenshotOptionsInstance?.icon_path)"
|
||||||
@@ -1437,6 +1440,10 @@ onBeforeUnmount(() => {
|
|||||||
/>
|
/>
|
||||||
{{ formatMessage(messages.goToInstance) }}
|
{{ formatMessage(messages.goToInstance) }}
|
||||||
</template>
|
</template>
|
||||||
|
<template #delete>
|
||||||
|
<TrashIcon />
|
||||||
|
{{ formatMessage(commonMessages.deleteLabel) }}
|
||||||
|
</template>
|
||||||
</ContextMenu>
|
</ContextMenu>
|
||||||
<ImageViewerEditor
|
<ImageViewerEditor
|
||||||
ref="imageViewer"
|
ref="imageViewer"
|
||||||
|
|||||||
@@ -6008,6 +6008,30 @@
|
|||||||
"app.onboarding.instance-actions.title": {
|
"app.onboarding.instance-actions.title": {
|
||||||
"message": "The main controls"
|
"message": "The main controls"
|
||||||
},
|
},
|
||||||
|
"app.onboarding.instance-content-tab.title": {
|
||||||
|
"message": "Content"
|
||||||
|
},
|
||||||
|
"app.onboarding.instance-content-tab.description": {
|
||||||
|
"message": "Manage this instance’s mods, resource packs, and shaders here. Search, enable or disable entries, check for updates, and browse for more."
|
||||||
|
},
|
||||||
|
"app.onboarding.instance-files-tab.title": {
|
||||||
|
"message": "Files"
|
||||||
|
},
|
||||||
|
"app.onboarding.instance-files-tab.description": {
|
||||||
|
"message": "Browse the instance folder and open configs or logs in the built-in editor without leaving the launcher."
|
||||||
|
},
|
||||||
|
"app.onboarding.instance-worlds-tab.title": {
|
||||||
|
"message": "Worlds"
|
||||||
|
},
|
||||||
|
"app.onboarding.instance-worlds-tab.description": {
|
||||||
|
"message": "Manage saved worlds: back them up, rename, edit world settings, and open the world folder."
|
||||||
|
},
|
||||||
|
"app.onboarding.instance-logs-tab.title": {
|
||||||
|
"message": "Logs"
|
||||||
|
},
|
||||||
|
"app.onboarding.instance-logs-tab.description": {
|
||||||
|
"message": "Read the instance’s live logs to trace crashes and errors without digging through files."
|
||||||
|
},
|
||||||
"app.onboarding.instance-mode.description": {
|
"app.onboarding.instance-mode.description": {
|
||||||
"message": "StarLight automatically installs the administrator-selected modpack and versions, then checks and completes updates before every launch. A StarLight login is required. Choose Local to select your own versions and modpacks for other servers or single-player."
|
"message": "StarLight automatically installs the administrator-selected modpack and versions, then checks and completes updates before every launch. A StarLight login is required. Choose Local to select your own versions and modpacks for other servers or single-player."
|
||||||
},
|
},
|
||||||
@@ -9922,5 +9946,8 @@
|
|||||||
},
|
},
|
||||||
"unknown-pack-warning-modal.warning.title": {
|
"unknown-pack-warning-modal.warning.title": {
|
||||||
"message": "Unknown file warning"
|
"message": "Unknown file warning"
|
||||||
|
},
|
||||||
|
"app.navigation.screenshots": {
|
||||||
|
"message": "Screenshots"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -5995,6 +5995,30 @@
|
|||||||
"app.onboarding.instance-actions.title": {
|
"app.onboarding.instance-actions.title": {
|
||||||
"message": "主操作台"
|
"message": "主操作台"
|
||||||
},
|
},
|
||||||
|
"app.onboarding.instance-content-tab.title": {
|
||||||
|
"message": "内容"
|
||||||
|
},
|
||||||
|
"app.onboarding.instance-content-tab.description": {
|
||||||
|
"message": "在这里管理实例的 Mod、资源包与光影。可搜索、启用或禁用条目、检查更新,也能继续浏览安装新内容。"
|
||||||
|
},
|
||||||
|
"app.onboarding.instance-files-tab.title": {
|
||||||
|
"message": "文件"
|
||||||
|
},
|
||||||
|
"app.onboarding.instance-files-tab.description": {
|
||||||
|
"message": "浏览实例目录,用内置编辑器直接打开配置文件或日志,无需离开启动器。"
|
||||||
|
},
|
||||||
|
"app.onboarding.instance-worlds-tab.title": {
|
||||||
|
"message": "世界"
|
||||||
|
},
|
||||||
|
"app.onboarding.instance-worlds-tab.description": {
|
||||||
|
"message": "管理存档世界:备份、重命名、编辑世界设置,或打开世界所在文件夹。"
|
||||||
|
},
|
||||||
|
"app.onboarding.instance-logs-tab.title": {
|
||||||
|
"message": "日志"
|
||||||
|
},
|
||||||
|
"app.onboarding.instance-logs-tab.description": {
|
||||||
|
"message": "查看实例的实时日志,排查崩溃与报错,不必再翻找文件。"
|
||||||
|
},
|
||||||
"app.onboarding.instance-tabs.description": {
|
"app.onboarding.instance-tabs.description": {
|
||||||
"message": "内容、文件、截图、世界和日志都在这些标签里。乱中有序。"
|
"message": "内容、文件、截图、世界和日志都在这些标签里。乱中有序。"
|
||||||
},
|
},
|
||||||
@@ -10680,5 +10704,8 @@
|
|||||||
},
|
},
|
||||||
"app.starlight-skin.frame-title": {
|
"app.starlight-skin.frame-title": {
|
||||||
"message": "StarLight 皮肤站"
|
"message": "StarLight 皮肤站"
|
||||||
|
},
|
||||||
|
"app.navigation.screenshots": {
|
||||||
|
"message": "截图"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -688,11 +688,13 @@ const tabs = computed(() => [
|
|||||||
label: formatMessage(messages.contentTab),
|
label: formatMessage(messages.contentTab),
|
||||||
href: `${basePath.value}`,
|
href: `${basePath.value}`,
|
||||||
icon: BoxesIcon,
|
icon: BoxesIcon,
|
||||||
|
onboardingId: 'instance-tab-content',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: formatMessage(messages.filesTab),
|
label: formatMessage(messages.filesTab),
|
||||||
href: `${basePath.value}/files`,
|
href: `${basePath.value}/files`,
|
||||||
icon: FolderOpenIcon,
|
icon: FolderOpenIcon,
|
||||||
|
onboardingId: 'instance-tab-files',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: formatMessage(messages.screenshotsTab),
|
label: formatMessage(messages.screenshotsTab),
|
||||||
@@ -703,11 +705,13 @@ const tabs = computed(() => [
|
|||||||
label: formatMessage(messages.worldsTab),
|
label: formatMessage(messages.worldsTab),
|
||||||
href: `${basePath.value}/worlds`,
|
href: `${basePath.value}/worlds`,
|
||||||
icon: GlobeIcon,
|
icon: GlobeIcon,
|
||||||
|
onboardingId: 'instance-tab-worlds',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: formatMessage(messages.logsTab),
|
label: formatMessage(messages.logsTab),
|
||||||
href: `${basePath.value}/logs`,
|
href: `${basePath.value}/logs`,
|
||||||
icon: TerminalSquareIcon,
|
icon: TerminalSquareIcon,
|
||||||
|
onboardingId: 'instance-tab-logs',
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import type { AbstractPopupNotificationManager, AbstractWebNotificationManager }
|
|||||||
import { setupCreationModal } from './setup/creation-modal'
|
import { setupCreationModal } from './setup/creation-modal'
|
||||||
import { setupFileDropProvider } from './setup/file-drop'
|
import { setupFileDropProvider } from './setup/file-drop'
|
||||||
import { setupFilePickerProvider } from './setup/file-picker'
|
import { setupFilePickerProvider } from './setup/file-picker'
|
||||||
|
import { setupImageViewerEditorProvider } from './setup/image-viewer-editor'
|
||||||
import { setupInstanceImportProvider } from './setup/instance-import'
|
import { setupInstanceImportProvider } from './setup/instance-import'
|
||||||
import { setupTagsProvider } from './setup/tags'
|
import { setupTagsProvider } from './setup/tags'
|
||||||
|
|
||||||
@@ -14,6 +15,7 @@ export function setupProviders(
|
|||||||
setupTagsProvider(notificationManager, stateInitialization)
|
setupTagsProvider(notificationManager, stateInitialization)
|
||||||
const fileDrop = setupFileDropProvider()
|
const fileDrop = setupFileDropProvider()
|
||||||
const filePicker = setupFilePickerProvider()
|
const filePicker = setupFilePickerProvider()
|
||||||
|
setupImageViewerEditorProvider()
|
||||||
setupInstanceImportProvider(notificationManager)
|
setupInstanceImportProvider(notificationManager)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import {
|
||||||
|
type ImageViewerEditorData,
|
||||||
|
type ImageViewerEditorSource,
|
||||||
|
provideImageViewerEditor,
|
||||||
|
} from '@modrinth/ui'
|
||||||
|
import { readFile } from '@tauri-apps/plugin-fs'
|
||||||
|
|
||||||
|
export function setupImageViewerEditorProvider() {
|
||||||
|
provideImageViewerEditor({
|
||||||
|
async loadEditorData(source: ImageViewerEditorSource): Promise<ImageViewerEditorData> {
|
||||||
|
return {
|
||||||
|
source: new Blob([await readFile(source.path)], { type: 'image/png' }),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -119,6 +119,10 @@ export default new createRouter({
|
|||||||
pageTransitionGroup: 'multiplayer',
|
pageTransitionGroup: 'multiplayer',
|
||||||
},
|
},
|
||||||
children: [
|
children: [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
redirect: { name: 'MultiplayerRooms' },
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: 'rooms',
|
path: 'rooms',
|
||||||
name: 'MultiplayerRooms',
|
name: 'MultiplayerRooms',
|
||||||
|
|||||||
@@ -365,6 +365,7 @@ fn main() {
|
|||||||
"install_existing_instance",
|
"install_existing_instance",
|
||||||
"hosted_default",
|
"hosted_default",
|
||||||
"hosted_set_session",
|
"hosted_set_session",
|
||||||
|
"hosted_has_game_account",
|
||||||
"hosted_create",
|
"hosted_create",
|
||||||
"hosted_binding",
|
"hosted_binding",
|
||||||
"hosted_sync",
|
"hosted_sync",
|
||||||
@@ -508,6 +509,20 @@ fn main() {
|
|||||||
"instance_edit_icon",
|
"instance_edit_icon",
|
||||||
"instance_export_mrpack",
|
"instance_export_mrpack",
|
||||||
"instance_get_pack_export_candidates",
|
"instance_get_pack_export_candidates",
|
||||||
|
"instance_list_screenshots",
|
||||||
|
"instance_list_all_screenshots",
|
||||||
|
"instance_list_synced_screenshots",
|
||||||
|
"instance_save_edited_screenshot",
|
||||||
|
"instance_list_screenshot_groups",
|
||||||
|
"instance_create_screenshot_group",
|
||||||
|
"instance_rename_screenshot_group",
|
||||||
|
"instance_delete_screenshot_group",
|
||||||
|
"instance_set_screenshot_group_memberships",
|
||||||
|
"instance_import_screenshot_groups",
|
||||||
|
"instance_delete_screenshots",
|
||||||
|
"instance_export_screenshots",
|
||||||
|
"instance_move_screenshots",
|
||||||
|
"instance_open_screenshot",
|
||||||
])
|
])
|
||||||
.default_permission(
|
.default_permission(
|
||||||
DefaultPermissionRule::AllowAllCommands,
|
DefaultPermissionRule::AllowAllCommands,
|
||||||
@@ -524,6 +539,7 @@ fn main() {
|
|||||||
"proxy_get",
|
"proxy_get",
|
||||||
"proxy_set",
|
"proxy_set",
|
||||||
"proxy_test",
|
"proxy_test",
|
||||||
|
"set_ui_scale",
|
||||||
])
|
])
|
||||||
.default_permission(
|
.default_permission(
|
||||||
DefaultPermissionRule::AllowAllCommands,
|
DefaultPermissionRule::AllowAllCommands,
|
||||||
@@ -669,6 +685,7 @@ fn main() {
|
|||||||
"file_save_as",
|
"file_save_as",
|
||||||
"file_read_dragged_file",
|
"file_read_dragged_file",
|
||||||
"screenshot_thumbnail",
|
"screenshot_thumbnail",
|
||||||
|
"instance_icon_thumbnail",
|
||||||
"studio_read_text",
|
"studio_read_text",
|
||||||
"studio_read_binary",
|
"studio_read_binary",
|
||||||
"studio_write_binary",
|
"studio_write_binary",
|
||||||
|
|||||||
@@ -650,6 +650,15 @@ Function PageLeaveOptions
|
|||||||
Abort
|
Abort
|
||||||
${EndIf}
|
${EndIf}
|
||||||
|
|
||||||
|
; 若安装位置被选在盘符根目录(如 E:\),自动在其下创建 Starlight 子目录,
|
||||||
|
; 避免启动器文件直接散落在根目录。用户选的具体子目录不受影响。
|
||||||
|
${GetRoot} "$INSTDIR" $0
|
||||||
|
StrCpy $1 "$0" -1
|
||||||
|
${If} "$INSTDIR" == "$1"
|
||||||
|
StrCpy $INSTDIR "$INSTDIR\Starlight"
|
||||||
|
${NSD_SetText} $InstallDirInput $INSTDIR
|
||||||
|
${EndIf}
|
||||||
|
|
||||||
${NSD_GetState} $DesktopShortcutCheckbox $DesktopShortcutState
|
${NSD_GetState} $DesktopShortcutCheckbox $DesktopShortcutState
|
||||||
|
|
||||||
${If} $FreshInstall <> 1
|
${If} $FreshInstall <> 1
|
||||||
|
|||||||
@@ -478,27 +478,51 @@ fn edit_to_core(edit_instance: EditInstance) -> Result<CoreEditInstance> {
|
|||||||
None => Ok(CoreInstanceLink::Unmanaged),
|
None => Ok(CoreInstanceLink::Unmanaged),
|
||||||
})
|
})
|
||||||
.transpose()?,
|
.transpose()?,
|
||||||
launch_overrides: Some(InstanceLaunchOverridesPatch {
|
launch_overrides: {
|
||||||
player: None,
|
let has_launch_change = edit_instance.java_path.is_some()
|
||||||
instance_mode: None,
|
|| edit_instance.extra_launch_args.is_some()
|
||||||
java_path: edit_instance.java_path,
|
|| edit_instance.custom_env_vars.is_some()
|
||||||
extra_launch_args: edit_instance.extra_launch_args,
|
|| edit_instance.memory.is_some()
|
||||||
custom_env_vars: edit_instance.custom_env_vars,
|
|| edit_instance.force_fullscreen.is_some()
|
||||||
memory: edit_instance.memory,
|
|| edit_instance.maximize_window.is_some()
|
||||||
force_fullscreen: edit_instance.force_fullscreen,
|
|| edit_instance.game_resolution.is_some()
|
||||||
maximize_window: edit_instance.maximize_window,
|
|| edit_instance.launch_preparation_timeout.is_some()
|
||||||
game_resolution: edit_instance.game_resolution,
|
|| edit_instance.hooks.is_some();
|
||||||
launch_preparation_timeout: edit_instance
|
if has_launch_change {
|
||||||
.launch_preparation_timeout,
|
Some(InstanceLaunchOverridesPatch {
|
||||||
hooks: edit_instance.hooks,
|
player: None,
|
||||||
}),
|
instance_mode: None,
|
||||||
content_set_patch: Some(AppliedContentSetPatch {
|
java_path: edit_instance.java_path,
|
||||||
source_kind: None,
|
extra_launch_args: edit_instance.extra_launch_args,
|
||||||
game_version: edit_instance.game_version,
|
custom_env_vars: edit_instance.custom_env_vars,
|
||||||
protocol_version: Some(None),
|
memory: edit_instance.memory,
|
||||||
loader: edit_instance.loader,
|
force_fullscreen: edit_instance.force_fullscreen,
|
||||||
loader_version: edit_instance.loader_version,
|
maximize_window: edit_instance.maximize_window,
|
||||||
}),
|
game_resolution: edit_instance.game_resolution,
|
||||||
|
launch_preparation_timeout: edit_instance
|
||||||
|
.launch_preparation_timeout,
|
||||||
|
hooks: edit_instance.hooks,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
},
|
||||||
|
content_set_patch: {
|
||||||
|
let has_content_change = edit_instance.game_version.is_some()
|
||||||
|
|| edit_instance.loader.is_some()
|
||||||
|
|| edit_instance.loader_version.is_some();
|
||||||
|
if has_content_change {
|
||||||
|
Some(AppliedContentSetPatch {
|
||||||
|
source_kind: None,
|
||||||
|
game_version: edit_instance.game_version,
|
||||||
|
protocol_version: Some(None),
|
||||||
|
loader: edit_instance.loader,
|
||||||
|
loader_version: edit_instance.loader_version,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
},
|
||||||
last_played: None,
|
last_played: None,
|
||||||
submitted_time_played: None,
|
submitted_time_played: None,
|
||||||
recent_time_played: None,
|
recent_time_played: None,
|
||||||
|
|||||||
@@ -181,6 +181,7 @@ pub struct Settings {
|
|||||||
pub migrated: bool,
|
pub migrated: bool,
|
||||||
|
|
||||||
pub developer_mode: bool,
|
pub developer_mode: bool,
|
||||||
|
#[serde(deserialize_with = "deserialize_feature_flags")]
|
||||||
pub feature_flags: HashMap<FeatureFlag, bool>,
|
pub feature_flags: HashMap<FeatureFlag, bool>,
|
||||||
|
|
||||||
pub skipped_update: Option<String>,
|
pub skipped_update: Option<String>,
|
||||||
@@ -212,6 +213,7 @@ pub enum FeatureFlag {
|
|||||||
ServersInApp,
|
ServersInApp,
|
||||||
ServerProjectQa,
|
ServerProjectQa,
|
||||||
I18nDebug,
|
I18nDebug,
|
||||||
|
MultiplayerInApp,
|
||||||
ShowInstancePlayTime,
|
ShowInstancePlayTime,
|
||||||
SkipNonEssentialWarnings,
|
SkipNonEssentialWarnings,
|
||||||
AdvancedFiltersCollapsed,
|
AdvancedFiltersCollapsed,
|
||||||
@@ -221,6 +223,43 @@ pub enum FeatureFlag {
|
|||||||
AutoInstallDependencies,
|
AutoInstallDependencies,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 反序列化 feature flags,遇到未知的 flag 名称时直接忽略,而不是报错。
|
||||||
|
///
|
||||||
|
/// 这样即使前端(或更新的版本)写入了当前后端不认识的 flag,
|
||||||
|
/// 旧版本也能正常读取设置,而不是整包失败导致启动器初始化崩溃。
|
||||||
|
fn deserialize_feature_flags<'de, D>(
|
||||||
|
deserializer: D,
|
||||||
|
) -> Result<HashMap<FeatureFlag, bool>, D::Error>
|
||||||
|
where
|
||||||
|
D: serde::Deserializer<'de>,
|
||||||
|
{
|
||||||
|
let raw = HashMap::<String, bool>::deserialize(deserializer)?;
|
||||||
|
Ok(filter_known_feature_flags(raw))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 从 JSON 字符串解析 feature flags,忽略未知名称。
|
||||||
|
fn parse_feature_flags_json(value: &str) -> HashMap<FeatureFlag, bool> {
|
||||||
|
match serde_json::from_str::<HashMap<String, bool>>(value) {
|
||||||
|
Ok(raw) => filter_known_feature_flags(raw),
|
||||||
|
Err(_) => HashMap::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 只保留能解析为 [`FeatureFlag`] 的条目,未知名称静默丢弃。
|
||||||
|
fn filter_known_feature_flags(
|
||||||
|
raw: HashMap<String, bool>,
|
||||||
|
) -> HashMap<FeatureFlag, bool> {
|
||||||
|
let mut flags = HashMap::with_capacity(raw.len());
|
||||||
|
for (key, value) in raw {
|
||||||
|
if let Ok(flag) = serde_json::from_value::<FeatureFlag>(
|
||||||
|
serde_json::Value::String(key),
|
||||||
|
) {
|
||||||
|
flags.insert(flag, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
flags
|
||||||
|
}
|
||||||
|
|
||||||
impl Settings {
|
impl Settings {
|
||||||
const CURRENT_VERSION: usize = 3;
|
const CURRENT_VERSION: usize = 3;
|
||||||
|
|
||||||
@@ -383,8 +422,8 @@ impl Settings {
|
|||||||
migrated: res.migrated == 1,
|
migrated: res.migrated == 1,
|
||||||
feature_flags: res
|
feature_flags: res
|
||||||
.feature_flags
|
.feature_flags
|
||||||
.as_ref()
|
.as_deref()
|
||||||
.and_then(|x| serde_json::from_str(x).ok())
|
.map(parse_feature_flags_json)
|
||||||
.unwrap_or_default(),
|
.unwrap_or_default(),
|
||||||
skipped_update: res.skipped_update,
|
skipped_update: res.skipped_update,
|
||||||
pending_update_toast_for_version: res
|
pending_update_toast_for_version: res
|
||||||
|
|||||||
Reference in new issue
Block a user