Some checks failed
Axolotl desktop CI / guardrails (push) Has been cancelled
Axolotl desktop CI / desktop (macos-latest) (push) Has been cancelled
Axolotl desktop CI / desktop (ubuntu-latest) (push) Has been cancelled
Axolotl desktop CI / desktop (windows-latest) (push) Has been cancelled
Axolotl desktop CI / website (push) Has been cancelled
Repository checks / typos (push) Has been cancelled
Repository checks / tombi (push) Has been cancelled
Rust checks / shear (push) Has been cancelled
Sync source to CNB / Sync Git ref (push) Has been cancelled
958 lines
27 KiB
Vue
958 lines
27 KiB
Vue
<script setup lang="ts">
|
|
import { DatabaseIcon, RefreshCwIcon, XIcon } from '@modrinth/assets'
|
|
import {
|
|
ButtonStyled,
|
|
defineMessages,
|
|
injectNotificationManager,
|
|
NewButton as Button,
|
|
NewModal,
|
|
Toggle,
|
|
useVIntl,
|
|
} from '@modrinth/ui'
|
|
import { getVersion } from '@tauri-apps/api/app'
|
|
import { invoke } from '@tauri-apps/api/core'
|
|
import { fetch as tauriFetch } from '@tauri-apps/plugin-http'
|
|
import { inject, nextTick, ref, watch } from 'vue'
|
|
|
|
import UpdateAnnouncementHistory from '@/components/ui/announcement/UpdateAnnouncementHistory.vue'
|
|
import {
|
|
betaDatabaseExists,
|
|
copyReleaseDatabaseToBeta,
|
|
copyDatabaseBetweenChannels,
|
|
getCurrentAppDatabasePath,
|
|
getUpdateChannel,
|
|
getUpdatePreferences,
|
|
setUpdateChannel,
|
|
setUpdatePreferences,
|
|
type UpdateChannel,
|
|
} from '@/helpers/settings.ts'
|
|
import { isDev, restartApp } from '@/helpers/utils.js'
|
|
import { type AppUpdateCheckResult, checkForAppUpdate } from '@/providers/app-update.ts'
|
|
|
|
import SettingsRow from './SettingsRow.vue'
|
|
import SettingsSection from './SettingsSection.vue'
|
|
|
|
const { formatMessage } = useVIntl()
|
|
const { addNotification, handleError } = injectNotificationManager()
|
|
const [
|
|
activeChannel,
|
|
initialUpdatePreferences,
|
|
currentVersion,
|
|
isDevEnvironment,
|
|
databasePath,
|
|
portable,
|
|
] = await Promise.all([
|
|
getUpdateChannel(),
|
|
getUpdatePreferences(),
|
|
getVersion(),
|
|
isDev(),
|
|
getCurrentAppDatabasePath().catch(() => ''),
|
|
invoke<boolean>('is_portable_mode').catch(() => false),
|
|
])
|
|
const selectedChannel = ref<UpdateChannel>(activeChannel)
|
|
const updatePreferences = ref(initialUpdatePreferences)
|
|
const checking = ref(false)
|
|
const checkResult = ref<AppUpdateCheckResult | 'failed' | 'portable' | null>(null)
|
|
const currentDatabasePath = ref(databasePath)
|
|
const latestChannelVersions = ref<Partial<Record<UpdateChannel, string>>>({})
|
|
const latestChannelVersionsLoaded = ref(false)
|
|
const isPortable = ref(portable)
|
|
const restartModal = ref<InstanceType<typeof NewModal>>()
|
|
const copyDatabaseModal = ref<InstanceType<typeof NewModal>>()
|
|
const databaseOperationModal = ref<InstanceType<typeof NewModal>>()
|
|
const pendingChannel = ref<UpdateChannel | null>(null)
|
|
const databaseOperation = ref<'release-to-beta' | 'beta-to-release' | ''>('')
|
|
const databaseOperationBusy = ref(false)
|
|
let restoringChannelSelection = false
|
|
|
|
const messages = defineMessages({
|
|
title: {
|
|
id: 'app.settings.updates.channel.title',
|
|
defaultMessage: 'Update channel',
|
|
},
|
|
preferencesTitle: {
|
|
id: 'app.settings.updates.preferences.title',
|
|
defaultMessage: 'Update behavior',
|
|
},
|
|
checkTitle: {
|
|
id: 'app.settings.updates.check.title',
|
|
defaultMessage: 'Check for updates',
|
|
},
|
|
description: {
|
|
id: 'app.settings.updates.channel.description',
|
|
defaultMessage: 'Choose which launcher versions Starlight receives.',
|
|
},
|
|
channelLabel: {
|
|
id: 'app.settings.updates.channel.label',
|
|
defaultMessage: 'Channel',
|
|
},
|
|
release: {
|
|
id: 'app.settings.updates.channel.release',
|
|
defaultMessage: 'Release',
|
|
},
|
|
releaseDescription: {
|
|
id: 'app.settings.updates.channel.release-description',
|
|
defaultMessage: 'Stable, tested features and fixes. Updates arrive less often.',
|
|
},
|
|
beta: {
|
|
id: 'app.settings.updates.channel.beta',
|
|
defaultMessage: 'Beta',
|
|
},
|
|
betaDescription: {
|
|
id: 'app.settings.updates.channel.beta-description',
|
|
defaultMessage: 'New, less stable updates with the latest features. Updates arrive more often.',
|
|
},
|
|
betaImmediateFetch: {
|
|
id: 'app.settings.updates.immediate-fetch.beta-description',
|
|
defaultMessage: 'Beta updates are always available immediately.',
|
|
},
|
|
check: {
|
|
id: 'app.settings.updates.check',
|
|
defaultMessage: 'Check for updates',
|
|
},
|
|
checking: {
|
|
id: 'app.settings.updates.checking',
|
|
defaultMessage: 'Checking for updates…',
|
|
},
|
|
available: {
|
|
id: 'app.settings.updates.available',
|
|
defaultMessage: 'An update is available.',
|
|
},
|
|
upToDate: {
|
|
id: 'app.settings.updates.up-to-date',
|
|
defaultMessage: 'Starlight is up to date.',
|
|
},
|
|
disabled: {
|
|
id: 'app.settings.updates.disabled',
|
|
defaultMessage: 'Updates are disabled in this build.',
|
|
},
|
|
offline: {
|
|
id: 'app.settings.updates.offline',
|
|
defaultMessage: 'Connect to the internet to check for updates.',
|
|
},
|
|
failed: {
|
|
id: 'app.settings.updates.failed',
|
|
defaultMessage: 'Could not check for updates.',
|
|
},
|
|
portable: {
|
|
id: 'app.settings.updates.portable',
|
|
defaultMessage:
|
|
'Portable mode cannot update automatically. Please download the latest version manually.',
|
|
},
|
|
security: {
|
|
id: 'app.settings.updates.security',
|
|
defaultMessage: 'Updates are installed only when their cryptographic signature is valid.',
|
|
},
|
|
currentVersion: {
|
|
id: 'app.settings.updates.current-version',
|
|
defaultMessage: 'Current version {version}',
|
|
},
|
|
latestVersion: {
|
|
id: 'app.settings.updates.latest-version',
|
|
defaultMessage: 'Latest {channel} version: {version}',
|
|
},
|
|
latestVersionUnavailable: {
|
|
id: 'app.settings.updates.latest-version-unavailable',
|
|
defaultMessage: 'Latest version unavailable',
|
|
},
|
|
latestVersionLoading: {
|
|
id: 'app.settings.updates.latest-version-loading',
|
|
defaultMessage: 'Fetching latest version…',
|
|
},
|
|
restartTitle: {
|
|
id: 'app.settings.updates.channel.restart-title',
|
|
defaultMessage: 'Restart required',
|
|
},
|
|
restartDescription: {
|
|
id: 'app.settings.updates.channel.restart-description',
|
|
defaultMessage:
|
|
'Restart Starlight now to start using the new update channel, or restart manually later.',
|
|
},
|
|
restartDevelopmentDescription: {
|
|
id: 'app.settings.updates.channel.restart-development-description',
|
|
defaultMessage:
|
|
'The new update channel will be used after you manually restart the development session.',
|
|
},
|
|
restartNow: {
|
|
id: 'app.settings.updates.channel.restart-now',
|
|
defaultMessage: 'Restart now',
|
|
},
|
|
restartLater: {
|
|
id: 'app.settings.updates.channel.restart-later',
|
|
defaultMessage: 'Restart manually later',
|
|
},
|
|
immediateFetch: {
|
|
id: 'app.settings.updates.immediate-fetch',
|
|
defaultMessage: 'Get updates as soon as they are available',
|
|
},
|
|
immediateFetchDescription: {
|
|
id: 'app.settings.updates.immediate-fetch-description',
|
|
defaultMessage:
|
|
'After the latest stable changes and fixes are released, be among the first to receive them.',
|
|
},
|
|
pause: {
|
|
id: 'app.settings.updates.pause',
|
|
defaultMessage: 'Pause updates',
|
|
},
|
|
pauseDescription: {
|
|
id: 'app.settings.updates.pause-description',
|
|
defaultMessage:
|
|
'Stop automatic update checks, downloads, and update notifications until you resume updates.',
|
|
},
|
|
paused: {
|
|
id: 'app.settings.updates.paused',
|
|
defaultMessage: 'Updates are paused.',
|
|
},
|
|
copyDatabaseTitle: {
|
|
id: 'app.settings.updates.channel.copy-database-title',
|
|
defaultMessage: 'Copy Release data to Beta?',
|
|
},
|
|
copyDatabaseDescription: {
|
|
id: 'app.settings.updates.channel.copy-database-description',
|
|
defaultMessage:
|
|
'Would you like to copy your Release database into the Beta channel? This cannot be undone automatically.',
|
|
},
|
|
copyDatabase: {
|
|
id: 'app.settings.updates.channel.copy-database',
|
|
defaultMessage: 'Copy database',
|
|
},
|
|
startEmpty: {
|
|
id: 'app.settings.updates.channel.start-empty',
|
|
defaultMessage: 'Start with empty database',
|
|
},
|
|
databaseIsolationTitle: {
|
|
id: 'app.settings.updates.database-isolation.title',
|
|
defaultMessage: 'Database isolation',
|
|
},
|
|
currentDatabase: {
|
|
id: 'app.settings.updates.database-isolation.current-database',
|
|
defaultMessage: 'Database currently in use',
|
|
},
|
|
databaseIsolationDescription: {
|
|
id: 'app.settings.updates.database-isolation.description',
|
|
defaultMessage:
|
|
'Release and Beta use separate databases, so testing Beta does not change your Release data.',
|
|
},
|
|
releaseDatabase: {
|
|
id: 'app.settings.updates.database-isolation.release',
|
|
defaultMessage: 'Release database',
|
|
},
|
|
betaDatabase: {
|
|
id: 'app.settings.updates.database-isolation.beta',
|
|
defaultMessage: 'Beta database',
|
|
},
|
|
activeDatabase: {
|
|
id: 'app.settings.updates.database-isolation.active',
|
|
defaultMessage: 'Active',
|
|
},
|
|
databaseOperation: {
|
|
id: 'app.settings.updates.database-operation.label',
|
|
defaultMessage: 'Database operation',
|
|
},
|
|
releaseToBeta: {
|
|
id: 'app.settings.updates.database-operation.release-to-beta',
|
|
defaultMessage: 'Copy Release to Beta',
|
|
},
|
|
betaToRelease: {
|
|
id: 'app.settings.updates.database-operation.beta-to-release',
|
|
defaultMessage: 'Copy Beta to Release',
|
|
},
|
|
databaseOperationTitle: {
|
|
id: 'app.settings.updates.database-operation.confirm-title',
|
|
defaultMessage: 'Overwrite database?',
|
|
},
|
|
databaseOperationDescription: {
|
|
id: 'app.settings.updates.database-operation.confirm-description',
|
|
defaultMessage:
|
|
'This will completely replace the inactive {target} database with the contents of the {source} database. This cannot be undone.',
|
|
},
|
|
databaseOperationConfirm: {
|
|
id: 'app.settings.updates.database-operation.confirm',
|
|
defaultMessage: 'Overwrite database',
|
|
},
|
|
databaseOperationActiveTarget: {
|
|
id: 'app.settings.updates.database-operation.active-target',
|
|
defaultMessage:
|
|
'Cannot overwrite the database currently in use. Restart Starlight and switch channels first.',
|
|
},
|
|
databaseOperationFailed: {
|
|
id: 'app.settings.updates.database-operation.failed',
|
|
defaultMessage:
|
|
'The database could not be copied. Please make sure Starlight is not using the target database.',
|
|
},
|
|
databaseOperationSuccess: {
|
|
id: 'app.settings.updates.database-operation.success',
|
|
defaultMessage: '{source} database was copied to {target} successfully.',
|
|
},
|
|
cancel: {
|
|
id: 'app.settings.updates.database-operation.cancel',
|
|
defaultMessage: 'Cancel',
|
|
},
|
|
})
|
|
|
|
async function loadLatestChannelVersions() {
|
|
const versions = await Promise.all(
|
|
(['release', 'beta'] as const).map(async (channel) => {
|
|
try {
|
|
const response = await tauriFetch(
|
|
`https://skin.starlight.cool/starlight/launcher/latest?channel=${channel}`,
|
|
)
|
|
if (!response.ok) return [channel, undefined] as const
|
|
const payload = (await response.json()) as { version?: string }
|
|
return [channel, payload.version] as const
|
|
} catch {
|
|
return [channel, undefined] as const
|
|
}
|
|
}),
|
|
)
|
|
latestChannelVersions.value = Object.fromEntries(versions.filter(([, version]) => version))
|
|
latestChannelVersionsLoaded.value = true
|
|
}
|
|
|
|
void loadLatestChannelVersions()
|
|
|
|
const resultMessages: Record<AppUpdateCheckResult | 'failed' | 'portable', keyof typeof messages> =
|
|
{
|
|
available: 'available',
|
|
'up-to-date': 'upToDate',
|
|
disabled: 'disabled',
|
|
offline: 'offline',
|
|
failed: 'failed',
|
|
portable: 'portable',
|
|
paused: 'paused',
|
|
}
|
|
|
|
watch(selectedChannel, async (channel, previousChannel) => {
|
|
if (restoringChannelSelection) return
|
|
|
|
if (channel === 'beta') updatePreferences.value.immediateUpdateFetch = true
|
|
if (channel === 'beta' && previousChannel === 'release') {
|
|
try {
|
|
if (!(await betaDatabaseExists())) {
|
|
pendingChannel.value = channel
|
|
restoringChannelSelection = true
|
|
selectedChannel.value = previousChannel
|
|
await nextTick()
|
|
restoringChannelSelection = false
|
|
copyDatabaseModal.value?.show()
|
|
return
|
|
}
|
|
} catch (error) {
|
|
restoringChannelSelection = true
|
|
selectedChannel.value = previousChannel
|
|
await nextTick()
|
|
restoringChannelSelection = false
|
|
handleError(error)
|
|
return
|
|
}
|
|
}
|
|
|
|
await applyChannel(channel)
|
|
checkResult.value = null
|
|
})
|
|
|
|
async function applyChannel(channel: UpdateChannel, copyDatabase = false) {
|
|
try {
|
|
if (copyDatabase) await copyReleaseDatabaseToBeta()
|
|
await setUpdateChannel(channel)
|
|
restartModal.value?.show()
|
|
return true
|
|
} catch (error) {
|
|
handleError(error)
|
|
return false
|
|
}
|
|
}
|
|
|
|
async function chooseBetaDatabase(copyDatabase: boolean) {
|
|
copyDatabaseModal.value?.hide()
|
|
const channel = pendingChannel.value
|
|
pendingChannel.value = null
|
|
if (channel && (await applyChannel(channel, copyDatabase))) {
|
|
restoringChannelSelection = true
|
|
selectedChannel.value = channel
|
|
await nextTick()
|
|
restoringChannelSelection = false
|
|
}
|
|
}
|
|
|
|
async function restartForChannelChange() {
|
|
restartModal.value?.hide()
|
|
try {
|
|
await restartApp()
|
|
} catch (error) {
|
|
handleError(error)
|
|
}
|
|
}
|
|
|
|
async function saveUpdatePreferences() {
|
|
try {
|
|
await setUpdatePreferences(updatePreferences.value)
|
|
} catch (error) {
|
|
handleError(error)
|
|
}
|
|
}
|
|
|
|
async function checkForUpdates() {
|
|
checking.value = true
|
|
checkResult.value = null
|
|
|
|
if (isPortable.value) {
|
|
checkResult.value = 'portable'
|
|
checking.value = false
|
|
return
|
|
}
|
|
|
|
try {
|
|
checkResult.value = await checkForAppUpdate()
|
|
} catch (error) {
|
|
checkResult.value = 'failed'
|
|
handleError(error)
|
|
} finally {
|
|
checking.value = false
|
|
}
|
|
}
|
|
|
|
function requestDatabaseOperation() {
|
|
if (!databaseOperation.value) return
|
|
databaseOperationModal.value?.show()
|
|
}
|
|
|
|
function selectDatabaseOperation(operation: 'release-to-beta' | 'beta-to-release') {
|
|
databaseOperation.value = operation
|
|
requestDatabaseOperation()
|
|
}
|
|
|
|
async function confirmDatabaseOperation() {
|
|
if (!databaseOperation.value || databaseOperationBusy.value) return
|
|
const [sourceChannel, targetChannel] =
|
|
databaseOperation.value === 'release-to-beta'
|
|
? (['release', 'beta'] as const)
|
|
: (['beta', 'release'] as const)
|
|
databaseOperationBusy.value = true
|
|
databaseOperationModal.value?.hide()
|
|
try {
|
|
await copyDatabaseBetweenChannels(sourceChannel, targetChannel)
|
|
addNotification({
|
|
type: 'success',
|
|
title: formatMessage(messages.databaseOperationSuccess, {
|
|
source:
|
|
sourceChannel === 'release'
|
|
? formatMessage(messages.releaseDatabase)
|
|
: formatMessage(messages.betaDatabase),
|
|
target:
|
|
targetChannel === 'release'
|
|
? formatMessage(messages.releaseDatabase)
|
|
: formatMessage(messages.betaDatabase),
|
|
}),
|
|
})
|
|
} catch (error) {
|
|
handleError(
|
|
new Error(
|
|
targetChannel === activeChannel
|
|
? formatMessage(messages.databaseOperationActiveTarget)
|
|
: formatMessage(messages.databaseOperationFailed),
|
|
),
|
|
)
|
|
} finally {
|
|
databaseOperationBusy.value = false
|
|
}
|
|
}
|
|
|
|
function cancelDatabaseOperation() {
|
|
databaseOperationModal.value?.hide()
|
|
databaseOperation.value = ''
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<div class="flex flex-col gap-6">
|
|
<SettingsSection :title="formatMessage(messages.title)">
|
|
<div class="update-channel-panel">
|
|
<p id="settings-target-updates-channel" class="m-0 text-sm leading-[1.45] text-secondary">
|
|
{{ formatMessage(messages.description) }}
|
|
</p>
|
|
<div
|
|
class="update-channel-options"
|
|
role="radiogroup"
|
|
:aria-label="formatMessage(messages.channelLabel)"
|
|
>
|
|
<button
|
|
type="button"
|
|
class="update-channel-card"
|
|
:class="{ 'update-channel-card-selected': selectedChannel === 'release' }"
|
|
role="radio"
|
|
:aria-checked="selectedChannel === 'release'"
|
|
@click="selectedChannel = 'release'"
|
|
>
|
|
<span class="update-channel-card-title">{{ formatMessage(messages.release) }}</span>
|
|
<span class="update-channel-card-description">
|
|
{{ formatMessage(messages.releaseDescription) }}
|
|
</span>
|
|
<span class="update-channel-card-version">
|
|
{{
|
|
!latestChannelVersionsLoaded
|
|
? formatMessage(messages.latestVersionLoading)
|
|
: latestChannelVersions.release
|
|
? formatMessage(messages.latestVersion, {
|
|
channel: formatMessage(messages.release),
|
|
version: latestChannelVersions.release,
|
|
})
|
|
: formatMessage(messages.latestVersionUnavailable)
|
|
}}
|
|
</span>
|
|
</button>
|
|
<button
|
|
type="button"
|
|
class="update-channel-card"
|
|
:class="{ 'update-channel-card-selected': selectedChannel === 'beta' }"
|
|
role="radio"
|
|
:aria-checked="selectedChannel === 'beta'"
|
|
@click="selectedChannel = 'beta'"
|
|
>
|
|
<span class="update-channel-card-title">{{ formatMessage(messages.beta) }}</span>
|
|
<span class="update-channel-card-description">
|
|
{{ formatMessage(messages.betaDescription) }}
|
|
</span>
|
|
<span class="update-channel-card-version">
|
|
{{
|
|
!latestChannelVersionsLoaded
|
|
? formatMessage(messages.latestVersionLoading)
|
|
: latestChannelVersions.beta
|
|
? formatMessage(messages.latestVersion, {
|
|
channel: formatMessage(messages.beta),
|
|
version: latestChannelVersions.beta,
|
|
})
|
|
: formatMessage(messages.latestVersionUnavailable)
|
|
}}
|
|
</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div class="database-isolation">
|
|
<div class="database-isolation-copy">
|
|
<h3 class="m-0 text-base font-semibold text-contrast">
|
|
{{ formatMessage(messages.databaseIsolationTitle) }}
|
|
</h3>
|
|
<p class="m-0 text-sm text-secondary">
|
|
{{ formatMessage(messages.databaseIsolationDescription) }}
|
|
</p>
|
|
<div class="database-path">
|
|
<span>{{ formatMessage(messages.currentDatabase) }}</span>
|
|
<code>{{ currentDatabasePath || '—' }}</code>
|
|
</div>
|
|
<div class="database-operation">
|
|
<span>{{ formatMessage(messages.databaseOperation) }}</span>
|
|
<div class="database-operation-buttons">
|
|
<ButtonStyled type="outlined" :disabled="activeChannel === 'beta'">
|
|
<button
|
|
type="button"
|
|
:disabled="activeChannel === 'beta'"
|
|
@click="selectDatabaseOperation('release-to-beta')"
|
|
>
|
|
<DatabaseIcon />
|
|
{{ formatMessage(messages.releaseToBeta) }}
|
|
</button>
|
|
</ButtonStyled>
|
|
<ButtonStyled type="outlined" :disabled="activeChannel === 'release'">
|
|
<button
|
|
type="button"
|
|
:disabled="activeChannel === 'release'"
|
|
@click="selectDatabaseOperation('beta-to-release')"
|
|
>
|
|
<DatabaseIcon />
|
|
{{ formatMessage(messages.betaToRelease) }}
|
|
</button>
|
|
</ButtonStyled>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div
|
|
class="database-diagram"
|
|
:aria-label="formatMessage(messages.databaseIsolationDescription)"
|
|
>
|
|
<div class="database-channel database-channel-release">
|
|
<span>R</span>
|
|
<strong>{{ formatMessage(messages.releaseDatabase) }}</strong>
|
|
<small>{{
|
|
activeChannel === 'release' ? formatMessage(messages.activeDatabase) : '\u00a0'
|
|
}}</small>
|
|
</div>
|
|
<div class="database-branch" aria-hidden="true"></div>
|
|
<div class="database-channel database-channel-beta">
|
|
<span>B</span>
|
|
<strong>{{ formatMessage(messages.betaDatabase) }}</strong>
|
|
<small>{{
|
|
activeChannel === 'beta' ? formatMessage(messages.activeDatabase) : '\u00a0'
|
|
}}</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</SettingsSection>
|
|
|
|
<SettingsSection :title="formatMessage(messages.preferencesTitle)">
|
|
<SettingsRow>
|
|
<template #label>{{ formatMessage(messages.immediateFetch) }}</template>
|
|
<template #description>
|
|
{{
|
|
selectedChannel === 'beta'
|
|
? formatMessage(messages.betaImmediateFetch)
|
|
: formatMessage(messages.immediateFetchDescription)
|
|
}}
|
|
</template>
|
|
<template #control>
|
|
<Toggle
|
|
id="immediate-update-fetch"
|
|
v-model="updatePreferences.immediateUpdateFetch"
|
|
:disabled="selectedChannel === 'beta'"
|
|
@update:model-value="saveUpdatePreferences"
|
|
/>
|
|
</template>
|
|
</SettingsRow>
|
|
<SettingsRow>
|
|
<template #label>{{ formatMessage(messages.pause) }}</template>
|
|
<template #description>{{ formatMessage(messages.pauseDescription) }}</template>
|
|
<template #control>
|
|
<Toggle
|
|
id="pause-updates"
|
|
v-model="updatePreferences.updatesPaused"
|
|
@update:model-value="saveUpdatePreferences"
|
|
/>
|
|
</template>
|
|
</SettingsRow>
|
|
</SettingsSection>
|
|
|
|
<SettingsSection :title="formatMessage(messages.checkTitle)">
|
|
<div class="update-check-panel">
|
|
<div class="update-check-heading">
|
|
<p class="m-0 text-sm text-secondary">
|
|
{{ formatMessage(messages.currentVersion, { version: currentVersion }) }}
|
|
</p>
|
|
<p class="m-0 text-sm text-secondary">{{ formatMessage(messages.security) }}</p>
|
|
</div>
|
|
<div class="flex flex-wrap gap-2">
|
|
<Button type="colored" color="brand" :disabled="checking" @click="checkForUpdates">
|
|
<RefreshCwIcon :class="{ 'animate-spin': checking }" />
|
|
{{ formatMessage(checking ? messages.checking : messages.check) }}
|
|
</Button>
|
|
</div>
|
|
<p
|
|
v-if="checkResult"
|
|
class="update-check-result"
|
|
:class="`update-check-result-${checkResult}`"
|
|
role="status"
|
|
>
|
|
{{ formatMessage(messages[resultMessages[checkResult]]) }}
|
|
</p>
|
|
</div>
|
|
</SettingsSection>
|
|
|
|
<UpdateAnnouncementHistory :current-version="currentVersion" />
|
|
</div>
|
|
|
|
<NewModal ref="restartModal" :header="formatMessage(messages.restartTitle)" :closable="false">
|
|
<p class="m-0">
|
|
{{
|
|
formatMessage(
|
|
isDevEnvironment ? messages.restartDevelopmentDescription : messages.restartDescription,
|
|
)
|
|
}}
|
|
</p>
|
|
<template #actions>
|
|
<div class="flex justify-end gap-2">
|
|
<ButtonStyled type="outlined">
|
|
<button type="button" @click="restartModal?.hide()">
|
|
<XIcon />
|
|
{{ formatMessage(messages.restartLater) }}
|
|
</button>
|
|
</ButtonStyled>
|
|
<ButtonStyled v-if="!isDevEnvironment" color="brand">
|
|
<button type="button" @click="restartForChannelChange">
|
|
<RefreshCwIcon />
|
|
{{ formatMessage(messages.restartNow) }}
|
|
</button>
|
|
</ButtonStyled>
|
|
</div>
|
|
</template>
|
|
</NewModal>
|
|
|
|
<NewModal
|
|
ref="copyDatabaseModal"
|
|
:header="formatMessage(messages.copyDatabaseTitle)"
|
|
:closable="false"
|
|
>
|
|
<p class="m-0">{{ formatMessage(messages.copyDatabaseDescription) }}</p>
|
|
<template #actions>
|
|
<div class="flex justify-end gap-2">
|
|
<ButtonStyled type="outlined">
|
|
<button type="button" @click="chooseBetaDatabase(false)">
|
|
<XIcon />
|
|
{{ formatMessage(messages.startEmpty) }}
|
|
</button>
|
|
</ButtonStyled>
|
|
<ButtonStyled color="brand">
|
|
<button type="button" @click="chooseBetaDatabase(true)">
|
|
<RefreshCwIcon />
|
|
{{ formatMessage(messages.copyDatabase) }}
|
|
</button>
|
|
</ButtonStyled>
|
|
</div>
|
|
</template>
|
|
</NewModal>
|
|
|
|
<NewModal
|
|
ref="databaseOperationModal"
|
|
:header="formatMessage(messages.databaseOperationTitle)"
|
|
:closable="false"
|
|
>
|
|
<p class="m-0">
|
|
{{
|
|
formatMessage(messages.databaseOperationDescription, {
|
|
source:
|
|
databaseOperation === 'release-to-beta'
|
|
? formatMessage(messages.releaseDatabase)
|
|
: formatMessage(messages.betaDatabase),
|
|
target:
|
|
databaseOperation === 'release-to-beta'
|
|
? formatMessage(messages.betaDatabase)
|
|
: formatMessage(messages.releaseDatabase),
|
|
})
|
|
}}
|
|
</p>
|
|
<template #actions>
|
|
<div class="flex justify-end gap-2">
|
|
<ButtonStyled type="outlined">
|
|
<button type="button" :disabled="databaseOperationBusy" @click="cancelDatabaseOperation">
|
|
<XIcon />
|
|
{{ formatMessage(messages.cancel) }}
|
|
</button>
|
|
</ButtonStyled>
|
|
<ButtonStyled color="brand">
|
|
<button type="button" :disabled="databaseOperationBusy" @click="confirmDatabaseOperation">
|
|
<RefreshCwIcon :class="{ 'animate-spin': databaseOperationBusy }" />
|
|
{{ formatMessage(messages.databaseOperationConfirm) }}
|
|
</button>
|
|
</ButtonStyled>
|
|
</div>
|
|
</template>
|
|
</NewModal>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.update-check-panel {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: flex-start;
|
|
gap: var(--gap-md);
|
|
padding: var(--gap-lg);
|
|
}
|
|
|
|
.database-isolation {
|
|
display: grid;
|
|
grid-template-columns: minmax(0, 1fr) minmax(16rem, 0.8fr);
|
|
gap: var(--gap-lg);
|
|
margin: 0 var(--gap-lg) var(--gap-lg);
|
|
padding: var(--gap-md);
|
|
border: 1px solid var(--surface-4);
|
|
border-radius: var(--radius-md);
|
|
background: var(--surface-1);
|
|
}
|
|
|
|
.database-isolation-copy {
|
|
display: flex;
|
|
min-width: 0;
|
|
flex-direction: column;
|
|
gap: var(--gap-sm);
|
|
}
|
|
|
|
.database-path {
|
|
display: flex;
|
|
min-width: 0;
|
|
flex-direction: column;
|
|
gap: var(--gap-xs);
|
|
color: var(--color-secondary);
|
|
font-size: 0.9375rem;
|
|
}
|
|
|
|
.database-path code {
|
|
overflow-wrap: anywhere;
|
|
color: var(--color-contrast);
|
|
font-family: var(--font-mono);
|
|
}
|
|
|
|
.database-operation {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: var(--gap-xs);
|
|
color: var(--color-secondary);
|
|
font-size: 0.9375rem;
|
|
}
|
|
|
|
.database-operation-buttons {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: var(--gap-sm);
|
|
}
|
|
|
|
.database-diagram {
|
|
display: grid;
|
|
grid-template-columns: 1fr auto 1fr;
|
|
align-items: center;
|
|
gap: var(--gap-sm);
|
|
min-width: 0;
|
|
}
|
|
|
|
.database-channel {
|
|
display: flex;
|
|
min-width: 0;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
gap: 0.2rem;
|
|
padding: var(--gap-sm);
|
|
border: 1px solid var(--surface-4);
|
|
border-radius: var(--radius-sm);
|
|
background: var(--surface-2);
|
|
color: var(--color-contrast);
|
|
text-align: center;
|
|
}
|
|
|
|
.database-channel > span {
|
|
display: grid;
|
|
width: 2rem;
|
|
height: 2rem;
|
|
place-items: center;
|
|
border-radius: 50%;
|
|
background: var(--color-brand);
|
|
color: var(--color-button-text);
|
|
font-weight: 700;
|
|
}
|
|
|
|
.database-channel small {
|
|
color: var(--color-brand);
|
|
font-size: 0.7rem;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.database-channel-beta > span {
|
|
background: var(--color-purple, var(--color-brand));
|
|
}
|
|
|
|
.database-branch {
|
|
width: 2.5rem;
|
|
height: 1px;
|
|
background: var(--surface-5);
|
|
}
|
|
|
|
.update-channel-panel {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: var(--gap-md);
|
|
padding: var(--gap-lg);
|
|
}
|
|
|
|
.update-channel-options {
|
|
display: grid;
|
|
grid-template-columns: minmax(0, 1fr);
|
|
gap: var(--gap-md);
|
|
width: 100%;
|
|
}
|
|
|
|
.update-channel-card {
|
|
display: grid;
|
|
grid-template-columns: minmax(0, 1fr) auto;
|
|
width: 100%;
|
|
min-width: 0;
|
|
gap: var(--gap-xs);
|
|
padding: var(--gap-md);
|
|
border: 1px solid var(--surface-4);
|
|
border-radius: var(--radius-md);
|
|
background: var(--surface-1);
|
|
color: var(--color-secondary);
|
|
text-align: left;
|
|
cursor: pointer;
|
|
transition:
|
|
border-color 150ms ease,
|
|
background-color 150ms ease,
|
|
color 150ms ease,
|
|
transform 150ms ease;
|
|
}
|
|
|
|
.update-channel-card:hover {
|
|
border-color: color-mix(in srgb, var(--color-brand) 55%, var(--surface-4));
|
|
background: var(--surface-2);
|
|
}
|
|
|
|
.update-channel-card:active {
|
|
transform: scale(0.98);
|
|
}
|
|
|
|
.update-channel-card:focus-visible {
|
|
outline: 2px solid var(--color-brand);
|
|
outline-offset: 2px;
|
|
}
|
|
|
|
.update-channel-card-selected {
|
|
border-color: var(--color-brand);
|
|
background: color-mix(in srgb, var(--color-brand) 12%, var(--surface-1));
|
|
}
|
|
|
|
.update-channel-card-title {
|
|
color: var(--color-contrast);
|
|
font-weight: 600;
|
|
}
|
|
|
|
.update-channel-card-description {
|
|
font-size: 0.875rem;
|
|
line-height: 1.4;
|
|
}
|
|
|
|
.update-channel-card-version {
|
|
grid-column: 2;
|
|
grid-row: 1 / span 2;
|
|
align-self: center;
|
|
color: var(--color-secondary);
|
|
font-size: 0.8125rem;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.update-check-heading {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: var(--gap-xs);
|
|
}
|
|
|
|
.update-check-result {
|
|
margin: 0;
|
|
padding: var(--gap-sm) var(--gap-md);
|
|
border: 1px solid var(--surface-4);
|
|
border-radius: var(--radius-sm);
|
|
background: var(--surface-1);
|
|
color: var(--color-secondary);
|
|
font-size: 0.875rem;
|
|
line-height: 1.4;
|
|
}
|
|
|
|
.update-check-result-available {
|
|
border-color: color-mix(in srgb, var(--color-brand) 45%, var(--surface-4));
|
|
color: var(--color-brand);
|
|
}
|
|
|
|
.update-check-result-failed,
|
|
.update-check-result-offline {
|
|
border-color: color-mix(in srgb, var(--color-red) 45%, var(--surface-4));
|
|
color: var(--color-red);
|
|
}
|
|
|
|
.update-check-result-paused,
|
|
.update-check-result-portable,
|
|
.update-check-result-disabled {
|
|
border-color: color-mix(in srgb, var(--color-yellow) 45%, var(--surface-4));
|
|
color: var(--color-yellow);
|
|
}
|
|
|
|
@media (max-width: 640px) {
|
|
.database-isolation {
|
|
grid-template-columns: minmax(0, 1fr);
|
|
}
|
|
}
|
|
</style>
|