2025-06-26 15:09:20 +08:00
|
|
|
<template>
|
|
|
|
|
<Card :title="t('dashboard.serviceManagement.title')" variant="elevated" hover class="service-management-card">
|
|
|
|
|
<div class="service-sections">
|
|
|
|
|
<div class="status-section">
|
|
|
|
|
<div class="status-header">
|
|
|
|
|
<div class="status-info">
|
|
|
|
|
<h4>{{ t('dashboard.serviceManagement.serviceStatus') }}</h4>
|
|
|
|
|
<div class="status-indicator" :class="statusClass">
|
|
|
|
|
<div class="status-dot"></div>
|
|
|
|
|
<span>{{ statusText }}</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="service-icon" :class="statusClass">
|
|
|
|
|
<component :is="statusIcon" :size="24" />
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="actions-section">
|
|
|
|
|
<div class="action-buttons">
|
|
|
|
|
<button
|
2025-08-27 14:13:30 +08:00
|
|
|
v-if="serviceStatus !== 'running' && serviceStatus !== 'stopped'"
|
2025-06-26 15:09:20 +08:00
|
|
|
:disabled="actionLoading || serviceStatus === 'installed'"
|
|
|
|
|
class="action-btn install-btn"
|
2025-08-27 14:13:30 +08:00
|
|
|
@click="installService"
|
2025-06-26 15:09:20 +08:00
|
|
|
>
|
|
|
|
|
<component :is="actionLoading && currentAction === 'install' ? LoaderIcon : Download" :size="16" />
|
|
|
|
|
<span>{{
|
|
|
|
|
actionLoading && currentAction === 'install'
|
|
|
|
|
? t('common.loading')
|
|
|
|
|
: t('dashboard.serviceManagement.install')
|
|
|
|
|
}}</span>
|
|
|
|
|
</button>
|
2025-06-27 14:02:36 +08:00
|
|
|
|
2025-06-26 15:09:20 +08:00
|
|
|
<button
|
2025-08-27 14:13:30 +08:00
|
|
|
v-if="serviceStatus === 'installed' || serviceStatus === 'stopped'"
|
2025-06-27 14:02:36 +08:00
|
|
|
:disabled="actionLoading || (serviceStatus !== 'installed' && serviceStatus !== 'stopped')"
|
2025-06-26 15:09:20 +08:00
|
|
|
class="action-btn start-btn"
|
2025-08-27 14:13:30 +08:00
|
|
|
@click="startService"
|
2025-06-26 15:09:20 +08:00
|
|
|
>
|
|
|
|
|
<component :is="actionLoading && currentAction === 'start' ? LoaderIcon : Play" :size="16" />
|
|
|
|
|
<span>{{
|
|
|
|
|
actionLoading && currentAction === 'start' ? t('common.loading') : t('dashboard.serviceManagement.start')
|
|
|
|
|
}}</span>
|
|
|
|
|
</button>
|
2025-06-27 14:02:36 +08:00
|
|
|
|
|
|
|
|
<button
|
2025-08-27 14:13:30 +08:00
|
|
|
v-if="serviceStatus === 'running'"
|
2025-06-27 14:02:36 +08:00
|
|
|
:disabled="actionLoading"
|
|
|
|
|
class="action-btn stop-btn"
|
2025-08-27 14:13:30 +08:00
|
|
|
@click="stopService"
|
2025-06-27 14:02:36 +08:00
|
|
|
>
|
|
|
|
|
<component :is="actionLoading && currentAction === 'stop' ? LoaderIcon : Stop" :size="16" />
|
|
|
|
|
<span>{{
|
|
|
|
|
actionLoading && currentAction === 'stop' ? t('common.loading') : t('dashboard.serviceManagement.stop')
|
|
|
|
|
}}</span>
|
|
|
|
|
</button>
|
|
|
|
|
|
2025-06-26 15:09:20 +08:00
|
|
|
<button
|
2025-08-27 14:13:30 +08:00
|
|
|
v-if="serviceStatus !== 'not-installed'"
|
2025-06-26 15:09:20 +08:00
|
|
|
:disabled="actionLoading"
|
|
|
|
|
class="action-btn uninstall-btn"
|
2025-08-27 14:13:30 +08:00
|
|
|
@click="showUninstallDialog = true"
|
2025-06-26 15:09:20 +08:00
|
|
|
>
|
|
|
|
|
<component :is="actionLoading && currentAction === 'uninstall' ? LoaderIcon : Trash2" :size="16" />
|
|
|
|
|
<span>{{
|
|
|
|
|
actionLoading && currentAction === 'uninstall'
|
|
|
|
|
? t('common.loading')
|
|
|
|
|
: t('dashboard.serviceManagement.uninstall')
|
|
|
|
|
}}</span>
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<ConfirmDialog
|
|
|
|
|
:is-open="showUninstallDialog"
|
|
|
|
|
:title="t('dashboard.serviceManagement.confirmUninstall.title')"
|
|
|
|
|
:message="t('dashboard.serviceManagement.confirmUninstall.message')"
|
|
|
|
|
:confirm-text="t('dashboard.serviceManagement.confirmUninstall.confirm')"
|
|
|
|
|
:cancel-text="t('common.cancel')"
|
|
|
|
|
variant="danger"
|
|
|
|
|
@confirm="confirmUninstall"
|
|
|
|
|
@cancel="cancelUninstall"
|
|
|
|
|
/>
|
|
|
|
|
</Card>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<script setup lang="ts">
|
2025-06-27 14:02:36 +08:00
|
|
|
import {
|
2025-08-27 14:13:30 +08:00
|
|
|
CheckCircle2,
|
|
|
|
|
Circle,
|
2025-06-27 14:02:36 +08:00
|
|
|
Download,
|
2025-08-27 14:13:30 +08:00
|
|
|
Loader2 as LoaderIcon,
|
2025-06-27 14:02:36 +08:00
|
|
|
Play,
|
2025-08-27 14:13:30 +08:00
|
|
|
Server,
|
2025-06-27 14:02:36 +08:00
|
|
|
Square as Stop,
|
|
|
|
|
Trash2,
|
2025-08-27 14:13:30 +08:00
|
|
|
XCircle
|
2025-06-27 14:02:36 +08:00
|
|
|
} from 'lucide-vue-next'
|
2025-08-27 14:13:30 +08:00
|
|
|
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
|
|
|
|
|
2025-06-26 15:09:20 +08:00
|
|
|
import { useRcloneStore } from '@/stores/rclone'
|
2025-06-27 14:02:36 +08:00
|
|
|
|
2025-08-27 14:13:30 +08:00
|
|
|
import { TauriAPI } from '../../api/tauri'
|
|
|
|
|
import { useTranslation } from '../../composables/useI18n'
|
|
|
|
|
import Card from '../ui/CardPage.vue'
|
|
|
|
|
import ConfirmDialog from '../ui/ConfirmDialog.vue'
|
|
|
|
|
|
2025-07-09 17:11:07 +08:00
|
|
|
const rcloneStore = useRcloneStore()
|
2025-06-26 15:09:20 +08:00
|
|
|
|
|
|
|
|
const { t } = useTranslation()
|
|
|
|
|
|
2025-06-27 14:02:36 +08:00
|
|
|
const serviceStatus = ref<'not-installed' | 'installed' | 'running' | 'error' | 'stopped'>('not-installed')
|
2025-06-26 15:09:20 +08:00
|
|
|
const actionLoading = ref(false)
|
|
|
|
|
const currentAction = ref('')
|
|
|
|
|
const showUninstallDialog = ref(false)
|
|
|
|
|
|
2025-08-27 14:13:30 +08:00
|
|
|
const statusCheckInterval: number | null = null
|
2025-06-26 15:09:20 +08:00
|
|
|
|
|
|
|
|
const statusClass = computed(() => {
|
|
|
|
|
switch (serviceStatus.value) {
|
|
|
|
|
case 'running':
|
|
|
|
|
return 'status-running'
|
|
|
|
|
case 'installed':
|
|
|
|
|
return 'status-installed'
|
|
|
|
|
case 'error':
|
2025-06-27 14:02:36 +08:00
|
|
|
case 'stopped':
|
2025-06-26 15:09:20 +08:00
|
|
|
return 'status-error'
|
|
|
|
|
default:
|
|
|
|
|
return 'status-not-installed'
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const statusText = computed(() => {
|
|
|
|
|
switch (serviceStatus.value) {
|
|
|
|
|
case 'running':
|
|
|
|
|
return t('dashboard.serviceManagement.status.running')
|
|
|
|
|
case 'installed':
|
|
|
|
|
return t('dashboard.serviceManagement.status.installed')
|
|
|
|
|
case 'error':
|
|
|
|
|
return t('dashboard.serviceManagement.status.error')
|
2025-06-27 14:02:36 +08:00
|
|
|
case 'stopped':
|
|
|
|
|
return t('dashboard.serviceManagement.status.stopped')
|
2025-06-26 15:09:20 +08:00
|
|
|
default:
|
|
|
|
|
return t('dashboard.serviceManagement.status.notInstalled')
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const statusIcon = computed(() => {
|
|
|
|
|
switch (serviceStatus.value) {
|
|
|
|
|
case 'running':
|
|
|
|
|
return CheckCircle2
|
|
|
|
|
case 'installed':
|
|
|
|
|
return Circle
|
|
|
|
|
case 'error':
|
|
|
|
|
return XCircle
|
2025-06-27 14:02:36 +08:00
|
|
|
case 'stopped':
|
|
|
|
|
return Stop
|
2025-06-26 15:09:20 +08:00
|
|
|
default:
|
|
|
|
|
return Server
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const checkServiceStatus = async () => {
|
|
|
|
|
try {
|
2025-07-03 14:45:25 +08:00
|
|
|
const status = await TauriAPI.service.status()
|
2025-06-27 14:02:36 +08:00
|
|
|
serviceStatus.value = status as 'not-installed' | 'installed' | 'running' | 'error' | 'stopped'
|
2025-06-26 15:09:20 +08:00
|
|
|
return status
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Failed to check service status:', error)
|
|
|
|
|
serviceStatus.value = 'error'
|
2025-06-27 14:02:36 +08:00
|
|
|
return 'error'
|
2025-06-26 15:09:20 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const installService = async () => {
|
|
|
|
|
actionLoading.value = true
|
|
|
|
|
currentAction.value = 'install'
|
|
|
|
|
try {
|
2025-07-03 14:45:25 +08:00
|
|
|
const result = await TauriAPI.service.install()
|
2025-06-26 15:09:20 +08:00
|
|
|
if (!result) {
|
|
|
|
|
throw new Error('Installation failed')
|
|
|
|
|
}
|
|
|
|
|
await new Promise(resolve => setTimeout(resolve, 5000))
|
|
|
|
|
const status = await checkServiceStatus()
|
2025-06-27 14:02:36 +08:00
|
|
|
if (status !== 'installed' && status !== 'running' && status !== 'stopped') {
|
|
|
|
|
throw new Error('Service installation did not complete successfully')
|
2025-06-26 15:09:20 +08:00
|
|
|
}
|
|
|
|
|
try {
|
2025-07-03 14:45:25 +08:00
|
|
|
await TauriAPI.rclone.backend.createAndStart()
|
2025-06-26 15:09:20 +08:00
|
|
|
await rcloneStore.checkRcloneBackendStatus()
|
|
|
|
|
} catch (stopError) {
|
|
|
|
|
console.warn('Failed to stop service during installation:', stopError)
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
serviceStatus.value = 'error'
|
|
|
|
|
} finally {
|
|
|
|
|
actionLoading.value = false
|
|
|
|
|
currentAction.value = ''
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const startService = async () => {
|
|
|
|
|
actionLoading.value = true
|
|
|
|
|
currentAction.value = 'start'
|
|
|
|
|
try {
|
2025-07-03 14:45:25 +08:00
|
|
|
const result = await TauriAPI.service.start()
|
2025-06-26 15:09:20 +08:00
|
|
|
if (!result) {
|
|
|
|
|
throw new Error('Service start failed')
|
|
|
|
|
}
|
|
|
|
|
serviceStatus.value = 'running'
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Failed to start service:', error)
|
|
|
|
|
serviceStatus.value = 'error'
|
|
|
|
|
} finally {
|
|
|
|
|
actionLoading.value = false
|
|
|
|
|
currentAction.value = ''
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-27 14:02:36 +08:00
|
|
|
const stopService = async () => {
|
|
|
|
|
actionLoading.value = true
|
|
|
|
|
currentAction.value = 'stop'
|
|
|
|
|
try {
|
2025-07-03 14:45:25 +08:00
|
|
|
const result = await TauriAPI.service.stop()
|
2025-06-27 14:02:36 +08:00
|
|
|
if (!result) {
|
|
|
|
|
throw new Error('Service stop failed')
|
|
|
|
|
}
|
2025-07-21 17:19:42 +08:00
|
|
|
let attempts = 0
|
|
|
|
|
const maxAttempts = 5
|
|
|
|
|
for (let i = 0; i < maxAttempts; i++) {
|
|
|
|
|
const status = await checkServiceStatus()
|
|
|
|
|
if (status === 'stopped' || status === 'not-installed' || status === 'error') {
|
|
|
|
|
serviceStatus.value = status
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
attempts++
|
|
|
|
|
await new Promise(resolve => setTimeout(resolve, 3000))
|
|
|
|
|
}
|
2025-06-27 14:02:36 +08:00
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Failed to stop service:', error)
|
|
|
|
|
serviceStatus.value = 'error'
|
|
|
|
|
} finally {
|
|
|
|
|
actionLoading.value = false
|
|
|
|
|
currentAction.value = ''
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-26 15:09:20 +08:00
|
|
|
const uninstallService = async () => {
|
|
|
|
|
actionLoading.value = true
|
|
|
|
|
currentAction.value = 'uninstall'
|
|
|
|
|
try {
|
2025-07-03 14:45:25 +08:00
|
|
|
const result = await TauriAPI.service.uninstall()
|
2025-06-26 15:09:20 +08:00
|
|
|
if (!result) {
|
|
|
|
|
throw new Error('Uninstallation failed')
|
|
|
|
|
}
|
|
|
|
|
serviceStatus.value = 'not-installed'
|
|
|
|
|
await rcloneStore.checkRcloneBackendStatus()
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Failed to uninstall service:', error)
|
|
|
|
|
serviceStatus.value = 'error'
|
|
|
|
|
} finally {
|
|
|
|
|
actionLoading.value = false
|
|
|
|
|
currentAction.value = ''
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const confirmUninstall = async () => {
|
|
|
|
|
showUninstallDialog.value = false
|
|
|
|
|
await uninstallService()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const cancelUninstall = () => {
|
|
|
|
|
showUninstallDialog.value = false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
onMounted(async () => {
|
|
|
|
|
await checkServiceStatus()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
onUnmounted(() => {
|
|
|
|
|
if (statusCheckInterval) {
|
|
|
|
|
clearInterval(statusCheckInterval)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
|
|
|
|
.service-sections {
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
gap: 1.25rem;
|
|
|
|
|
flex: 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.status-section {
|
|
|
|
|
border: 1px solid rgb(229 231 235);
|
|
|
|
|
border-radius: 0.75rem;
|
|
|
|
|
padding: 1.25rem;
|
|
|
|
|
background: rgb(249 250 251);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
:root.dark .status-section,
|
|
|
|
|
:root.auto.dark .status-section {
|
|
|
|
|
border-color: rgb(55 65 81);
|
|
|
|
|
background: rgb(31 41 55);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.status-header {
|
|
|
|
|
display: flex;
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
align-items: flex-start;
|
|
|
|
|
margin-bottom: 1rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.status-info h4 {
|
|
|
|
|
margin: 0 0 0.5rem 0;
|
|
|
|
|
font-size: 1rem;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
color: rgb(17 24 39);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
:root.dark .status-info h4,
|
|
|
|
|
:root.auto.dark .status-info h4 {
|
|
|
|
|
color: rgb(243 244 246);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.status-indicator {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 0.5rem;
|
|
|
|
|
font-size: 0.875rem;
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.status-dot {
|
|
|
|
|
width: 0.5rem;
|
|
|
|
|
height: 0.5rem;
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.status-running .status-dot {
|
|
|
|
|
background: rgb(34 197 94);
|
|
|
|
|
box-shadow: 0 0 0 2px rgba(34, 197, 94, 0.2);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.status-installed .status-dot {
|
|
|
|
|
background: rgb(251 191 36);
|
|
|
|
|
box-shadow: 0 0 0 2px rgba(251, 191, 36, 0.2);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.status-error .status-dot {
|
|
|
|
|
background: rgb(239 68 68);
|
|
|
|
|
box-shadow: 0 0 0 2px rgba(239, 68, 68, 0.2);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.status-not-installed .status-dot {
|
|
|
|
|
background: rgb(107 114 128);
|
|
|
|
|
box-shadow: 0 0 0 2px rgba(107, 114, 128, 0.2);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.status-running {
|
|
|
|
|
color: rgb(34 197 94);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.status-installed {
|
|
|
|
|
color: rgb(251 191 36);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.status-error {
|
|
|
|
|
color: rgb(239 68 68);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.status-not-installed {
|
|
|
|
|
color: rgb(107 114 128);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.service-icon {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
width: 3rem;
|
|
|
|
|
height: 3rem;
|
|
|
|
|
border-radius: 0.75rem;
|
|
|
|
|
margin-left: 1rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.service-icon.status-running {
|
|
|
|
|
background: linear-gradient(135deg, rgba(34, 197, 94, 0.1), rgba(34, 197, 94, 0.2));
|
|
|
|
|
color: rgb(34 197 94);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.service-icon.status-installed {
|
|
|
|
|
background: linear-gradient(135deg, rgba(251, 191, 36, 0.1), rgba(251, 191, 36, 0.2));
|
|
|
|
|
color: rgb(251 191 36);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.service-icon.status-error {
|
|
|
|
|
background: linear-gradient(135deg, rgba(239, 68, 68, 0.1), rgba(239, 68, 68, 0.2));
|
|
|
|
|
color: rgb(239 68 68);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.service-icon.status-not-installed {
|
|
|
|
|
background: linear-gradient(135deg, rgba(107, 114, 128, 0.1), rgba(107, 114, 128, 0.2));
|
|
|
|
|
color: rgb(107 114 128);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.service-details {
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
gap: 0.5rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
:root.dark .status-section,
|
|
|
|
|
:root.auto.dark .status-section {
|
|
|
|
|
border-color: rgb(55 65 81);
|
|
|
|
|
background: rgb(31 41 55);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.actions-section {
|
|
|
|
|
border: 1px solid rgb(229 231 235);
|
|
|
|
|
border-radius: 0.75rem;
|
|
|
|
|
padding: 1.25rem;
|
|
|
|
|
background: rgb(249 250 251);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
:root.dark .actions-section,
|
|
|
|
|
:root.auto.dark .actions-section {
|
|
|
|
|
border-color: rgb(55 65 81);
|
|
|
|
|
background: rgb(31 41 55);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.action-buttons {
|
|
|
|
|
display: flex;
|
|
|
|
|
gap: 0.75rem;
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.action-btn {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 0.5rem;
|
|
|
|
|
padding: 0.75rem 1rem;
|
|
|
|
|
font-size: 0.875rem;
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
border: none;
|
|
|
|
|
border-radius: 0.5rem;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
flex: 1;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
min-width: 7rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.install-btn {
|
|
|
|
|
background: rgb(34 197 94);
|
|
|
|
|
color: white;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.install-btn:hover:not(:disabled) {
|
|
|
|
|
background: rgb(21 128 61);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.start-btn {
|
|
|
|
|
background: rgb(59 130 246);
|
|
|
|
|
color: white;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.start-btn:hover:not(:disabled) {
|
|
|
|
|
background: rgb(37 99 235);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.stop-btn {
|
|
|
|
|
background: rgb(239 68 68);
|
|
|
|
|
color: white;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.stop-btn:hover:not(:disabled) {
|
|
|
|
|
background: rgb(220 38 38);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.restart-btn {
|
|
|
|
|
background: rgb(251 191 36);
|
|
|
|
|
color: white;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.restart-btn:hover:not(:disabled) {
|
|
|
|
|
background: rgb(245 158 11);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.uninstall-btn {
|
|
|
|
|
background: rgb(107 114 128);
|
|
|
|
|
color: white;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.uninstall-btn:hover:not(:disabled) {
|
|
|
|
|
background: rgb(75 85 99);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.action-btn:disabled {
|
|
|
|
|
opacity: 0.6;
|
|
|
|
|
cursor: not-allowed;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Responsive design */
|
|
|
|
|
@media (max-width: 768px) {
|
|
|
|
|
.action-buttons {
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.action-btn {
|
|
|
|
|
flex: none;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
</style>
|