mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-09-06 16:16:42 +08:00
feat: optimize plugin market repository settings
This commit is contained in:
@@ -10,27 +10,121 @@ const display = useDisplay()
|
|||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
const $toast = useToast()
|
const $toast = useToast()
|
||||||
|
|
||||||
|
type EditorMode = 'list' | 'text'
|
||||||
|
|
||||||
|
interface RepoParseResult {
|
||||||
|
repos: string[]
|
||||||
|
invalidRepos: string[]
|
||||||
|
duplicateRepos: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const editorMode = ref<EditorMode>('list')
|
||||||
const repoList = ref<string[]>([])
|
const repoList = ref<string[]>([])
|
||||||
|
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 emit = defineEmits(['save', 'close'])
|
const emit = defineEmits(['save', 'close'])
|
||||||
|
|
||||||
|
const parsedTextRepos = computed(() => parseRepoInput(repoText.value))
|
||||||
|
const activeRepoCount = computed(() => (editorMode.value === 'text' ? parsedTextRepos.value.repos.length : repoList.value.length))
|
||||||
|
const saveDisabled = computed(
|
||||||
|
() => activeRepoCount.value === 0 || (editorMode.value === 'text' && parsedTextRepos.value.invalidRepos.length > 0),
|
||||||
|
)
|
||||||
|
|
||||||
|
/** 判断仓库地址是否为可保存的 HTTP URL。 */
|
||||||
|
function isValidRepoUrl(url: string) {
|
||||||
|
return /^https?:\/\//i.test(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 将粘贴的仓库地址文本解析为有效、无效和重复地址列表。 */
|
||||||
|
function parseRepoInput(value: string): RepoParseResult {
|
||||||
|
const repos: string[] = []
|
||||||
|
const invalidRepos: string[] = []
|
||||||
|
const duplicateRepos: string[] = []
|
||||||
|
const seenRepos = new Set<string>()
|
||||||
|
|
||||||
|
value
|
||||||
|
.split(/[\n,,]+/)
|
||||||
|
.map(repo => repo.trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
.forEach(repo => {
|
||||||
|
if (!isValidRepoUrl(repo)) {
|
||||||
|
invalidRepos.push(repo)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (seenRepos.has(repo)) {
|
||||||
|
duplicateRepos.push(repo)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
seenRepos.add(repo)
|
||||||
|
repos.push(repo)
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
repos,
|
||||||
|
invalidRepos,
|
||||||
|
duplicateRepos: [...new Set(duplicateRepos)],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 将列表模式中的仓库地址同步到文本模式。 */
|
||||||
|
function syncTextFromList() {
|
||||||
|
repoText.value = repoList.value.join('\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 将文本模式中的仓库地址同步到列表模式,并忽略无法加入列表的无效地址。 */
|
||||||
|
function syncListFromText() {
|
||||||
|
const result = parseRepoInput(repoText.value)
|
||||||
|
|
||||||
|
repoList.value = result.repos
|
||||||
|
syncTextFromList()
|
||||||
|
|
||||||
|
if (result.invalidRepos.length > 0) {
|
||||||
|
$toast.warning(t('dialog.pluginMarketSetting.invalidTextIgnored', { count: result.invalidRepos.length }))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 切换仓库维护模式,并在切换时同步当前模式的编辑内容。 */
|
||||||
|
function switchEditorMode(mode: EditorMode | undefined) {
|
||||||
|
if (!mode || mode === editorMode.value) return
|
||||||
|
|
||||||
|
if (editorMode.value === 'text') {
|
||||||
|
syncListFromText()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mode === 'text') {
|
||||||
|
syncTextFromList()
|
||||||
|
}
|
||||||
|
|
||||||
|
editorMode.value = mode
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 加载插件市场仓库配置。 */
|
||||||
async function queryMarketRepoSetting() {
|
async function queryMarketRepoSetting() {
|
||||||
try {
|
try {
|
||||||
const result: { [key: string]: any } = await api.get('system/setting/PLUGIN_MARKET')
|
const result: { [key: string]: any } = await api.get('system/setting/PLUGIN_MARKET')
|
||||||
if (result && result.data && result.data.value) {
|
if (result && result.data && result.data.value) {
|
||||||
repoList.value = result.data.value.split(',').filter((repo: string) => repo.trim() !== '')
|
repoList.value = parseRepoInput(result.data.value).repos
|
||||||
|
syncTextFromList()
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error)
|
console.log(error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 保存插件市场仓库配置。 */
|
||||||
async function saveHandle() {
|
async function saveHandle() {
|
||||||
try {
|
try {
|
||||||
const repoStringToSave = repoList.value.join(',')
|
const reposToSave = normalizeCurrentRepos()
|
||||||
|
if (!reposToSave) return
|
||||||
|
|
||||||
|
const repoStringToSave = reposToSave.join(',')
|
||||||
const result: { [key: string]: any } = await api.post('system/setting/PLUGIN_MARKET', repoStringToSave)
|
const result: { [key: string]: any } = await api.post('system/setting/PLUGIN_MARKET', repoStringToSave)
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
@@ -42,54 +136,88 @@ async function saveHandle() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 获取当前维护模式下可保存的仓库地址。 */
|
||||||
|
function normalizeCurrentRepos() {
|
||||||
|
if (editorMode.value === 'text') {
|
||||||
|
const result = parseRepoInput(repoText.value)
|
||||||
|
|
||||||
|
if (result.invalidRepos.length > 0) {
|
||||||
|
$toast.error(t('dialog.pluginMarketSetting.invalidText', { count: result.invalidRepos.length }))
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
repoList.value = result.repos
|
||||||
|
syncTextFromList()
|
||||||
|
|
||||||
|
return result.repos
|
||||||
|
}
|
||||||
|
|
||||||
|
return repoList.value
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 校验单个仓库地址是否可以加入或更新到列表。 */
|
||||||
|
function validateRepoUrl(url: string, editingRepoIndex: number | null = null) {
|
||||||
|
if (!url) return false
|
||||||
|
|
||||||
|
if (!isValidRepoUrl(url)) {
|
||||||
|
$toast.error(t('dialog.pluginMarketSetting.invalidUrl'))
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const duplicated = repoList.value.some((repo, index) => repo === url && index !== editingRepoIndex)
|
||||||
|
if (duplicated) {
|
||||||
|
$toast.error(t('dialog.pluginMarketSetting.duplicateUrl'))
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 添加一个仓库地址到列表。 */
|
||||||
function addRepo() {
|
function addRepo() {
|
||||||
const url = newRepoUrl.value.trim()
|
const url = newRepoUrl.value.trim()
|
||||||
if (!url) return
|
if (!validateRepoUrl(url)) return
|
||||||
|
|
||||||
if (!url.startsWith('http://') && !url.startsWith('https://')) {
|
|
||||||
$toast.error(t('dialog.pluginMarketSetting.invalidUrl'))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (repoList.value.includes(url)) {
|
|
||||||
$toast.error(t('dialog.pluginMarketSetting.duplicateUrl'))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
repoList.value.push(url)
|
repoList.value.push(url)
|
||||||
newRepoUrl.value = ''
|
newRepoUrl.value = ''
|
||||||
|
syncTextFromList()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 从列表中删除一个仓库地址。 */
|
||||||
function removeRepo(index: number) {
|
function removeRepo(index: number) {
|
||||||
repoList.value.splice(index, 1)
|
repoList.value.splice(index, 1)
|
||||||
|
syncTextFromList()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 进入指定仓库地址的行内编辑状态。 */
|
||||||
function startEdit(index: number) {
|
function startEdit(index: number) {
|
||||||
editingIndex.value = index
|
editingIndex.value = index
|
||||||
editingUrl.value = repoList.value[index]
|
editingUrl.value = repoList.value[index]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 保存当前行内编辑的仓库地址。 */
|
||||||
function saveEdit() {
|
function saveEdit() {
|
||||||
if (editingIndex.value === null) return
|
if (editingIndex.value === null) return
|
||||||
|
|
||||||
const url = editingUrl.value.trim()
|
const url = editingUrl.value.trim()
|
||||||
if (!url) return
|
if (!validateRepoUrl(url, editingIndex.value)) return
|
||||||
|
|
||||||
if (!url.startsWith('http://') && !url.startsWith('https://')) {
|
|
||||||
$toast.error(t('dialog.pluginMarketSetting.invalidUrl'))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
repoList.value[editingIndex.value] = url
|
repoList.value[editingIndex.value] = url
|
||||||
|
syncTextFromList()
|
||||||
editingIndex.value = null
|
editingIndex.value = null
|
||||||
editingUrl.value = ''
|
editingUrl.value = ''
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 取消当前行内编辑状态。 */
|
||||||
function cancelEdit() {
|
function cancelEdit() {
|
||||||
editingIndex.value = null
|
editingIndex.value = null
|
||||||
editingUrl.value = ''
|
editingUrl.value = ''
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 将仓库地址格式化为更易扫描的显示名称。 */
|
||||||
function formatRepoDisplay(url: string) {
|
function formatRepoDisplay(url: string) {
|
||||||
try {
|
try {
|
||||||
const parsedUrl = new URL(url)
|
const parsedUrl = new URL(url)
|
||||||
@@ -108,6 +236,16 @@ function formatRepoDisplay(url: string) {
|
|||||||
return url
|
return url
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 获取仓库地址的主机名用于辅助显示。 */
|
||||||
|
function formatRepoHost(url: string) {
|
||||||
|
try {
|
||||||
|
return new URL(url).hostname
|
||||||
|
} catch {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 返回拖拽列表项的稳定键。 */
|
||||||
function repoItemKey(repo: string) {
|
function repoItemKey(repo: string) {
|
||||||
return repo
|
return repo
|
||||||
}
|
}
|
||||||
@@ -118,108 +256,239 @@ onMounted(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<VDialog width="50rem" scrollable :fullscreen="!display.mdAndUp.value">
|
<VDialog width="56rem" scrollable :fullscreen="!display.mdAndUp.value">
|
||||||
<VCard class="plugin-market-dialog-card">
|
<VCard class="plugin-market-dialog-card">
|
||||||
<VCardItem>
|
<VCardItem>
|
||||||
<VCardTitle>
|
<div class="plugin-market-header">
|
||||||
<VIcon icon="mdi-store-cog" class="me-2" />
|
<div class="plugin-market-title-block">
|
||||||
{{ t('dialog.pluginMarketSetting.title') }}
|
<VCardTitle class="d-flex align-center pa-0">
|
||||||
</VCardTitle>
|
<VIcon icon="mdi-store-cog" class="me-2" />
|
||||||
|
{{ t('dialog.pluginMarketSetting.title') }}
|
||||||
|
</VCardTitle>
|
||||||
|
<div class="plugin-market-subtitle">
|
||||||
|
{{ t('dialog.pluginMarketSetting.description') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<VChip color="primary" variant="tonal" prepend-icon="mdi-source-repository">
|
||||||
|
{{ t('dialog.pluginMarketSetting.repoCount', { count: activeRepoCount }) }}
|
||||||
|
</VChip>
|
||||||
|
</div>
|
||||||
<VDialogCloseBtn @click="emit('close')" />
|
<VDialogCloseBtn @click="emit('close')" />
|
||||||
</VCardItem>
|
</VCardItem>
|
||||||
<VDivider />
|
<VDivider />
|
||||||
|
|
||||||
<VCardText class="plugin-market-dialog-body pt-4">
|
<VCardText class="plugin-market-dialog-body pt-4">
|
||||||
<div class="plugin-market-input mb-4">
|
<div class="plugin-market-toolbar">
|
||||||
<VTextField
|
<VBtnToggle
|
||||||
v-model="newRepoUrl"
|
:model-value="editorMode"
|
||||||
density="compact"
|
mandatory
|
||||||
:placeholder="t('dialog.pluginMarketSetting.urlPlaceholder')"
|
color="primary"
|
||||||
prepend-inner-icon="mdi-link-plus"
|
density="comfortable"
|
||||||
clearable
|
variant="tonal"
|
||||||
@keyup.enter="addRepo"
|
class="plugin-market-mode-toggle"
|
||||||
|
@update:model-value="switchEditorMode"
|
||||||
>
|
>
|
||||||
<template #append>
|
<VBtn value="list" prepend-icon="mdi-format-list-bulleted">
|
||||||
<VBtn icon="mdi-plus" variant="text" color="primary" @click="addRepo" />
|
{{ t('dialog.pluginMarketSetting.listMode') }}
|
||||||
</template>
|
</VBtn>
|
||||||
</VTextField>
|
<VBtn value="text" prepend-icon="mdi-text-box-edit-outline">
|
||||||
</div>
|
{{ t('dialog.pluginMarketSetting.textMode') }}
|
||||||
|
</VBtn>
|
||||||
|
</VBtnToggle>
|
||||||
|
|
||||||
<div class="plugin-market-list-wrap">
|
<div class="plugin-market-toolbar-info">
|
||||||
<VList v-if="repoList.length > 0" class="px-0">
|
{{ editorMode === 'list' ? t('dialog.pluginMarketSetting.listHint') : t('dialog.pluginMarketSetting.textHint') }}
|
||||||
<draggable
|
|
||||||
v-model="repoList"
|
|
||||||
:item-key="repoItemKey"
|
|
||||||
handle=".drag-handle"
|
|
||||||
animation="200"
|
|
||||||
:disabled="editingIndex !== null"
|
|
||||||
>
|
|
||||||
<template #item="{ element: repo, index }">
|
|
||||||
<div>
|
|
||||||
<VListItem class="py-2">
|
|
||||||
<template #prepend>
|
|
||||||
<VBtn
|
|
||||||
icon="mdi-drag-vertical"
|
|
||||||
size="small"
|
|
||||||
variant="text"
|
|
||||||
color="primary"
|
|
||||||
class="drag-handle me-2"
|
|
||||||
:disabled="editingIndex !== null"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<VListItemTitle v-if="editingIndex !== index">
|
|
||||||
<span class="text-truncate" :title="repo">{{ formatRepoDisplay(repo) }}</span>
|
|
||||||
</VListItemTitle>
|
|
||||||
|
|
||||||
<VTextField
|
|
||||||
v-else
|
|
||||||
v-model="editingUrl"
|
|
||||||
density="compact"
|
|
||||||
variant="outlined"
|
|
||||||
hide-details
|
|
||||||
@keyup.enter="saveEdit"
|
|
||||||
@keyup.escape="cancelEdit"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<template #append v-if="editingIndex !== index">
|
|
||||||
<div class="d-flex align-center">
|
|
||||||
<IconBtn icon="mdi-pencil" size="small" variant="text" @click="startEdit(index)" />
|
|
||||||
<IconBtn
|
|
||||||
icon="mdi-delete"
|
|
||||||
size="small"
|
|
||||||
variant="text"
|
|
||||||
color="error"
|
|
||||||
@click="removeRepo(index)"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template #append v-else>
|
|
||||||
<div class="d-flex align-center">
|
|
||||||
<IconBtn icon="mdi-check" size="small" variant="text" color="success" @click="saveEdit" />
|
|
||||||
<IconBtn icon="mdi-close" size="small" variant="text" @click="cancelEdit" />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</VListItem>
|
|
||||||
<VDivider v-if="index < repoList.length - 1" class="mx-4" />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</draggable>
|
|
||||||
</VList>
|
|
||||||
|
|
||||||
<div v-else class="text-center text-medium-emphasis py-8">
|
|
||||||
<VIcon icon="mdi-folder-open-outline" size="48" class="mb-2" />
|
|
||||||
<div>{{ t('dialog.pluginMarketSetting.noRepos') }}</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div v-if="editorMode === 'list'" class="plugin-market-list-panel">
|
||||||
|
<div class="plugin-market-input">
|
||||||
|
<VTextField
|
||||||
|
v-model="newRepoUrl"
|
||||||
|
density="compact"
|
||||||
|
:placeholder="t('dialog.pluginMarketSetting.urlPlaceholder')"
|
||||||
|
prepend-inner-icon="mdi-link-plus"
|
||||||
|
clearable
|
||||||
|
hide-details
|
||||||
|
@keyup.enter="addRepo"
|
||||||
|
>
|
||||||
|
<template #append>
|
||||||
|
<VBtn
|
||||||
|
icon="mdi-plus"
|
||||||
|
variant="tonal"
|
||||||
|
color="primary"
|
||||||
|
:aria-label="t('dialog.pluginMarketSetting.addRepo')"
|
||||||
|
@click="addRepo"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</VTextField>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="plugin-market-list-wrap">
|
||||||
|
<VList v-if="repoList.length > 0" class="plugin-market-repo-list px-0">
|
||||||
|
<draggable
|
||||||
|
v-model="repoList"
|
||||||
|
:item-key="repoItemKey"
|
||||||
|
handle=".drag-handle"
|
||||||
|
animation="200"
|
||||||
|
:disabled="editingIndex !== null"
|
||||||
|
@end="syncTextFromList"
|
||||||
|
>
|
||||||
|
<template #item="{ element: repo, index }">
|
||||||
|
<div>
|
||||||
|
<VListItem class="plugin-market-repo-item py-3">
|
||||||
|
<template #prepend>
|
||||||
|
<VBtn
|
||||||
|
icon="mdi-drag-vertical"
|
||||||
|
size="small"
|
||||||
|
variant="text"
|
||||||
|
color="primary"
|
||||||
|
class="drag-handle me-2"
|
||||||
|
:disabled="editingIndex !== null"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-if="editingIndex !== index">
|
||||||
|
<VListItemTitle>
|
||||||
|
<div class="plugin-market-repo-title">
|
||||||
|
<span class="plugin-market-repo-index">{{ index + 1 }}</span>
|
||||||
|
<span class="text-truncate" :title="repo">{{ formatRepoDisplay(repo) }}</span>
|
||||||
|
<VChip
|
||||||
|
v-if="formatRepoHost(repo)"
|
||||||
|
size="x-small"
|
||||||
|
variant="tonal"
|
||||||
|
color="secondary"
|
||||||
|
class="plugin-market-host-chip"
|
||||||
|
>
|
||||||
|
{{ formatRepoHost(repo) }}
|
||||||
|
</VChip>
|
||||||
|
</div>
|
||||||
|
</VListItemTitle>
|
||||||
|
<VListItemSubtitle class="text-truncate mt-1" :title="repo">
|
||||||
|
{{ repo }}
|
||||||
|
</VListItemSubtitle>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<VTextField
|
||||||
|
v-else
|
||||||
|
v-model="editingUrl"
|
||||||
|
density="compact"
|
||||||
|
variant="outlined"
|
||||||
|
hide-details
|
||||||
|
autofocus
|
||||||
|
@keyup.enter="saveEdit"
|
||||||
|
@keyup.escape="cancelEdit"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<template #append v-if="editingIndex !== index">
|
||||||
|
<div class="d-flex align-center">
|
||||||
|
<IconBtn icon="mdi-pencil" size="small" variant="text" @click="startEdit(index)" />
|
||||||
|
<IconBtn
|
||||||
|
icon="mdi-delete"
|
||||||
|
size="small"
|
||||||
|
variant="text"
|
||||||
|
color="error"
|
||||||
|
@click="removeRepo(index)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #append v-else>
|
||||||
|
<div class="d-flex align-center">
|
||||||
|
<IconBtn icon="mdi-check" size="small" variant="text" color="success" @click="saveEdit" />
|
||||||
|
<IconBtn icon="mdi-close" size="small" variant="text" @click="cancelEdit" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</VListItem>
|
||||||
|
<VDivider v-if="index < repoList.length - 1" class="mx-4" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</draggable>
|
||||||
|
</VList>
|
||||||
|
|
||||||
|
<div v-else class="plugin-market-empty text-center text-medium-emphasis">
|
||||||
|
<VIcon icon="mdi-source-repository-multiple" size="48" class="mb-2" />
|
||||||
|
<div>{{ t('dialog.pluginMarketSetting.noRepos') }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="plugin-market-text-panel">
|
||||||
|
<VTextarea
|
||||||
|
v-model="repoText"
|
||||||
|
class="plugin-market-textarea"
|
||||||
|
rows="11"
|
||||||
|
auto-grow
|
||||||
|
max-rows="18"
|
||||||
|
variant="outlined"
|
||||||
|
prepend-inner-icon="mdi-text-box-edit-outline"
|
||||||
|
:placeholder="t('dialog.pluginMarketSetting.textPlaceholder')"
|
||||||
|
:hint="t('dialog.pluginMarketSetting.textHint')"
|
||||||
|
persistent-hint
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="plugin-market-text-summary">
|
||||||
|
<VChip color="primary" variant="tonal" prepend-icon="mdi-check-circle-outline">
|
||||||
|
{{ t('dialog.pluginMarketSetting.repoCount', { count: parsedTextRepos.repos.length }) }}
|
||||||
|
</VChip>
|
||||||
|
<VChip
|
||||||
|
v-if="parsedTextRepos.duplicateRepos.length > 0"
|
||||||
|
color="warning"
|
||||||
|
variant="tonal"
|
||||||
|
prepend-icon="mdi-content-duplicate"
|
||||||
|
>
|
||||||
|
{{ t('dialog.pluginMarketSetting.duplicateCount', { count: parsedTextRepos.duplicateRepos.length }) }}
|
||||||
|
</VChip>
|
||||||
|
<VChip
|
||||||
|
v-if="parsedTextRepos.invalidRepos.length > 0"
|
||||||
|
color="error"
|
||||||
|
variant="tonal"
|
||||||
|
prepend-icon="mdi-alert-circle-outline"
|
||||||
|
>
|
||||||
|
{{ t('dialog.pluginMarketSetting.invalidCount', { count: parsedTextRepos.invalidRepos.length }) }}
|
||||||
|
</VChip>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<VAlert
|
||||||
|
v-if="parsedTextRepos.invalidRepos.length > 0"
|
||||||
|
type="error"
|
||||||
|
variant="tonal"
|
||||||
|
density="compact"
|
||||||
|
class="plugin-market-invalid-alert"
|
||||||
|
>
|
||||||
|
<div>{{ t('dialog.pluginMarketSetting.invalidText', { count: parsedTextRepos.invalidRepos.length }) }}</div>
|
||||||
|
<div class="text-truncate">
|
||||||
|
{{ parsedTextRepos.invalidRepos.slice(0, 3).join(', ') }}
|
||||||
|
</div>
|
||||||
|
</VAlert>
|
||||||
|
|
||||||
|
<VAlert
|
||||||
|
v-else-if="parsedTextRepos.duplicateRepos.length > 0"
|
||||||
|
type="warning"
|
||||||
|
variant="tonal"
|
||||||
|
density="compact"
|
||||||
|
>
|
||||||
|
{{ t('dialog.pluginMarketSetting.duplicateTextIgnored') }}
|
||||||
|
</VAlert>
|
||||||
|
</div>
|
||||||
</VCardText>
|
</VCardText>
|
||||||
<VCardActions>
|
|
||||||
|
<VDivider />
|
||||||
|
<VCardActions class="plugin-market-actions">
|
||||||
|
<div class="plugin-market-footer-summary">
|
||||||
|
{{ t('dialog.pluginMarketSetting.repoCount', { count: activeRepoCount }) }}
|
||||||
|
</div>
|
||||||
<VSpacer />
|
<VSpacer />
|
||||||
|
<VBtn variant="text" @click="emit('close')">
|
||||||
|
{{ t('dialog.pluginMarketSetting.close') }}
|
||||||
|
</VBtn>
|
||||||
<VBtn
|
<VBtn
|
||||||
|
color="primary"
|
||||||
|
variant="flat"
|
||||||
@click="saveHandle"
|
@click="saveHandle"
|
||||||
prepend-icon="mdi-content-save-check"
|
prepend-icon="mdi-content-save-check"
|
||||||
class="px-5 me-3"
|
class="px-5"
|
||||||
:disabled="repoList.length === 0"
|
:disabled="saveDisabled"
|
||||||
>
|
>
|
||||||
{{ t('dialog.pluginMarketSetting.save') }}
|
{{ t('dialog.pluginMarketSetting.save') }}
|
||||||
</VBtn>
|
</VBtn>
|
||||||
@@ -232,6 +501,25 @@ onMounted(() => {
|
|||||||
.plugin-market-dialog-card {
|
.plugin-market-dialog-card {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
max-block-size: min(82vh, 50rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.plugin-market-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 1rem;
|
||||||
|
padding-inline-end: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plugin-market-title-block {
|
||||||
|
min-inline-size: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plugin-market-subtitle {
|
||||||
|
color: rgba(var(--v-theme-on-surface), 0.68);
|
||||||
|
font-size: 0.875rem;
|
||||||
|
margin-block-start: 0.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.plugin-market-dialog-body {
|
.plugin-market-dialog-body {
|
||||||
@@ -239,6 +527,41 @@ onMounted(() => {
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
min-block-size: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plugin-market-toolbar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 1rem;
|
||||||
|
border: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
|
||||||
|
border-radius: 8px;
|
||||||
|
background: rgba(var(--v-theme-surface-variant), 0.18);
|
||||||
|
padding: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plugin-market-mode-toggle {
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
:deep(.v-btn) {
|
||||||
|
min-inline-size: 7rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.plugin-market-toolbar-info {
|
||||||
|
color: rgba(var(--v-theme-on-surface), 0.68);
|
||||||
|
font-size: 0.875rem;
|
||||||
|
text-align: end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plugin-market-list-panel,
|
||||||
|
.plugin-market-text-panel {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
min-block-size: 0;
|
min-block-size: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -248,7 +571,103 @@ onMounted(() => {
|
|||||||
|
|
||||||
.plugin-market-list-wrap {
|
.plugin-market-list-wrap {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-block-size: 0;
|
border: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
|
||||||
|
border-radius: 8px;
|
||||||
|
background: rgba(var(--v-theme-surface), 0.72);
|
||||||
|
min-block-size: 14rem;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.plugin-market-repo-list {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plugin-market-repo-item {
|
||||||
|
min-block-size: 4.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plugin-market-repo-title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
min-inline-size: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plugin-market-repo-index {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
color: rgba(var(--v-theme-on-surface), 0.48);
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
inline-size: 1.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plugin-market-host-chip {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plugin-market-empty {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-direction: column;
|
||||||
|
min-block-size: 14rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plugin-market-textarea {
|
||||||
|
:deep(textarea) {
|
||||||
|
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.plugin-market-text-summary {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plugin-market-invalid-alert {
|
||||||
|
:deep(.v-alert__content) {
|
||||||
|
min-inline-size: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.plugin-market-actions {
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plugin-market-footer-summary {
|
||||||
|
color: rgba(var(--v-theme-on-surface), 0.62);
|
||||||
|
font-size: 0.875rem;
|
||||||
|
margin-inline-start: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
.plugin-market-dialog-card {
|
||||||
|
max-block-size: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plugin-market-header,
|
||||||
|
.plugin-market-toolbar {
|
||||||
|
align-items: stretch;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plugin-market-toolbar-info {
|
||||||
|
text-align: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plugin-market-mode-toggle {
|
||||||
|
inline-size: 100%;
|
||||||
|
|
||||||
|
:deep(.v-btn) {
|
||||||
|
flex: 1;
|
||||||
|
min-inline-size: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.plugin-market-footer-summary {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
+14
-1
@@ -2485,13 +2485,26 @@ export default {
|
|||||||
},
|
},
|
||||||
pluginMarketSetting: {
|
pluginMarketSetting: {
|
||||||
title: 'Plugin Market Settings',
|
title: 'Plugin Market Settings',
|
||||||
|
description: 'Maintain plugin market repositories as an ordered list or by pasting bulk text.',
|
||||||
repoUrl: 'Plugin Repository URL',
|
repoUrl: 'Plugin Repository URL',
|
||||||
repoPlaceholder: 'Format: https://github.com/jxxghp/MoviePilot-Plugins/,https://github.com/xxxx/xxxxxx/',
|
repoPlaceholder: 'Format: https://github.com/jxxghp/MoviePilot-Plugins/,https://github.com/xxxx/xxxxxx/',
|
||||||
repoHint: 'Multiple URLs separated by lines, only Github repositories are supported',
|
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/',
|
||||||
|
listMode: 'List',
|
||||||
|
textMode: 'Text',
|
||||||
|
listHint: 'Add, edit, delete, or drag repositories to adjust their order.',
|
||||||
|
textHint: 'Paste repository URLs one per line or separated by commas.',
|
||||||
|
addRepo: 'Add repository',
|
||||||
noRepos: 'No plugin repository URLs',
|
noRepos: 'No plugin repository URLs',
|
||||||
|
repoCount: '{count} repositories',
|
||||||
|
duplicateCount: '{count} duplicates',
|
||||||
|
invalidCount: '{count} invalid',
|
||||||
invalidUrl: 'Please enter a valid URL',
|
invalidUrl: 'Please enter a valid URL',
|
||||||
duplicateUrl: 'This URL already exists',
|
duplicateUrl: 'This URL already exists',
|
||||||
|
invalidText: 'There are {count} invalid URLs in the text. Fix them before saving.',
|
||||||
|
invalidTextIgnored: '{count} invalid URLs ignored',
|
||||||
|
duplicateTextIgnored: 'Duplicate URLs will be removed automatically when saving.',
|
||||||
close: 'Close',
|
close: 'Close',
|
||||||
save: 'Save',
|
save: 'Save',
|
||||||
saveSuccess: 'Plugin repository saved successfully',
|
saveSuccess: 'Plugin repository saved successfully',
|
||||||
|
|||||||
+14
-1
@@ -2440,13 +2440,26 @@ export default {
|
|||||||
},
|
},
|
||||||
pluginMarketSetting: {
|
pluginMarketSetting: {
|
||||||
title: '插件市场设置',
|
title: '插件市场设置',
|
||||||
|
description: '维护插件市场仓库来源,可按列表逐条调整,也可直接粘贴批量文本。',
|
||||||
repoUrl: '插件仓库地址',
|
repoUrl: '插件仓库地址',
|
||||||
repoPlaceholder: '格式:https://github.com/jxxghp/MoviePilot-Plugins/,https://github.com/xxxx/xxxxxx/',
|
repoPlaceholder: '格式:https://github.com/jxxghp/MoviePilot-Plugins/,https://github.com/xxxx/xxxxxx/',
|
||||||
repoHint: '多个地址使用换行分隔,仅支持Github仓库',
|
repoHint: '多个地址可使用换行或英文逗号分隔',
|
||||||
urlPlaceholder: '输入插件仓库地址',
|
urlPlaceholder: '输入插件仓库地址',
|
||||||
|
textPlaceholder: 'https://github.com/jxxghp/MoviePilot-Plugins/\nhttps://github.com/xxxx/xxxxxx/',
|
||||||
|
listMode: '列表维护',
|
||||||
|
textMode: '文本维护',
|
||||||
|
listHint: '逐条添加、编辑、删除或拖拽调整仓库顺序。',
|
||||||
|
textHint: '直接粘贴仓库地址串,一行一个或使用英文逗号分隔。',
|
||||||
|
addRepo: '添加仓库',
|
||||||
noRepos: '暂无插件仓库地址',
|
noRepos: '暂无插件仓库地址',
|
||||||
|
repoCount: '{count} 个仓库',
|
||||||
|
duplicateCount: '{count} 个重复',
|
||||||
|
invalidCount: '{count} 个无效',
|
||||||
invalidUrl: '请输入有效的URL地址',
|
invalidUrl: '请输入有效的URL地址',
|
||||||
duplicateUrl: '该地址已存在',
|
duplicateUrl: '该地址已存在',
|
||||||
|
invalidText: '文本中有 {count} 个无效地址,请修正后保存。',
|
||||||
|
invalidTextIgnored: '已忽略 {count} 个无效地址',
|
||||||
|
duplicateTextIgnored: '重复地址会在保存时自动去重。',
|
||||||
close: '关闭',
|
close: '关闭',
|
||||||
save: '保存',
|
save: '保存',
|
||||||
saveSuccess: '插件仓库保存成功',
|
saveSuccess: '插件仓库保存成功',
|
||||||
|
|||||||
+14
-1
@@ -2441,13 +2441,26 @@ export default {
|
|||||||
},
|
},
|
||||||
pluginMarketSetting: {
|
pluginMarketSetting: {
|
||||||
title: '插件市場設置',
|
title: '插件市場設置',
|
||||||
|
description: '維護插件市場倉庫來源,可按列表逐條調整,也可直接貼上批量文字。',
|
||||||
repoUrl: '插件倉庫地址',
|
repoUrl: '插件倉庫地址',
|
||||||
repoPlaceholder: '格式:https://github.com/jxxghp/MoviePilot-Plugins/,https://github.com/xxxx/xxxxxx/',
|
repoPlaceholder: '格式:https://github.com/jxxghp/MoviePilot-Plugins/,https://github.com/xxxx/xxxxxx/',
|
||||||
repoHint: '多個地址使用换行分隔,僅支援Github倉庫',
|
repoHint: '多個地址可使用換行或英文逗號分隔',
|
||||||
urlPlaceholder: '輸入插件倉庫地址',
|
urlPlaceholder: '輸入插件倉庫地址',
|
||||||
|
textPlaceholder: 'https://github.com/jxxghp/MoviePilot-Plugins/\nhttps://github.com/xxxx/xxxxxx/',
|
||||||
|
listMode: '列表維護',
|
||||||
|
textMode: '文字維護',
|
||||||
|
listHint: '逐條新增、編輯、刪除或拖曳調整倉庫順序。',
|
||||||
|
textHint: '直接貼上倉庫地址串,一行一個或使用英文逗號分隔。',
|
||||||
|
addRepo: '新增倉庫',
|
||||||
noRepos: '暫無插件倉庫地址',
|
noRepos: '暫無插件倉庫地址',
|
||||||
|
repoCount: '{count} 個倉庫',
|
||||||
|
duplicateCount: '{count} 個重複',
|
||||||
|
invalidCount: '{count} 個無效',
|
||||||
invalidUrl: '請輸入有效的URL地址',
|
invalidUrl: '請輸入有效的URL地址',
|
||||||
duplicateUrl: '該地址已存在',
|
duplicateUrl: '該地址已存在',
|
||||||
|
invalidText: '文字中有 {count} 個無效地址,請修正後儲存。',
|
||||||
|
invalidTextIgnored: '已忽略 {count} 個無效地址',
|
||||||
|
duplicateTextIgnored: '重複地址會在儲存時自動去重。',
|
||||||
close: '關閉',
|
close: '關閉',
|
||||||
save: '儲存',
|
save: '儲存',
|
||||||
saveSuccess: '插件倉庫儲存成功',
|
saveSuccess: '插件倉庫儲存成功',
|
||||||
|
|||||||
Reference in New Issue
Block a user