mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-09-07 00:36:41 +08:00
feat: add wiki sync to plugin market settings
This commit is contained in:
@@ -24,11 +24,14 @@ const repoText = ref('')
|
|||||||
const newRepoUrl = ref('')
|
const newRepoUrl = ref('')
|
||||||
const editingIndex = ref<number | null>(null)
|
const editingIndex = ref<number | null>(null)
|
||||||
const editingUrl = ref('')
|
const editingUrl = ref('')
|
||||||
|
const syncingWiki = ref(false)
|
||||||
|
|
||||||
const emit = defineEmits(['save', 'close'])
|
const emit = defineEmits(['save', 'close'])
|
||||||
|
|
||||||
const parsedTextRepos = computed(() => parseRepoInput(repoText.value))
|
const parsedTextRepos = computed(() => parseRepoInput(repoText.value))
|
||||||
const activeRepoCount = computed(() => (editorMode.value === 'text' ? parsedTextRepos.value.repos.length : repoList.value.length))
|
const activeRepoCount = computed(() =>
|
||||||
|
editorMode.value === 'text' ? parsedTextRepos.value.repos.length : repoList.value.length,
|
||||||
|
)
|
||||||
const saveDisabled = computed(
|
const saveDisabled = computed(
|
||||||
() => activeRepoCount.value === 0 || (editorMode.value === 'text' && parsedTextRepos.value.invalidRepos.length > 0),
|
() => activeRepoCount.value === 0 || (editorMode.value === 'text' && parsedTextRepos.value.invalidRepos.length > 0),
|
||||||
)
|
)
|
||||||
@@ -136,6 +139,35 @@ async function saveHandle() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 从 Wiki 同步公开插件仓库清单并写入配置。 */
|
||||||
|
async function syncWikiRepos() {
|
||||||
|
try {
|
||||||
|
syncingWiki.value = true
|
||||||
|
const result: { [key: string]: any } = await api.post('system/setting/PLUGIN_MARKET/sync-wiki', {})
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
const repos = Array.isArray(result.data?.repos)
|
||||||
|
? result.data.repos
|
||||||
|
: parseRepoInput(result.data?.value || '').repos
|
||||||
|
repoList.value = repos
|
||||||
|
syncTextFromList()
|
||||||
|
$toast.success(
|
||||||
|
t('dialog.pluginMarketSetting.syncSuccess', {
|
||||||
|
added: result.data?.added_count ?? 0,
|
||||||
|
total: result.data?.total_count ?? repos.length,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
$toast.error(t('dialog.pluginMarketSetting.syncFailed', { message: result?.message }))
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error)
|
||||||
|
$toast.error(t('dialog.pluginMarketSetting.syncFailed', { message: error instanceof Error ? error.message : '' }))
|
||||||
|
} finally {
|
||||||
|
syncingWiki.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** 获取当前维护模式下可保存的仓库地址。 */
|
/** 获取当前维护模式下可保存的仓库地址。 */
|
||||||
function normalizeCurrentRepos() {
|
function normalizeCurrentRepos() {
|
||||||
if (editorMode.value === 'text') {
|
if (editorMode.value === 'text') {
|
||||||
@@ -224,8 +256,8 @@ function formatRepoDisplay(url: string) {
|
|||||||
const pathSegments = parsedUrl.pathname.split('/').filter(Boolean)
|
const pathSegments = parsedUrl.pathname.split('/').filter(Boolean)
|
||||||
|
|
||||||
if (
|
if (
|
||||||
['github.com', 'www.github.com', 'raw.githubusercontent.com'].includes(parsedUrl.hostname)
|
['github.com', 'www.github.com', 'raw.githubusercontent.com'].includes(parsedUrl.hostname) &&
|
||||||
&& pathSegments.length >= 2
|
pathSegments.length >= 2
|
||||||
) {
|
) {
|
||||||
return `${pathSegments[0]}/${pathSegments[1].replace(/\.git$/, '')}`
|
return `${pathSegments[0]}/${pathSegments[1].replace(/\.git$/, '')}`
|
||||||
}
|
}
|
||||||
@@ -261,22 +293,44 @@ onMounted(() => {
|
|||||||
|
|
||||||
<VCardText class="plugin-market-dialog-body pt-4">
|
<VCardText class="plugin-market-dialog-body pt-4">
|
||||||
<div class="plugin-market-toolbar">
|
<div class="plugin-market-toolbar">
|
||||||
<VBtnToggle
|
<div class="plugin-market-toolbar-hint">
|
||||||
:model-value="editorMode"
|
<VIcon icon="mdi-information-outline" size="18" />
|
||||||
mandatory
|
<span>{{ t('dialog.pluginMarketSetting.repoCountHint', { count: activeRepoCount }) }}</span>
|
||||||
color="primary"
|
</div>
|
||||||
density="comfortable"
|
<div class="plugin-market-mode-switch" role="tablist" :aria-label="t('dialog.pluginMarketSetting.title')">
|
||||||
variant="tonal"
|
<VTooltip :text="t('dialog.pluginMarketSetting.listMode')" location="top">
|
||||||
class="plugin-market-mode-toggle"
|
<template #activator="{ props }">
|
||||||
@update:model-value="switchEditorMode"
|
<button
|
||||||
>
|
v-bind="props"
|
||||||
<VBtn value="list" prepend-icon="mdi-format-list-bulleted">
|
type="button"
|
||||||
{{ t('dialog.pluginMarketSetting.listMode') }}
|
class="plugin-market-mode-button"
|
||||||
</VBtn>
|
:class="{ 'is-active': editorMode === 'list' }"
|
||||||
<VBtn value="text" prepend-icon="mdi-text-box-edit-outline">
|
role="tab"
|
||||||
{{ t('dialog.pluginMarketSetting.textMode') }}
|
:aria-label="t('dialog.pluginMarketSetting.listMode')"
|
||||||
</VBtn>
|
:aria-selected="editorMode === 'list'"
|
||||||
</VBtnToggle>
|
@click="switchEditorMode('list')"
|
||||||
|
>
|
||||||
|
<VIcon icon="mdi-format-list-bulleted" size="20" />
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
</VTooltip>
|
||||||
|
<VTooltip :text="t('dialog.pluginMarketSetting.textMode')" location="top">
|
||||||
|
<template #activator="{ props }">
|
||||||
|
<button
|
||||||
|
v-bind="props"
|
||||||
|
type="button"
|
||||||
|
class="plugin-market-mode-button"
|
||||||
|
:class="{ 'is-active': editorMode === 'text' }"
|
||||||
|
role="tab"
|
||||||
|
:aria-label="t('dialog.pluginMarketSetting.textMode')"
|
||||||
|
:aria-selected="editorMode === 'text'"
|
||||||
|
@click="switchEditorMode('text')"
|
||||||
|
>
|
||||||
|
<VIcon icon="mdi-text-box-edit-outline" size="20" />
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
</VTooltip>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="editorMode === 'list'" class="plugin-market-list-panel">
|
<div v-if="editorMode === 'list'" class="plugin-market-list-panel">
|
||||||
@@ -425,6 +479,16 @@ onMounted(() => {
|
|||||||
</VCardText>
|
</VCardText>
|
||||||
|
|
||||||
<VCardActions class="plugin-market-actions">
|
<VCardActions class="plugin-market-actions">
|
||||||
|
<VBtn
|
||||||
|
color="primary"
|
||||||
|
variant="tonal"
|
||||||
|
prepend-icon="mdi-cloud-sync-outline"
|
||||||
|
:loading="syncingWiki"
|
||||||
|
:disabled="syncingWiki"
|
||||||
|
@click="syncWikiRepos"
|
||||||
|
>
|
||||||
|
{{ t('dialog.pluginMarketSetting.syncWiki') }}
|
||||||
|
</VBtn>
|
||||||
<VSpacer />
|
<VSpacer />
|
||||||
<VBtn
|
<VBtn
|
||||||
color="primary"
|
color="primary"
|
||||||
@@ -478,14 +542,70 @@ onMounted(() => {
|
|||||||
.plugin-market-toolbar {
|
.plugin-market-toolbar {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 0.75rem;
|
||||||
|
min-block-size: 2.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.plugin-market-mode-toggle {
|
.plugin-market-toolbar-hint {
|
||||||
inline-size: 100%;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
background: rgba(var(--v-theme-info), 0.08);
|
||||||
|
color: rgb(var(--v-theme-info));
|
||||||
|
font-size: 0.875rem;
|
||||||
|
gap: 0.5rem;
|
||||||
|
min-inline-size: 0;
|
||||||
|
padding-block: 0.5rem;
|
||||||
|
padding-inline: 1rem;
|
||||||
|
|
||||||
:deep(.v-btn) {
|
span {
|
||||||
flex: 1;
|
overflow: hidden;
|
||||||
min-inline-size: 0;
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.plugin-market-mode-switch {
|
||||||
|
display: inline-flex;
|
||||||
|
padding: 0.125rem;
|
||||||
|
border: 1px solid rgba(var(--v-theme-on-surface), 0.08);
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
background: rgba(var(--v-theme-surface), 0.72);
|
||||||
|
gap: 0.125rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plugin-market-mode-button {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 0;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
background: transparent;
|
||||||
|
block-size: 2.25rem;
|
||||||
|
color: rgba(var(--v-theme-on-surface), 0.68);
|
||||||
|
cursor: pointer;
|
||||||
|
font: inherit;
|
||||||
|
inline-size: 2.25rem;
|
||||||
|
transition:
|
||||||
|
background-color 0.16s ease,
|
||||||
|
color 0.16s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: rgba(var(--v-theme-primary), 0.07);
|
||||||
|
color: rgb(var(--v-theme-on-surface));
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus-visible {
|
||||||
|
outline: 2px solid rgba(var(--v-theme-primary), 0.48);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-active {
|
||||||
|
background: rgba(var(--v-theme-primary), 0.12);
|
||||||
|
color: rgb(var(--v-theme-primary));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -529,8 +649,8 @@ onMounted(() => {
|
|||||||
display: -webkit-box;
|
display: -webkit-box;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
-webkit-box-orient: vertical;
|
-webkit-box-orient: vertical;
|
||||||
-webkit-line-clamp: 2;
|
|
||||||
line-break: anywhere;
|
line-break: anywhere;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
overflow-wrap: anywhere;
|
overflow-wrap: anywhere;
|
||||||
white-space: normal;
|
white-space: normal;
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
@@ -550,20 +670,22 @@ onMounted(() => {
|
|||||||
|
|
||||||
.plugin-market-empty {
|
.plugin-market-empty {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
flex-direction: column;
|
|
||||||
min-block-size: 14rem;
|
min-block-size: 14rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.plugin-market-textarea-field {
|
.plugin-market-textarea-field {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
overflow: hidden;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
background: rgba(var(--v-theme-surface), 0.72);
|
background: rgba(var(--v-theme-surface), 0.72);
|
||||||
min-block-size: 0;
|
min-block-size: 0;
|
||||||
overflow: hidden;
|
transition:
|
||||||
transition: border-color 0.2s ease, box-shadow 0.2s ease;
|
border-color 0.2s ease,
|
||||||
|
box-shadow 0.2s ease;
|
||||||
|
|
||||||
&:focus-within {
|
&:focus-within {
|
||||||
border-color: rgb(var(--v-theme-primary));
|
border-color: rgb(var(--v-theme-primary));
|
||||||
@@ -586,13 +708,14 @@ onMounted(() => {
|
|||||||
background: transparent;
|
background: transparent;
|
||||||
block-size: 100%;
|
block-size: 100%;
|
||||||
color: rgb(var(--v-theme-on-surface));
|
color: rgb(var(--v-theme-on-surface));
|
||||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
|
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', monospace;
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
min-block-size: 0;
|
min-block-size: 0;
|
||||||
outline: none;
|
outline: none;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
padding: 1rem 1rem 1rem 3.25rem;
|
padding-block: 1rem;
|
||||||
|
padding-inline: 3.25rem 1rem;
|
||||||
resize: none;
|
resize: none;
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
@@ -615,16 +738,22 @@ onMounted(() => {
|
|||||||
.plugin-market-actions {
|
.plugin-market-actions {
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
padding: 0.75rem 1.5rem 1rem;
|
padding-block: 0.75rem 1rem;
|
||||||
|
padding-inline: 1.5rem;
|
||||||
|
|
||||||
|
:deep(.v-btn) {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 600px) {
|
@media (width <= 600px) {
|
||||||
.plugin-market-dialog-card {
|
.plugin-market-dialog-card {
|
||||||
block-size: 100dvh;
|
block-size: 100dvh;
|
||||||
}
|
}
|
||||||
|
|
||||||
.plugin-market-card-item {
|
.plugin-market-card-item {
|
||||||
padding: 0.75rem 1rem 0.625rem;
|
padding-block: 0.75rem 0.625rem;
|
||||||
|
padding-inline: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.plugin-market-header {
|
.plugin-market-header {
|
||||||
@@ -640,16 +769,22 @@ onMounted(() => {
|
|||||||
|
|
||||||
.plugin-market-dialog-body {
|
.plugin-market-dialog-body {
|
||||||
gap: 0.625rem;
|
gap: 0.625rem;
|
||||||
padding: 0.75rem 1rem !important;
|
padding-block: 0.75rem !important;
|
||||||
|
padding-inline: 1rem !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.plugin-market-mode-toggle {
|
.plugin-market-toolbar {
|
||||||
inline-size: 100%;
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
:deep(.v-btn) {
|
.plugin-market-mode-switch {
|
||||||
flex: 1;
|
flex: 0 0 auto;
|
||||||
min-inline-size: 0;
|
}
|
||||||
}
|
|
||||||
|
.plugin-market-toolbar-hint {
|
||||||
|
flex: 1 1 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.plugin-market-list-panel,
|
.plugin-market-list-panel,
|
||||||
@@ -666,7 +801,18 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.plugin-market-actions {
|
.plugin-market-actions {
|
||||||
padding: 0.75rem 1rem calc(0.75rem + env(safe-area-inset-bottom));
|
flex-wrap: wrap;
|
||||||
|
padding-block: 0.75rem calc(0.75rem + env(safe-area-inset-bottom));
|
||||||
|
padding-inline: 1rem;
|
||||||
|
|
||||||
|
:deep(.v-btn) {
|
||||||
|
flex: 1 1 9rem;
|
||||||
|
min-inline-size: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.v-spacer) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -2582,6 +2582,7 @@ export default {
|
|||||||
repoHint: 'Separate multiple URLs with new lines or commas',
|
repoHint: 'Separate multiple URLs with new lines or commas',
|
||||||
urlPlaceholder: 'Enter plugin repository URL',
|
urlPlaceholder: 'Enter plugin repository URL',
|
||||||
textPlaceholder: 'https://github.com/jxxghp/MoviePilot-Plugins/\nhttps://github.com/xxxx/xxxxxx/',
|
textPlaceholder: 'https://github.com/jxxghp/MoviePilot-Plugins/\nhttps://github.com/xxxx/xxxxxx/',
|
||||||
|
repoCountHint: '{count} plugin repository URLs maintained',
|
||||||
listMode: 'List',
|
listMode: 'List',
|
||||||
textMode: 'Text',
|
textMode: 'Text',
|
||||||
textHint: 'Paste repository URLs one per line or separated by commas.',
|
textHint: 'Paste repository URLs one per line or separated by commas.',
|
||||||
@@ -2592,6 +2593,9 @@ export default {
|
|||||||
invalidText: 'There are {count} invalid URLs in the text. Fix them before saving.',
|
invalidText: 'There are {count} invalid URLs in the text. Fix them before saving.',
|
||||||
invalidTextIgnored: '{count} invalid URLs ignored',
|
invalidTextIgnored: '{count} invalid URLs ignored',
|
||||||
duplicateTextIgnored: 'Duplicate URLs will be removed automatically when saving.',
|
duplicateTextIgnored: 'Duplicate URLs will be removed automatically when saving.',
|
||||||
|
syncWiki: 'Sync Wiki',
|
||||||
|
syncSuccess: 'Plugin repositories synced from Wiki. {added} added, {total} total.',
|
||||||
|
syncFailed: 'Failed to sync Wiki: {message}!',
|
||||||
close: 'Close',
|
close: 'Close',
|
||||||
save: 'Save',
|
save: 'Save',
|
||||||
saveSuccess: 'Plugin repository saved successfully',
|
saveSuccess: 'Plugin repository saved successfully',
|
||||||
|
|||||||
@@ -2534,6 +2534,7 @@ export default {
|
|||||||
repoHint: '多个地址可使用换行或英文逗号分隔',
|
repoHint: '多个地址可使用换行或英文逗号分隔',
|
||||||
urlPlaceholder: '输入插件仓库地址',
|
urlPlaceholder: '输入插件仓库地址',
|
||||||
textPlaceholder: 'https://github.com/jxxghp/MoviePilot-Plugins/\nhttps://github.com/xxxx/xxxxxx/',
|
textPlaceholder: 'https://github.com/jxxghp/MoviePilot-Plugins/\nhttps://github.com/xxxx/xxxxxx/',
|
||||||
|
repoCountHint: '当前已维护 {count} 个插件仓库地址',
|
||||||
listMode: '列表维护',
|
listMode: '列表维护',
|
||||||
textMode: '文本维护',
|
textMode: '文本维护',
|
||||||
textHint: '直接粘贴仓库地址串,一行一个或使用英文逗号分隔。',
|
textHint: '直接粘贴仓库地址串,一行一个或使用英文逗号分隔。',
|
||||||
@@ -2544,6 +2545,9 @@ export default {
|
|||||||
invalidText: '文本中有 {count} 个无效地址,请修正后保存。',
|
invalidText: '文本中有 {count} 个无效地址,请修正后保存。',
|
||||||
invalidTextIgnored: '已忽略 {count} 个无效地址',
|
invalidTextIgnored: '已忽略 {count} 个无效地址',
|
||||||
duplicateTextIgnored: '重复地址会在保存时自动去重。',
|
duplicateTextIgnored: '重复地址会在保存时自动去重。',
|
||||||
|
syncWiki: '同步 Wiki',
|
||||||
|
syncSuccess: '已从 Wiki 同步插件仓库,新增 {added} 个,共 {total} 个',
|
||||||
|
syncFailed: '同步 Wiki 失败:{message}!',
|
||||||
close: '关闭',
|
close: '关闭',
|
||||||
save: '保存',
|
save: '保存',
|
||||||
saveSuccess: '插件仓库保存成功',
|
saveSuccess: '插件仓库保存成功',
|
||||||
|
|||||||
@@ -2535,6 +2535,7 @@ export default {
|
|||||||
repoHint: '多個地址可使用換行或英文逗號分隔',
|
repoHint: '多個地址可使用換行或英文逗號分隔',
|
||||||
urlPlaceholder: '輸入插件倉庫地址',
|
urlPlaceholder: '輸入插件倉庫地址',
|
||||||
textPlaceholder: 'https://github.com/jxxghp/MoviePilot-Plugins/\nhttps://github.com/xxxx/xxxxxx/',
|
textPlaceholder: 'https://github.com/jxxghp/MoviePilot-Plugins/\nhttps://github.com/xxxx/xxxxxx/',
|
||||||
|
repoCountHint: '目前已維護 {count} 個插件倉庫地址',
|
||||||
listMode: '列表維護',
|
listMode: '列表維護',
|
||||||
textMode: '文字維護',
|
textMode: '文字維護',
|
||||||
textHint: '直接貼上倉庫地址串,一行一個或使用英文逗號分隔。',
|
textHint: '直接貼上倉庫地址串,一行一個或使用英文逗號分隔。',
|
||||||
@@ -2545,6 +2546,9 @@ export default {
|
|||||||
invalidText: '文字中有 {count} 個無效地址,請修正後儲存。',
|
invalidText: '文字中有 {count} 個無效地址,請修正後儲存。',
|
||||||
invalidTextIgnored: '已忽略 {count} 個無效地址',
|
invalidTextIgnored: '已忽略 {count} 個無效地址',
|
||||||
duplicateTextIgnored: '重複地址會在儲存時自動去重。',
|
duplicateTextIgnored: '重複地址會在儲存時自動去重。',
|
||||||
|
syncWiki: '同步 Wiki',
|
||||||
|
syncSuccess: '已從 Wiki 同步插件倉庫,新增 {added} 個,共 {total} 個',
|
||||||
|
syncFailed: '同步 Wiki 失敗:{message}!',
|
||||||
close: '關閉',
|
close: '關閉',
|
||||||
save: '儲存',
|
save: '儲存',
|
||||||
saveSuccess: '插件倉庫儲存成功',
|
saveSuccess: '插件倉庫儲存成功',
|
||||||
|
|||||||
Reference in New Issue
Block a user