astrbbbb / dashboard /src /views /ExtensionPage.vue
qa1145's picture
Upload 1245 files
8ede856 verified
<script setup>
import AstrBotConfig from "@/components/shared/AstrBotConfig.vue";
import ConsoleDisplayer from "@/components/shared/ConsoleDisplayer.vue";
import ReadmeDialog from "@/components/shared/ReadmeDialog.vue";
import ProxySelector from "@/components/shared/ProxySelector.vue";
import UninstallConfirmDialog from "@/components/shared/UninstallConfirmDialog.vue";
import McpServersSection from "@/components/extension/McpServersSection.vue";
import SkillsSection from "@/components/extension/SkillsSection.vue";
import ComponentPanel from "@/components/extension/componentPanel/index.vue";
import InstalledPluginsTab from "./extension/InstalledPluginsTab.vue";
import MarketPluginsTab from "./extension/MarketPluginsTab.vue";
import { useExtensionPage } from "./extension/useExtensionPage";
const pageState = useExtensionPage();
const {
commonStore,
t,
tm,
router,
route,
getSelectedGitHubProxy,
conflictDialog,
checkAndPromptConflicts,
handleConflictConfirm,
fileInput,
activeTab,
validTabs,
isValidTab,
getLocationHash,
extractTabFromHash,
syncTabFromHash,
extension_data,
getInitialShowReserved,
showReserved,
snack_message,
snack_show,
snack_success,
configDialog,
extension_config,
pluginMarketData,
loadingDialog,
showPluginInfoDialog,
selectedPlugin,
curr_namespace,
updatingAll,
readmeDialog,
forceUpdateDialog,
updateAllConfirmDialog,
changelogDialog,
getInitialListViewMode,
isListView,
pluginSearch,
loading_,
currentPage,
dangerConfirmDialog,
selectedDangerPlugin,
selectedMarketInstallPlugin,
installCompat,
versionCompatibilityDialog,
showUninstallDialog,
uninstallTarget,
showSourceDialog,
showSourceManagerDialog,
sourceName,
sourceUrl,
customSources,
selectedSource,
showRemoveSourceDialog,
sourceToRemove,
editingSource,
originalSourceUrl,
extension_url,
dialog,
upload_file,
uploadTab,
showPluginFullName,
marketSearch,
debouncedMarketSearch,
refreshingMarket,
sortBy,
sortOrder,
randomPluginNames,
normalizeStr,
toPinyinText,
toInitials,
plugin_handler_info_headers,
pluginHeaders,
filteredExtensions,
filteredPlugins,
filteredMarketPlugins,
sortedPlugins,
RANDOM_PLUGINS_COUNT,
randomPlugins,
shufflePlugins,
refreshRandomPlugins,
displayItemsPerPage,
totalPages,
paginatedPlugins,
updatableExtensions,
toggleShowReserved,
toast,
resetLoadingDialog,
onLoadingDialogResult,
failedPluginsDict,
getExtensions,
handleReloadAllFailed,
checkUpdate,
uninstallExtension,
handleUninstallConfirm,
updateExtension,
showUpdateAllConfirm,
confirmUpdateAll,
cancelUpdateAll,
confirmForceUpdate,
updateAllExtensions,
pluginOn,
pluginOff,
openExtensionConfig,
updateConfig,
showPluginInfo,
reloadPlugin,
viewReadme,
viewChangelog,
handleInstallPlugin,
confirmDangerInstall,
cancelDangerInstall,
loadCustomSources,
saveCustomSources,
addCustomSource,
openSourceManagerDialog,
selectPluginSource,
sourceSelectItems,
editCustomSource,
removeCustomSource,
confirmRemoveSource,
saveCustomSource,
trimExtensionName,
checkAlreadyInstalled,
showVersionCompatibilityWarning,
continueInstallIgnoringVersionWarning,
cancelInstallOnVersionWarning,
newExtension,
normalizePlatformList,
getPlatformDisplayList,
resolveSelectedInstallPlugin,
selectedInstallPlugin,
checkInstallCompatibility,
refreshPluginMarket,
handleLocaleChange,
searchDebounceTimer,
} = pageState;
</script>
<template>
<v-row>
<v-col cols="12" md="12">
<v-card variant="flat" style="background-color: transparent">
<!-- 标签页 -->
<v-card-text style="padding: 0px 12px">
<!-- 已安装插件标签页内容 -->
<InstalledPluginsTab :state="pageState" />
<!-- 指令面板标签页内容 -->
<v-tab-item v-show="activeTab === 'components'">
<div class="mb-4 pt-4 pb-4">
<div class="d-flex align-center flex-wrap" style="gap: 12px">
<h2 class="text-h2 mb-0">{{ tm("tabs.handlersOperation") }}</h2>
</div>
</div>
<v-card
class="rounded-lg"
variant="flat"
style="background-color: transparent"
>
<v-card-text class="pa-0">
<ComponentPanel :active="activeTab === 'components'" />
</v-card-text>
</v-card>
</v-tab-item>
<!-- 已安装的 MCP 服务器标签页内容 -->
<v-tab-item v-show="activeTab === 'mcp'">
<div class="mb-4 pt-4 pb-4">
<div class="d-flex align-center flex-wrap" style="gap: 12px">
<h2 class="text-h2 mb-0">{{ tm("tabs.installedMcpServers") }}</h2>
</div>
</div>
<v-card
class="rounded-lg"
variant="flat"
style="background-color: transparent"
>
<v-card-text class="pa-0">
<McpServersSection />
</v-card-text>
</v-card>
</v-tab-item>
<!-- Skills 标签页内容 -->
<v-tab-item v-show="activeTab === 'skills'">
<div class="mb-4 pt-4 pb-4">
<div class="d-flex align-center flex-wrap" style="gap: 12px">
<h2 class="text-h2 mb-0">{{ tm("tabs.skills") }}</h2>
</div>
</div>
<v-card
class="rounded-lg"
variant="flat"
style="background-color: transparent"
>
<v-card-text class="pa-0">
<SkillsSection />
</v-card-text>
</v-card>
</v-tab-item>
<!-- 插件市场标签页内容 -->
<MarketPluginsTab :state="pageState" />
<v-row v-if="loading_">
<v-col cols="12" class="d-flex justify-center">
<v-progress-circular
indeterminate
color="primary"
size="48"
></v-progress-circular>
</v-col>
</v-row>
</v-card-text>
</v-card>
</v-col>
<v-col v-if="activeTab === 'market'" cols="12" md="12">
<div class="d-flex align-center justify-center mt-4 mb-4 gap-4">
<v-btn
variant="text"
prepend-icon="mdi-book-open-variant"
href="https://astrbot.app/dev/plugin.html"
target="_blank"
color="primary"
class="text-none"
>
{{ tm("market.devDocs") }}
</v-btn>
<div
style="
height: 24px;
width: 1px;
background-color: rgba(var(--v-theme-on-surface), 0.12);
"
></div>
<v-btn
variant="text"
prepend-icon="mdi-github"
href="https://github.com/AstrBotDevs/AstrBot_Plugins_Collection"
target="_blank"
color="primary"
class="text-none"
>
{{ tm("market.submitRepo") }}
</v-btn>
</div>
</v-col>
</v-row>
<!-- 配置对话框 -->
<v-dialog v-model="configDialog" max-width="900">
<v-card>
<v-card-title class="text-h2 pa-4 pl-6 pb-0">{{
tm("dialogs.config.title")
}}</v-card-title>
<v-card-text>
<div style="max-height: 60vh; overflow-y: auto; padding-right: 8px">
<AstrBotConfig
v-if="extension_config.metadata"
:metadata="extension_config.metadata"
:iterable="extension_config.config"
:metadataKey="curr_namespace"
:pluginName="curr_namespace"
/>
<p v-else>{{ tm("dialogs.config.noConfig") }}</p>
</div>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn color="blue-darken-1" variant="text" @click="updateConfig">{{
tm("buttons.saveAndClose")
}}</v-btn>
<v-btn
color="blue-darken-1"
variant="text"
@click="configDialog = false"
>{{ tm("buttons.close") }}</v-btn
>
</v-card-actions>
</v-card>
</v-dialog>
<!-- 加载对话框 -->
<v-dialog v-model="loadingDialog.show" width="700" persistent>
<v-card>
<v-card-title class="text-h5">{{ loadingDialog.title }}</v-card-title>
<v-card-text style="max-height: calc(100vh - 200px); overflow-y: auto">
<v-progress-linear
v-if="loadingDialog.statusCode === 0"
indeterminate
color="primary"
class="mb-4"
></v-progress-linear>
<div v-if="loadingDialog.statusCode !== 0" class="py-8 text-center">
<v-icon
class="mb-6"
:color="loadingDialog.statusCode === 1 ? 'success' : 'error'"
:icon="
loadingDialog.statusCode === 1
? 'mdi-check-circle-outline'
: 'mdi-alert-circle-outline'
"
size="128"
></v-icon>
<div class="text-h4 font-weight-bold">{{ loadingDialog.result }}</div>
</div>
<div style="margin-top: 32px">
<h3>{{ tm("dialogs.loading.logs") }}</h3>
<ConsoleDisplayer
historyNum="10"
style="height: 200px; margin-top: 16px; margin-bottom: 24px"
>
</ConsoleDisplayer>
</div>
</v-card-text>
<v-divider></v-divider>
<v-card-actions class="pa-4">
<v-spacer></v-spacer>
<v-btn
color="blue-darken-1"
variant="text"
@click="resetLoadingDialog"
>{{ tm("buttons.close") }}</v-btn
>
</v-card-actions>
</v-card>
</v-dialog>
<!-- 插件信息对话框 -->
<v-dialog v-model="showPluginInfoDialog" width="1200">
<v-card>
<v-card-title class="text-h5"
>{{ selectedPlugin.name }} {{ tm("buttons.viewInfo") }}</v-card-title
>
<v-card-text>
<v-data-table
style="font-size: 17px"
:headers="plugin_handler_info_headers"
:items="selectedPlugin.handlers"
item-key="name"
>
<template v-slot:header.id="{ column }">
<p style="font-weight: bold">{{ column.title }}</p>
</template>
<template v-slot:item.event_type="{ item }">
{{ item.event_type }}
</template>
<template v-slot:item.desc="{ item }">
{{ item.desc }}
</template>
<template v-slot:item.type="{ item }">
<v-chip color="success">
{{ item.type }}
</v-chip>
</template>
<template v-slot:item.cmd="{ item }">
<span style="font-weight: bold">{{ item.cmd }}</span>
</template>
</v-data-table>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn
color="blue-darken-1"
variant="text"
@click="showPluginInfoDialog = false"
>{{ tm("buttons.close") }}</v-btn
>
</v-card-actions>
</v-card>
</v-dialog>
<v-snackbar
:timeout="2000"
elevation="24"
:color="snack_success"
v-model="snack_show"
>
{{ snack_message }}
</v-snackbar>
<ReadmeDialog
v-model:show="readmeDialog.show"
:plugin-name="readmeDialog.pluginName"
:repo-url="readmeDialog.repoUrl"
/>
<!-- 插件更新日志对话框(复用 ReadmeDialog) -->
<ReadmeDialog
v-model:show="changelogDialog.show"
:plugin-name="changelogDialog.pluginName"
:repo-url="changelogDialog.repoUrl"
mode="changelog"
/>
<!-- 卸载插件确认对话框(列表模式用) -->
<UninstallConfirmDialog
v-model="showUninstallDialog"
@confirm="handleUninstallConfirm"
/>
<!-- 更新全部插件确认对话框 -->
<v-dialog v-model="updateAllConfirmDialog.show" max-width="420">
<v-card class="rounded-lg">
<v-card-title class="d-flex align-center pa-4">
<v-icon color="warning" class="mr-2">mdi-update</v-icon>
{{ tm("dialogs.updateAllConfirm.title") }}
</v-card-title>
<v-card-text>
<p class="text-body-1">
{{ tm("dialogs.updateAllConfirm.message", { count: updatableExtensions.length }) }}
</p>
</v-card-text>
<v-card-actions class="pa-4">
<v-spacer></v-spacer>
<v-btn
variant="text"
@click="cancelUpdateAll"
>{{ tm("buttons.cancel") }}</v-btn>
<v-btn
color="warning"
variant="flat"
@click="confirmUpdateAll"
>{{ tm("dialogs.updateAllConfirm.confirm") }}</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<!-- 指令冲突提示对话框 -->
<v-dialog v-model="conflictDialog.show" max-width="420">
<v-card class="rounded-lg">
<v-card-title class="d-flex align-center pa-4">
<v-icon color="warning" class="mr-2">mdi-alert-circle</v-icon>
{{ tm("conflicts.title") }}
</v-card-title>
<v-card-text class="px-4 pb-2">
<div class="d-flex align-center mb-3">
<v-chip
color="warning"
variant="tonal"
size="large"
class="font-weight-bold"
>
{{ conflictDialog.count }}
</v-chip>
<span class="ml-2 text-body-1">{{ tm("conflicts.pairs") }}</span>
</div>
<p
class="text-body-2"
style="color: rgba(var(--v-theme-on-surface), 0.7)"
>
{{ tm("conflicts.message") }}
</p>
</v-card-text>
<v-card-actions class="pa-4 pt-2">
<v-spacer></v-spacer>
<v-btn variant="text" @click="conflictDialog.show = false">{{
tm("conflicts.later")
}}</v-btn>
<v-btn color="warning" variant="flat" @click="handleConflictConfirm">
{{ tm("conflicts.goToManage") }}
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<!-- 危险插件确认对话框 -->
<v-dialog v-model="dangerConfirmDialog" width="500" persistent>
<v-card>
<v-card-title class="text-h5 d-flex align-center">
<v-icon color="warning" class="mr-2">mdi-alert-circle</v-icon>
{{ tm("dialogs.danger_warning.title") }}
</v-card-title>
<v-card-text>
<div>{{ tm("dialogs.danger_warning.message") }}</div>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn color="grey" @click="cancelDangerInstall">
{{ tm("dialogs.danger_warning.cancel") }}
</v-btn>
<v-btn color="warning" @click="confirmDangerInstall">
{{ tm("dialogs.danger_warning.confirm") }}
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<!-- 版本不兼容警告对话框 -->
<v-dialog v-model="versionCompatibilityDialog.show" width="520" persistent>
<v-card>
<v-card-title class="text-h5 d-flex align-center">
<v-icon color="warning" class="mr-2">mdi-alert</v-icon>
{{ tm("dialogs.versionCompatibility.title") }}
</v-card-title>
<v-card-text>
<div class="mb-2">{{ tm("dialogs.versionCompatibility.message") }}</div>
<div class="text-medium-emphasis">
{{ versionCompatibilityDialog.message }}
</div>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn color="grey" @click="cancelInstallOnVersionWarning">
{{ tm("dialogs.versionCompatibility.cancel") }}
</v-btn>
<v-btn color="warning" @click="continueInstallIgnoringVersionWarning">
{{ tm("dialogs.versionCompatibility.confirm") }}
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<!-- 上传插件对话框 -->
<v-dialog v-model="dialog" width="500">
<div
class="v-card v-card--density-default rounded-lg v-card--variant-elevated"
>
<div class="v-card__loader">
<v-progress-linear
:indeterminate="loading_"
color="primary"
height="2"
:active="loading_"
></v-progress-linear>
</div>
<v-card-title class="text-h3 pa-4 pb-0 pl-6">
{{ tm("dialogs.install.title") }}
</v-card-title>
<div class="v-card-text">
<v-tabs v-model="uploadTab" color="primary">
<v-tab value="file">{{ tm("dialogs.install.fromFile") }}</v-tab>
<v-tab value="url">{{ tm("dialogs.install.fromUrl") }}</v-tab>
</v-tabs>
<v-window v-model="uploadTab" class="mt-4">
<v-window-item value="file">
<div class="d-flex flex-column align-center justify-center pa-4">
<v-file-input
ref="fileInput"
v-model="upload_file"
:label="tm('upload.selectFile')"
accept=".zip"
hide-details
hide-input
class="d-none"
></v-file-input>
<v-btn
color="primary"
size="large"
prepend-icon="mdi-upload"
@click="$refs.fileInput.click()"
elevation="2"
>
{{ tm("buttons.selectFile") }}
</v-btn>
<div class="text-body-2 text-medium-emphasis mt-2">
{{ tm("messages.supportedFormats") }}
</div>
<div v-if="upload_file" class="mt-4 text-center">
<v-chip
color="primary"
size="large"
closable
@click:close="upload_file = null"
>
{{ upload_file.name }}
<template v-slot:append>
<span class="text-caption ml-2"
>({{ (upload_file.size / 1024).toFixed(1) }}KB)</span
>
</template>
</v-chip>
</div>
</div>
</v-window-item>
<v-window-item value="url">
<div class="pa-4">
<v-text-field
v-model="extension_url"
:label="tm('upload.enterUrl')"
variant="outlined"
prepend-inner-icon="mdi-link"
hide-details
class="rounded-lg mb-4"
placeholder="https://github.com/username/repo"
></v-text-field>
<div v-if="selectedInstallPlugin" class="mb-3">
<v-chip
v-if="selectedInstallPlugin.astrbot_version"
size="small"
color="secondary"
variant="outlined"
class="mr-2 mb-2"
>
{{ tm("card.status.astrbotVersion") }}:
{{ selectedInstallPlugin.astrbot_version }}
</v-chip>
<v-chip
v-if="normalizePlatformList(selectedInstallPlugin.support_platforms).length"
size="small"
color="info"
variant="outlined"
class="mb-2"
>
{{ tm("card.status.supportPlatform") }}:
{{
getPlatformDisplayList(selectedInstallPlugin.support_platforms).join(
", ",
)
}}
</v-chip>
<v-alert
v-if="
selectedInstallPlugin.astrbot_version &&
installCompat.checked &&
!installCompat.compatible
"
type="warning"
variant="tonal"
density="comfortable"
class="mt-2"
>
{{ installCompat.message }}
</v-alert>
</div>
<ProxySelector></ProxySelector>
</div>
</v-window-item>
</v-window>
</div>
<div class="v-card-actions">
<v-spacer></v-spacer>
<v-btn color="grey" variant="text" @click="dialog = false">{{
tm("buttons.cancel")
}}</v-btn>
<v-btn color="primary" variant="text" @click="newExtension">{{
tm("buttons.install")
}}</v-btn>
</div>
</div>
</v-dialog>
<!-- 插件源管理对话框 -->
<v-dialog v-model="showSourceManagerDialog" width="640">
<v-card>
<v-card-title class="text-h3 pa-4 pl-6">{{
tm("market.sourceManagement")
}}</v-card-title>
<v-card-text>
<v-select
:model-value="selectedSource || '__default__'"
@update:model-value="
selectPluginSource($event === '__default__' ? null : $event)
"
:items="sourceSelectItems"
:label="tm('market.currentSource')"
variant="outlined"
prepend-inner-icon="mdi-source-branch"
hide-details
class="mb-4"
></v-select>
<div class="d-flex align-center justify-space-between mb-2">
<div class="text-subtitle-2">{{ tm("market.availableSources") }}</div>
<v-btn
size="small"
color="primary"
variant="tonal"
prepend-icon="mdi-plus"
@click="addCustomSource"
>
{{ tm("market.addSource") }}
</v-btn>
</div>
<v-list density="compact" nav class="pa-0">
<v-list-item
rounded="md"
color="primary"
:active="selectedSource === null"
@click="selectPluginSource(null)"
>
<template v-slot:prepend>
<v-icon icon="mdi-shield-check" size="small" class="mr-2"></v-icon>
</template>
<v-list-item-title>{{ tm("market.defaultSource") }}</v-list-item-title>
</v-list-item>
<v-list-item
v-for="source in customSources"
:key="source.url"
rounded="md"
color="primary"
:active="selectedSource === source.url"
@click="selectPluginSource(source.url)"
>
<template v-slot:prepend>
<v-icon icon="mdi-link-variant" size="small" class="mr-2"></v-icon>
</template>
<v-list-item-title>{{ source.name }}</v-list-item-title>
<v-list-item-subtitle class="text-caption">{{
source.url
}}</v-list-item-subtitle>
<template v-slot:append>
<v-btn
icon="mdi-pencil-outline"
size="small"
variant="text"
color="medium-emphasis"
@click.stop="editCustomSource(source)"
></v-btn>
<v-btn
icon="mdi-trash-can-outline"
size="small"
variant="text"
color="error"
@click.stop="removeCustomSource(source)"
></v-btn>
</template>
</v-list-item>
</v-list>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn color="primary" variant="text" @click="showSourceManagerDialog = false">{{
tm("buttons.close")
}}</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<!-- 添加/编辑自定义插件源对话框 -->
<v-dialog v-model="showSourceDialog" width="500">
<v-card>
<v-card-title class="text-h5">{{
editingSource ? tm("market.editSource") : tm("market.addSource")
}}</v-card-title>
<v-card-text>
<div class="pa-2">
<v-text-field
v-model="sourceName"
:label="tm('market.sourceName')"
variant="outlined"
prepend-inner-icon="mdi-rename-box"
hide-details
class="mb-4"
placeholder="我的插件源"
></v-text-field>
<v-text-field
v-model="sourceUrl"
:label="tm('market.sourceUrl')"
variant="outlined"
prepend-inner-icon="mdi-link"
hide-details
placeholder="https://example.com/plugins.json"
></v-text-field>
<div class="text-caption text-medium-emphasis mt-2">
{{ tm("messages.enterJsonUrl") }}
</div>
</div>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn color="grey" variant="text" @click="showSourceDialog = false">{{
tm("buttons.cancel")
}}</v-btn>
<v-btn color="primary" variant="text" @click="saveCustomSource">{{
tm("buttons.save")
}}</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<!-- 删除插件源确认对话框 -->
<v-dialog v-model="showRemoveSourceDialog" width="400">
<v-card>
<v-card-title class="text-h5 d-flex align-center">
<v-icon color="warning" class="mr-2">mdi-alert-circle</v-icon>
{{ tm("dialogs.uninstall.title") }}
</v-card-title>
<v-card-text>
<div>{{ tm("market.confirmRemoveSource") }}</div>
<div v-if="sourceToRemove" class="mt-2">
<strong>{{ sourceToRemove.name }}</strong>
<div class="text-caption">{{ sourceToRemove.url }}</div>
</div>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn
color="grey"
variant="text"
@click="showRemoveSourceDialog = false"
>{{ tm("buttons.cancel") }}</v-btn
>
<v-btn color="error" variant="text" @click="confirmRemoveSource">{{
tm("buttons.deleteSource")
}}</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<!-- 强制更新确认对话框 -->
<v-dialog v-model="forceUpdateDialog.show" max-width="420">
<v-card class="rounded-lg">
<v-card-title class="text-h6 d-flex align-center">
<v-icon color="info" class="mr-2">mdi-information-outline</v-icon>
{{ tm("dialogs.forceUpdate.title") }}
</v-card-title>
<v-card-text>
{{ tm("dialogs.forceUpdate.message") }}
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn variant="text" @click="forceUpdateDialog.show = false">{{
tm("buttons.cancel")
}}</v-btn>
<v-btn color="primary" variant="flat" @click="confirmForceUpdate">{{
tm("dialogs.forceUpdate.confirm")
}}</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</template>
<style scoped>
.plugin-handler-item {
margin-bottom: 10px;
padding: 5px;
border-radius: 5px;
background-color: #f5f5f5;
}
.fab-button {
transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.fab-button:hover {
transform: translateY(-4px) scale(1.05);
box-shadow: 0 12px 20px rgba(var(--v-theme-primary), 0.4);
}
</style>