feat: add wiki sync to plugin market settings

This commit is contained in:
jxxghp
2026-06-14 12:57:40 +08:00
parent 785cbcf81d
commit 7fc5b74851
4 changed files with 199 additions and 41 deletions
@@ -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>
+4
View File
@@ -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',
+4
View File
@@ -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: '插件仓库保存成功',
+4
View File
@@ -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: '插件倉庫儲存成功',