mirror of
https://github.com/OpenListTeam/OpenList-Desktop.git
synced 2025-11-25 19:27:33 +08:00
932 lines
22 KiB
Vue
932 lines
22 KiB
Vue
<template>
|
||
<Card
|
||
:title="t('update.title')"
|
||
variant="elevated"
|
||
hover
|
||
class="update-manager-card"
|
||
:class="{ standalone: isStandalone }"
|
||
>
|
||
<div class="update-content">
|
||
<div class="version-info">
|
||
<div class="current-version">
|
||
<h4>{{ t('update.currentVersion') }}</h4>
|
||
<span class="version-tag">v{{ currentVersion }}</span>
|
||
</div>
|
||
<button @click="checkForUpdates" :disabled="checking || downloading || installing" class="check-update-btn">
|
||
<RefreshCw :size="16" />
|
||
{{ checking ? t('update.checking') : t('update.checkForUpdates') }}
|
||
</button>
|
||
</div>
|
||
|
||
<div class="settings-row">
|
||
<div class="auto-check-setting">
|
||
<label class="checkbox-container">
|
||
<input type="checkbox" v-model="autoCheckEnabled" @change="toggleAutoCheck" :disabled="settingsLoading" />
|
||
<span class="label-text">{{ t('update.autoCheck') }}</span>
|
||
</label>
|
||
</div>
|
||
</div>
|
||
|
||
<div v-if="error" class="error-state">
|
||
<div class="error-content">
|
||
<AlertCircle :size="16" />
|
||
<span>{{ error }}</span>
|
||
</div>
|
||
<button @click="clearError" class="clear-error-btn">×</button>
|
||
</div>
|
||
|
||
<div v-if="!updateCheck?.hasUpdate && lastChecked && !checking && !error" class="no-updates">
|
||
<CheckCircle :size="24" class="check-icon" />
|
||
<div class="no-updates-text">
|
||
<h4>{{ t('update.upToDate') }}</h4>
|
||
<p>{{ t('update.lastChecked') }}: {{ formatDate(lastChecked) }}</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div v-if="updateCheck?.hasUpdate && !installing" class="update-available">
|
||
<div class="update-header">
|
||
<Download :size="24" class="update-icon" />
|
||
<div class="update-details">
|
||
<h4>{{ t('update.updateAvailable') }}</h4>
|
||
<div class="version-comparison">
|
||
<span class="current">v{{ updateCheck.currentVersion }}</span>
|
||
<ArrowRight :size="16" />
|
||
<span class="new">{{ updateCheck.latestVersion }}</span>
|
||
</div>
|
||
<div class="release-date">{{ t('update.releaseDate') }}: {{ formatDate(updateCheck.releaseDate) }}</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div v-if="updateCheck.releaseNotes" class="release-notes">
|
||
<h5>{{ t('update.releaseNotes') }}</h5>
|
||
<div class="notes-content" v-html="formatReleaseNotes(updateCheck.releaseNotes)"></div>
|
||
</div>
|
||
|
||
<div v-if="updateCheck.assets.length > 0" class="assets-section">
|
||
<h5>{{ t('update.availableInstallers') }}</h5>
|
||
<div class="assets-list">
|
||
<div
|
||
v-for="asset in updateCheck.assets"
|
||
:key="asset.name"
|
||
class="asset-item"
|
||
:class="{ selected: selectedAsset?.name === asset.name }"
|
||
@click="selectAsset(asset)"
|
||
>
|
||
<div class="asset-info">
|
||
<div class="asset-name">{{ asset.name }}</div>
|
||
<div class="asset-details">
|
||
<span class="asset-type">{{ asset.type.toUpperCase() }}</span>
|
||
<span class="asset-size">{{ formatBytes(asset.size) }}</span>
|
||
</div>
|
||
</div>
|
||
<div class="asset-platform">{{ asset.platform }}</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div v-if="downloading" class="progress-container">
|
||
<div class="progress-info">
|
||
<span class="progress-text">{{ t('update.downloading') }}...</span>
|
||
<span class="progress-percentage">{{ Math.round(downloadProgress?.percentage || 0) }}%</span>
|
||
</div>
|
||
<div class="progress-bar">
|
||
<div class="progress-fill" :style="{ width: `${downloadProgress?.percentage || 0}%` }"></div>
|
||
</div>
|
||
<div class="progress-details">
|
||
<span class="speed">{{ formatSpeed(downloadProgress?.speed || 0) }}</span>
|
||
<span class="size-info">
|
||
{{ formatBytes(downloadProgress?.downloaded || 0) }} / {{ formatBytes(downloadProgress?.total || 0) }}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="update-actions" v-if="!downloading">
|
||
<button
|
||
@click="downloadAndInstall"
|
||
:disabled="!selectedAsset || checking || downloading || installing"
|
||
class="install-btn"
|
||
>
|
||
<Download :size="16" />
|
||
{{ t('update.downloadAndInstall') }}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div v-if="installationStatus" class="status-message" :class="installationStatusType">
|
||
<div class="status-content">
|
||
<component :is="getStatusIcon()" :size="16" />
|
||
<span>{{ installationStatus }}</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div v-if="backgroundUpdateAvailable" class="background-update-notification">
|
||
<div class="notification-content">
|
||
<Info :size="20" class="notification-icon" />
|
||
<div class="notification-text">
|
||
<span>{{ t('update.backgroundUpdateAvailable') }}</span>
|
||
<button @click="showBackgroundUpdate" class="show-update-btn">
|
||
{{ t('update.showUpdate') }}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</Card>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { ref, onMounted, onUnmounted, computed } from 'vue'
|
||
import { useTranslation } from '../../composables/useI18n'
|
||
import { useAppStore } from '../../stores/app'
|
||
import { TauriAPI } from '../../api/tauri'
|
||
import Card from '../ui/Card.vue'
|
||
import { formatBytes } from '@/utils/formatters'
|
||
import { RefreshCw, Download, ArrowRight, CheckCircle, AlertCircle, Info, CheckCircle2 } from 'lucide-vue-next'
|
||
|
||
interface Props {
|
||
isStandalone?: boolean
|
||
}
|
||
|
||
const props = withDefaults(defineProps<Props>(), {
|
||
isStandalone: false
|
||
})
|
||
|
||
const { t } = useTranslation()
|
||
const appStore = useAppStore()
|
||
|
||
const isStandalone = computed(() => props.isStandalone)
|
||
|
||
const currentVersion = ref('')
|
||
const updateCheck = ref<UpdateCheck | null>(null)
|
||
const backgroundUpdateCheck = ref<UpdateCheck | null>(null)
|
||
const checking = ref(false)
|
||
const downloading = ref(false)
|
||
const installing = ref(false)
|
||
const downloadProgress = ref<DownloadProgress | null>(null)
|
||
const lastChecked = ref<string | null>(null)
|
||
const error = ref<string | null>(null)
|
||
const autoCheckEnabled = ref(true)
|
||
const settingsLoading = ref(false)
|
||
const installationStatus = ref<string | null>(null)
|
||
const installationStatusType = ref<'info' | 'success' | 'error'>('info')
|
||
const selectedAsset = ref<UpdateAsset | null>(null)
|
||
|
||
const backgroundUpdateAvailable = computed(() => backgroundUpdateCheck.value && !updateCheck.value?.hasUpdate)
|
||
|
||
let backgroundUpdateUnlisten: (() => void) | null = null
|
||
let downloadProgressUnlisten: (() => void) | null = null
|
||
let installStartedUnlisten: (() => void) | null = null
|
||
let installErrorUnlisten: (() => void) | null = null
|
||
let appQuitEventUnsubscriber: (() => void) | null = null
|
||
|
||
const checkForUpdates = async () => {
|
||
if (checking.value || downloading.value || installing.value) return
|
||
|
||
try {
|
||
checking.value = true
|
||
error.value = null
|
||
|
||
const result = await TauriAPI.updater.check()
|
||
updateCheck.value = result
|
||
|
||
if (result.hasUpdate && result.assets.length > 0) {
|
||
selectedAsset.value = result.assets[0]
|
||
}
|
||
|
||
lastChecked.value = new Date().toISOString()
|
||
|
||
if (!result.hasUpdate) {
|
||
installationStatus.value = t('update.noUpdatesFound')
|
||
installationStatusType.value = 'success'
|
||
setTimeout(() => {
|
||
installationStatus.value = null
|
||
}, 3000)
|
||
}
|
||
} catch (err: any) {
|
||
console.error('Failed to check for updates:', err)
|
||
error.value = t('update.checkError') + String(err ? `: ${err}` : '')
|
||
} finally {
|
||
checking.value = false
|
||
}
|
||
}
|
||
|
||
const selectAsset = (asset: UpdateAsset) => {
|
||
selectedAsset.value = asset
|
||
}
|
||
|
||
const downloadAndInstall = async () => {
|
||
if (!selectedAsset.value || downloading.value || installing.value) return
|
||
|
||
try {
|
||
downloading.value = true
|
||
installationStatus.value = t('update.startingDownload')
|
||
installationStatusType.value = 'info'
|
||
|
||
const filePath = await TauriAPI.updater.download(selectedAsset.value.url, selectedAsset.value.name)
|
||
|
||
downloading.value = false
|
||
installing.value = true
|
||
|
||
installationStatus.value = t('update.installingUpdate')
|
||
installationStatusType.value = 'info'
|
||
|
||
await TauriAPI.updater.installAndRestart(filePath)
|
||
} catch (err: any) {
|
||
console.error('Failed to download/install update:', err)
|
||
downloading.value = false
|
||
installing.value = false
|
||
error.value = err.message || t('update.installError')
|
||
installationStatus.value = t('update.installError')
|
||
installationStatusType.value = 'error'
|
||
}
|
||
}
|
||
|
||
const toggleAutoCheck = async () => {
|
||
if (settingsLoading.value) return
|
||
|
||
try {
|
||
settingsLoading.value = true
|
||
await TauriAPI.updater.setAutoCheck(autoCheckEnabled.value)
|
||
} catch (err: any) {
|
||
console.error('Failed to update auto-check setting:', err)
|
||
autoCheckEnabled.value = !autoCheckEnabled.value
|
||
} finally {
|
||
settingsLoading.value = false
|
||
}
|
||
}
|
||
|
||
const showBackgroundUpdate = () => {
|
||
if (backgroundUpdateCheck.value) {
|
||
updateCheck.value = backgroundUpdateCheck.value
|
||
backgroundUpdateCheck.value = null
|
||
if (updateCheck.value.assets.length > 0) {
|
||
selectedAsset.value = updateCheck.value.assets[0]
|
||
}
|
||
}
|
||
}
|
||
|
||
const clearError = () => {
|
||
error.value = null
|
||
}
|
||
|
||
const getStatusIcon = () => {
|
||
switch (installationStatusType.value) {
|
||
case 'success':
|
||
return CheckCircle2
|
||
case 'error':
|
||
return AlertCircle
|
||
default:
|
||
return Info
|
||
}
|
||
}
|
||
|
||
const formatDate = (dateString: string) => {
|
||
try {
|
||
const date = new Date(dateString)
|
||
return date.toLocaleDateString() + ' ' + date.toLocaleTimeString()
|
||
} catch {
|
||
return dateString
|
||
}
|
||
}
|
||
|
||
const formatReleaseNotes = (notes: string) => {
|
||
return notes
|
||
.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
|
||
.replace(/\*(.*?)\*/g, '<em>$1</em>')
|
||
.replace(/\n/g, '<br>')
|
||
}
|
||
|
||
const formatSpeed = (bytesPerSecond: number) => {
|
||
if (bytesPerSecond === 0) return '0 B/s'
|
||
const k = 1024
|
||
const sizes = ['B/s', 'KB/s', 'MB/s', 'GB/s']
|
||
const i = Math.floor(Math.log(bytesPerSecond) / Math.log(k))
|
||
return parseFloat((bytesPerSecond / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
|
||
}
|
||
|
||
onMounted(async () => {
|
||
try {
|
||
if (appStore.updateAvailable && appStore.updateCheck) {
|
||
updateCheck.value = appStore.updateCheck
|
||
if (appStore.updateCheck.assets.length > 0) {
|
||
selectedAsset.value = appStore.updateCheck.assets[0]
|
||
}
|
||
}
|
||
appStore.clearUpdateStatus()
|
||
currentVersion.value = await TauriAPI.updater.currentVersion()
|
||
autoCheckEnabled.value = await TauriAPI.updater.isAutoCheckEnabled()
|
||
try {
|
||
backgroundUpdateUnlisten = await TauriAPI.updater.onBackgroundUpdate(updateInfo => {
|
||
console.log('Background update available:', updateInfo)
|
||
backgroundUpdateCheck.value = updateInfo
|
||
})
|
||
} catch (err) {
|
||
console.warn('Background update listener not available:', err)
|
||
backgroundUpdateUnlisten = null
|
||
}
|
||
|
||
try {
|
||
downloadProgressUnlisten = await TauriAPI.updater.onDownloadProgress(progress => {
|
||
downloadProgress.value = progress
|
||
})
|
||
} catch (err) {
|
||
console.warn('Download progress listener not available:', err)
|
||
downloadProgressUnlisten = null
|
||
}
|
||
|
||
try {
|
||
installStartedUnlisten = await TauriAPI.updater.onInstallStarted(() => {
|
||
installing.value = true
|
||
installationStatus.value = t('update.installingUpdate')
|
||
installationStatusType.value = 'info'
|
||
})
|
||
} catch (err) {
|
||
console.warn('Install started listener not available:', err)
|
||
installStartedUnlisten = null
|
||
}
|
||
|
||
try {
|
||
installErrorUnlisten = await TauriAPI.updater.onInstallError(errorMsg => {
|
||
installing.value = false
|
||
error.value = errorMsg
|
||
installationStatus.value = t('update.installError')
|
||
installationStatusType.value = 'error'
|
||
})
|
||
} catch (err) {
|
||
console.warn('Install error listener not available:', err)
|
||
installErrorUnlisten = null
|
||
}
|
||
|
||
try {
|
||
appQuitEventUnsubscriber = await TauriAPI.updater.onAppQuit(() => {
|
||
installationStatus.value = t('update.quitApp')
|
||
installationStatusType.value = 'success'
|
||
})
|
||
} catch (err) {
|
||
console.warn('App restarting listener not available:', err)
|
||
appQuitEventUnsubscriber = null
|
||
}
|
||
if (autoCheckEnabled.value) {
|
||
await checkForUpdates()
|
||
}
|
||
} catch (err) {
|
||
console.error('Failed to initialize update manager:', err)
|
||
}
|
||
})
|
||
|
||
onUnmounted(() => {
|
||
try {
|
||
backgroundUpdateUnlisten?.()
|
||
} catch (err) {
|
||
console.warn('Error unregistering background update listener:', err)
|
||
}
|
||
|
||
try {
|
||
downloadProgressUnlisten?.()
|
||
} catch (err) {
|
||
console.warn('Error unregistering download progress listener:', err)
|
||
}
|
||
|
||
try {
|
||
installStartedUnlisten?.()
|
||
} catch (err) {
|
||
console.warn('Error unregistering install started listener:', err)
|
||
}
|
||
|
||
try {
|
||
installErrorUnlisten?.()
|
||
} catch (err) {
|
||
console.warn('Error unregistering install error listener:', err)
|
||
}
|
||
|
||
try {
|
||
appQuitEventUnsubscriber?.()
|
||
} catch (err) {
|
||
console.warn('Error unregistering app restarting listener:', err)
|
||
}
|
||
})
|
||
</script>
|
||
|
||
<style scoped>
|
||
.update-manager-card {
|
||
max-width: 700px;
|
||
}
|
||
|
||
.update-manager-card.standalone {
|
||
max-width: 100%;
|
||
width: 100%;
|
||
box-shadow: none;
|
||
border: 1px solid var(--color-border);
|
||
}
|
||
|
||
.update-content {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 1.5rem;
|
||
}
|
||
|
||
.version-info {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding: 1rem;
|
||
background: var(--color-surface);
|
||
border-radius: 8px;
|
||
}
|
||
|
||
.current-version h4 {
|
||
margin: 0 0 0.5rem 0;
|
||
font-size: 0.9rem;
|
||
color: var(--color-text-secondary);
|
||
}
|
||
|
||
.version-tag {
|
||
font-size: 1.1rem;
|
||
font-weight: 600;
|
||
color: var(--color-primary);
|
||
}
|
||
|
||
.check-update-btn {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.5rem;
|
||
padding: 0.5rem 1rem;
|
||
background: var(--color-primary);
|
||
color: white;
|
||
border: none;
|
||
border-radius: 6px;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.check-update-btn:hover:not(:disabled) {
|
||
background: rgb(39, 221, 145);
|
||
}
|
||
|
||
.check-update-btn:disabled {
|
||
opacity: 0.6;
|
||
cursor: not-allowed;
|
||
}
|
||
|
||
.settings-row {
|
||
padding: 1rem;
|
||
background: var(--color-surface);
|
||
border-radius: 8px;
|
||
border: 1px solid var(--color-border);
|
||
}
|
||
|
||
.auto-check-setting {
|
||
display: flex;
|
||
align-items: center;
|
||
}
|
||
|
||
.checkbox-container {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.75rem;
|
||
cursor: pointer;
|
||
user-select: none;
|
||
}
|
||
|
||
.checkbox-container input[type='checkbox'] {
|
||
width: 18px;
|
||
height: 18px;
|
||
accent-color: var(--color-primary);
|
||
}
|
||
|
||
.checkbox-container .label-text {
|
||
font-size: 0.9rem;
|
||
color: var(--color-text);
|
||
}
|
||
|
||
.error-state {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 1rem;
|
||
background: rgba(255, 59, 48, 0.1);
|
||
border: 1px solid rgba(255, 59, 48, 0.2);
|
||
border-radius: 6px;
|
||
color: var(--color-danger);
|
||
}
|
||
|
||
:root.dark .error-state,
|
||
:root.auto.dark .error-state {
|
||
background: rgba(255, 59, 48, 0.05);
|
||
border-color: rgba(255, 59, 48, 0.2);
|
||
}
|
||
|
||
.error-content {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.5rem;
|
||
flex: 1;
|
||
}
|
||
|
||
.clear-error-btn {
|
||
background: none;
|
||
border: none;
|
||
font-size: 1.2rem;
|
||
cursor: pointer;
|
||
padding: 0.25rem;
|
||
color: var(--color-danger);
|
||
opacity: 0.7;
|
||
}
|
||
|
||
.clear-error-btn:hover {
|
||
opacity: 1;
|
||
}
|
||
|
||
.no-updates {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 1rem;
|
||
padding: 1.5rem;
|
||
background: var(--color-surface);
|
||
border-radius: 8px;
|
||
}
|
||
|
||
.check-icon {
|
||
color: var(--color-success);
|
||
}
|
||
|
||
.no-updates-text h4 {
|
||
margin: 0 0 0.25rem 0;
|
||
}
|
||
|
||
.no-updates-text p {
|
||
margin: 0;
|
||
font-size: 0.9rem;
|
||
color: var(--color-text-secondary);
|
||
}
|
||
|
||
.update-available {
|
||
border: 2px solid var(--color-success);
|
||
border-radius: 12px;
|
||
padding: 1.5rem;
|
||
background: rgba(52, 199, 89, 0.1);
|
||
}
|
||
|
||
:root.dark .update-available,
|
||
:root.auto.dark .update-available {
|
||
background: rgba(52, 199, 89, 0.05);
|
||
border-color: var(--color-success);
|
||
}
|
||
|
||
.update-header {
|
||
display: flex;
|
||
gap: 1rem;
|
||
margin-bottom: 1rem;
|
||
}
|
||
|
||
.update-icon {
|
||
color: var(--color-success);
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.update-details h4 {
|
||
margin: 0 0 0.5rem 0;
|
||
color: var(--color-success);
|
||
}
|
||
|
||
.version-comparison {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.5rem;
|
||
margin-bottom: 0.5rem;
|
||
}
|
||
|
||
.version-comparison .current {
|
||
color: var(--color-text-secondary);
|
||
}
|
||
|
||
.version-comparison .new {
|
||
color: var(--color-success);
|
||
font-weight: 600;
|
||
}
|
||
|
||
.release-date {
|
||
font-size: 0.85rem;
|
||
color: var(--color-text-secondary);
|
||
}
|
||
|
||
.release-notes {
|
||
margin: 1rem 0;
|
||
}
|
||
|
||
.release-notes h5 {
|
||
margin: 0 0 0.5rem 0;
|
||
font-size: 0.9rem;
|
||
}
|
||
|
||
.notes-content {
|
||
max-height: 200px;
|
||
overflow-y: auto;
|
||
padding: 0.75rem;
|
||
background: rgba(255, 255, 255, 0.7);
|
||
border-radius: 6px;
|
||
font-size: 0.85rem;
|
||
line-height: 1.4;
|
||
}
|
||
|
||
:root.dark .notes-content,
|
||
:root.auto.dark .notes-content {
|
||
background: rgba(255, 255, 255, 0.05);
|
||
color: var(--color-text-primary);
|
||
}
|
||
|
||
.assets-section {
|
||
margin: 1rem 0;
|
||
}
|
||
|
||
.assets-section h5 {
|
||
margin: 0 0 0.75rem 0;
|
||
font-size: 0.9rem;
|
||
color: var(--color-success);
|
||
}
|
||
|
||
.assets-list {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 0.5rem;
|
||
}
|
||
|
||
.asset-item {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding: 0.75rem;
|
||
background: rgba(255, 255, 255, 0.8);
|
||
border: 2px solid transparent;
|
||
border-radius: 6px;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.asset-item:hover {
|
||
background: rgba(255, 255, 255, 0.9);
|
||
}
|
||
|
||
.asset-item.selected {
|
||
border-color: var(--color-success);
|
||
background: rgba(255, 255, 255, 0.95);
|
||
}
|
||
|
||
:root.dark .asset-item,
|
||
:root.auto.dark .asset-item {
|
||
background: rgba(255, 255, 255, 0.05);
|
||
}
|
||
|
||
:root.dark .asset-item:hover,
|
||
:root.auto.dark .asset-item:hover {
|
||
background: rgba(255, 255, 255, 0.08);
|
||
}
|
||
|
||
:root.dark .asset-item.selected,
|
||
:root.auto.dark .asset-item.selected {
|
||
background: rgba(255, 255, 255, 0.1);
|
||
border-color: var(--color-success);
|
||
}
|
||
|
||
.asset-info {
|
||
flex: 1;
|
||
}
|
||
|
||
.asset-name {
|
||
font-weight: 500;
|
||
margin-bottom: 0.25rem;
|
||
}
|
||
|
||
.asset-details {
|
||
display: flex;
|
||
gap: 1rem;
|
||
font-size: 0.8rem;
|
||
color: var(--color-text-secondary);
|
||
}
|
||
|
||
.asset-type {
|
||
font-weight: 600;
|
||
padding: 0.125rem 0.5rem;
|
||
background: var(--color-primary);
|
||
color: white;
|
||
border-radius: 3px;
|
||
text-transform: uppercase;
|
||
font-size: 0.7rem;
|
||
}
|
||
|
||
.asset-platform {
|
||
font-size: 0.8rem;
|
||
color: var(--color-text-secondary);
|
||
font-weight: 500;
|
||
}
|
||
|
||
.progress-container {
|
||
margin: 1rem 0;
|
||
}
|
||
|
||
.progress-info {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 0.5rem;
|
||
}
|
||
|
||
.progress-text {
|
||
font-size: 0.9rem;
|
||
font-weight: 500;
|
||
color: var(--color-success);
|
||
}
|
||
|
||
.progress-percentage {
|
||
font-size: 0.9rem;
|
||
font-weight: 600;
|
||
color: var(--color-success);
|
||
}
|
||
|
||
.progress-bar {
|
||
width: 100%;
|
||
height: 8px;
|
||
background: rgba(255, 255, 255, 0.7);
|
||
border-radius: 4px;
|
||
overflow: hidden;
|
||
margin-bottom: 0.5rem;
|
||
}
|
||
|
||
:root.dark .progress-bar,
|
||
:root.auto.dark .progress-bar {
|
||
background: rgba(255, 255, 255, 0.1);
|
||
}
|
||
|
||
.progress-fill {
|
||
height: 100%;
|
||
background: var(--color-success);
|
||
}
|
||
|
||
.progress-details {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
font-size: 0.8rem;
|
||
color: var(--color-text-secondary);
|
||
}
|
||
|
||
.update-actions {
|
||
display: flex;
|
||
gap: 0.75rem;
|
||
}
|
||
|
||
.install-btn {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.5rem;
|
||
padding: 0.75rem 1.5rem;
|
||
background: var(--color-success);
|
||
color: white;
|
||
border: none;
|
||
border-radius: 8px;
|
||
cursor: pointer;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.install-btn:hover:not(:disabled) {
|
||
background: rgba(52, 199, 89, 0.8);
|
||
}
|
||
|
||
.install-btn:disabled {
|
||
opacity: 0.6;
|
||
cursor: not-allowed;
|
||
}
|
||
|
||
.status-message {
|
||
padding: 0.75rem;
|
||
border-radius: 6px;
|
||
margin-top: 1rem;
|
||
}
|
||
|
||
.status-message.info {
|
||
background: rgba(0, 122, 255, 0.1);
|
||
border: 1px solid rgba(0, 122, 255, 0.2);
|
||
color: var(--color-accent);
|
||
}
|
||
|
||
.status-message.success {
|
||
background: rgba(52, 199, 89, 0.1);
|
||
border: 1px solid rgba(52, 199, 89, 0.2);
|
||
color: var(--color-success);
|
||
}
|
||
|
||
.status-message.error {
|
||
background: rgba(255, 59, 48, 0.1);
|
||
border: 1px solid rgba(255, 59, 48, 0.2);
|
||
color: var(--color-danger);
|
||
}
|
||
|
||
:root.dark .status-message.info,
|
||
:root.auto.dark .status-message.info {
|
||
background: rgba(10, 132, 255, 0.05);
|
||
border-color: rgba(10, 132, 255, 0.2);
|
||
color: var(--color-accent);
|
||
}
|
||
|
||
:root.dark .status-message.success,
|
||
:root.auto.dark .status-message.success {
|
||
background: rgba(52, 199, 89, 0.05);
|
||
border-color: rgba(52, 199, 89, 0.2);
|
||
color: var(--color-success);
|
||
}
|
||
|
||
:root.dark .status-message.error,
|
||
:root.auto.dark .status-message.error {
|
||
background: rgba(255, 59, 48, 0.05);
|
||
border-color: rgba(255, 59, 48, 0.2);
|
||
color: var(--color-danger);
|
||
}
|
||
|
||
.status-content {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.5rem;
|
||
}
|
||
|
||
.background-update-notification {
|
||
padding: 1rem;
|
||
background: rgba(0, 122, 255, 0.1);
|
||
border: 1px solid rgba(0, 122, 255, 0.2);
|
||
border-radius: 8px;
|
||
color: var(--color-accent);
|
||
}
|
||
|
||
:root.dark .background-update-notification,
|
||
:root.auto.dark .background-update-notification {
|
||
background: rgba(10, 132, 255, 0.05);
|
||
border-color: rgba(10, 132, 255, 0.2);
|
||
}
|
||
|
||
.notification-content {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.75rem;
|
||
}
|
||
|
||
.notification-icon {
|
||
color: var(--color-accent);
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.notification-text {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.75rem;
|
||
flex: 1;
|
||
}
|
||
|
||
.show-update-btn {
|
||
padding: 0.25rem 0.75rem;
|
||
background: var(--color-accent);
|
||
color: white;
|
||
border: none;
|
||
border-radius: 4px;
|
||
cursor: pointer;
|
||
font-size: 0.8rem;
|
||
}
|
||
|
||
.show-update-btn:hover {
|
||
background: var(--color-accent-hover);
|
||
}
|
||
|
||
/* Dark mode specific improvements */
|
||
:root.dark .asset-type,
|
||
:root.auto.dark .asset-type {
|
||
background: var(--color-primary);
|
||
color: white;
|
||
}
|
||
|
||
:root.dark .asset-details,
|
||
:root.auto.dark .asset-details {
|
||
color: var(--color-text-secondary);
|
||
}
|
||
|
||
:root.dark .asset-platform,
|
||
:root.auto.dark .asset-platform {
|
||
color: var(--color-text-secondary);
|
||
}
|
||
|
||
:root.dark .progress-details,
|
||
:root.auto.dark .progress-details {
|
||
color: var(--color-text-secondary);
|
||
}
|
||
|
||
:root.dark .update-icon,
|
||
:root.auto.dark .update-icon {
|
||
color: var(--color-success);
|
||
}
|
||
|
||
:root.dark .install-btn,
|
||
:root.auto.dark .install-btn {
|
||
background: var(--color-success);
|
||
color: white;
|
||
}
|
||
|
||
:root.dark .install-btn:hover:not(:disabled),
|
||
:root.auto.dark .install-btn:hover:not(:disabled) {
|
||
background: rgba(52, 199, 89, 0.8);
|
||
}
|
||
</style>
|