Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions dashboard/src/i18n/locales/en-US/features/extension.json
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@
"randomPlugins": "🎲 Random Plugins",
"showRandomPlugins": "Show Random Plugins",
"hideRandomPlugins": "Hide Random Plugins",
"itemsPerPage": "Items per page",
"sourceSafetyWarning": "Even with the default source, plugin stability and security cannot be fully guaranteed. Please verify carefully before use."
},
"sort": {
Expand Down
1 change: 1 addition & 0 deletions dashboard/src/i18n/locales/zh-CN/features/extension.json
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@
"randomPlugins": "🎲 随机插件",
"showRandomPlugins": "显示随机插件",
"hideRandomPlugins": "隐藏随机插件",
"itemsPerPage": "每页显示",
"sourceSafetyWarning": "即使是默认插件源,我们也不能完全保证插件的稳定性和安全性,使用前请谨慎核查。"
},
"sort": {
Expand Down
253 changes: 245 additions & 8 deletions dashboard/src/views/extension/MarketPluginsTab.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<script setup>
import MarketPluginCard from "@/components/extension/MarketPluginCard.vue";
import PluginSortControl from "@/components/extension/PluginSortControl.vue";
import PluginPlatformChip from "@/components/shared/PluginPlatformChip.vue";
import defaultPluginIcon from "@/assets/images/plugin_icon.png";
import { computed } from "vue";
import { normalizeTextInput } from "@/utils/inputValue";
Expand Down Expand Up @@ -74,6 +75,7 @@ const {
upload_file,
uploadTab,
showPluginFullName,
marketIsListView,
marketSearch,
debouncedMarketSearch,
refreshingMarket,
Expand All @@ -84,6 +86,7 @@ const {
normalizeStr,
toPinyinText,
toInitials,
marketPluginHeaders,
plugin_handler_info_headers,
pluginHeaders,
filteredExtensions,
Expand All @@ -95,6 +98,8 @@ const {
shufflePlugins,
refreshRandomPlugins,
toggleRandomPluginsVisibility,
marketItemsPerPage,
marketItemsPerPageOptions,
displayItemsPerPage,
totalPages,
paginatedPlugins,
Expand Down Expand Up @@ -348,10 +353,171 @@ const marketSortItems = computed(() => [
:show-order="sortBy !== 'default'"
@update:order="sortOrder = $event"
/>

<v-btn-toggle
v-model="marketIsListView"
mandatory
density="compact"
color="primary"
class="view-mode-toggle"
>
<v-btn :value="false" icon="mdi-view-grid" :title="tm('views.card')"></v-btn>
<v-btn :value="true" icon="mdi-view-list" :title="tm('views.list')"></v-btn>
</v-btn-toggle>
</div>
</div>

<v-row style="min-height: 26rem" dense>
<div v-if="marketIsListView">
<v-card class="rounded-lg overflow-hidden elevation-0">
<v-data-table
class="plugin-list-table"
:headers="marketPluginHeaders"
:items="paginatedPlugins"
:items-per-page="marketItemsPerPage"
item-key="name"
hover
hide-default-footer
>
<template v-slot:item.name="{ item }">
<div class="d-flex align-center py-2">
<div class="mr-3" style="flex-shrink: 0">
<img
:src="item.logo || defaultPluginIcon"
:alt="item.name"
style="height: 40px; width: 40px; border-radius: 8px; object-fit: cover"
/>
</div>
<div style="min-width: 0">
<div class="d-flex align-center flex-wrap" style="gap: 6px">
<div class="text-h5" style="font-family: inherit;">
{{
item.display_name && item.display_name.length
? item.display_name
: showPluginFullName
? item.name
: item.trimmedName
}}
</div>
<v-chip v-if="item.pinned" color="warning" size="x-small" label>
{{ tm("market.recommended") }}
</v-chip>
<v-chip v-if="item.installed" color="success" size="x-small" label>
{{ tm("status.installed") }}
</v-chip>
</div>
<div
v-if="item.display_name && item.display_name.length"
class="text-caption text-medium-emphasis mt-1"
>
{{ item.name }}
</div>
</div>
</div>
</template>

<template v-slot:item.desc="{ item }">
<div class="py-2">
<div
class="text-body-2 text-medium-emphasis"
style="display: -webkit-box; -webkit-line-clamp: 2; line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; text-overflow: ellipsis"
>
{{ item.desc }}
</div>

<div class="d-flex align-center flex-wrap mt-2" style="gap: 6px">
<PluginPlatformChip
v-if="item.support_platforms?.length"
:platforms="item.support_platforms"
size="x-small"
:chip-style="{ height: '20px' }"
/>

<v-chip
v-if="item.astrbot_version"
size="x-small"
color="secondary"
variant="outlined"
style="height: 20px"
>
{{ tm("card.status.astrbotVersion") }}: {{ item.astrbot_version }}
</v-chip>

<v-chip
v-if="item.stars !== undefined"
size="x-small"
color="warning"
variant="outlined"
style="height: 20px"
>
Star {{ item.stars }}
</v-chip>

<v-chip
v-if="item.updated_at"
size="x-small"
variant="outlined"
style="height: 20px"
>
{{ new Date(item.updated_at).toLocaleDateString() }}
</v-chip>
</div>
</div>
</template>

<template v-slot:item.version="{ item }">
<div class="text-body-2">{{ item.version }}</div>
</template>

<template v-slot:item.author="{ item }">
<div class="text-body-2">{{ item.author }}</div>
</template>

<template v-slot:item.actions="{ item }">
<div class="market-list-actions py-1">
<v-btn
v-if="item.repo"
size="small"
variant="tonal"
color="secondary"
prepend-icon="mdi-github"
:href="item.repo"
target="_blank"
>
{{ tm("buttons.viewRepo") }}
</v-btn>

<v-btn
v-if="!item.installed"
size="small"
color="primary"
variant="flat"
@click="handleInstallPlugin(item)"
>
{{ tm("buttons.install") }}
</v-btn>

<v-chip v-else color="success" size="x-small" label>
✓ {{ tm("status.installed") }}
</v-chip>
</div>
</template>

<template v-slot:no-data>
<div class="text-center pa-8">
<v-icon size="64" color="info" class="mb-4">mdi-puzzle-outline</v-icon>
<div class="text-h5 mb-2">
{{ tm("empty.noPlugins") }}
</div>
<div class="text-body-1 mb-4">
{{ tm("empty.noPluginsDesc") }}
</div>
</div>
</template>
</v-data-table>
</v-card>
</div>

<v-row v-else style="min-height: 26rem" dense>
<v-col
v-for="plugin in paginatedPlugins"
:key="plugin.name"
Expand All @@ -369,14 +535,85 @@ const marketSortItems = computed(() => [
</v-col>
</v-row>

<div class="d-flex justify-center mt-4" v-if="totalPages > 1">
<v-pagination
v-model="currentPage"
:length="totalPages"
:total-visible="7"
size="small"
></v-pagination>
<div
v-if="sortedPlugins.length > 0"
class="market-pagination-footer"
>
<div class="market-pagination-footer__spacer"></div>

<div class="market-pagination-footer__pagination">
<v-pagination
v-if="totalPages > 1"
v-model="currentPage"
:length="totalPages"
:total-visible="7"
size="small"
></v-pagination>
</div>

<div class="market-pagination-footer__page-size">
<span class="text-caption text-medium-emphasis">
{{ tm("market.itemsPerPage") }}
</span>
<v-select
v-model="marketItemsPerPage"
:items="marketItemsPerPageOptions"
density="compact"
variant="outlined"
hide-details
class="market-pagination-footer__select"
></v-select>
</div>
</div>
</div>
</v-tab-item>
</template>

<style scoped>
.market-pagination-footer {
display: grid;
grid-template-columns: 1fr auto 1fr;
align-items: center;
gap: 12px;
margin-top: 16px;
}

.market-pagination-footer__pagination {
display: flex;
justify-content: center;
}

.market-list-actions {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: flex-start;
gap: 8px;
}

.market-pagination-footer__page-size {
display: flex;
justify-content: flex-end;
align-items: center;
gap: 8px;
}

.market-pagination-footer__select {
width: 110px;
}

@media (max-width: 960px) {
.market-pagination-footer {
grid-template-columns: 1fr;
}

.market-pagination-footer__spacer {
display: none;
}

.market-pagination-footer__pagination,
.market-pagination-footer__page-size {
justify-content: center;
}
}
</style>
Loading
Loading