Feature(custom): allow user to add up to 6 favorite picbes in upload page, and quick select

This commit is contained in:
Kuingsmile
2026-01-11 21:59:16 +08:00
parent 8e6f4f9d64
commit 49b5f7a724
6 changed files with 359 additions and 4 deletions

View File

@@ -958,6 +958,7 @@
"updateReady": "Update Ready to Install"
},
"upload": {
"addToFavorites": "Add current picbed to quick switch",
"changePicBed": "Change PicBed",
"clickToUpload": "Click to Upload",
"clipboardPicture": "Clipboard",
@@ -970,9 +971,13 @@
"inputValidUrl": "Please enter a valid URL",
"invalidUrlsFound": "Invalid URLs found: {urls}",
"linkFormat": "Link Format",
"longPressToRemoveFromFavorites": "Long press to remove",
"multipleUrlsHint": "Each URL on a separate line for multiple uploads",
"outputFormat": "Output Format",
"picbedAddedToFavorites": "Added to quick switch",
"picbedSwitched": "Switched to {name}",
"quickUpload": "Quick Upload",
"removeFromFavorites": "Remove from quick switch",
"taskQueue": {
"addFiles": "Add Files",
"allCancelled": "All tasks cancelled",

View File

@@ -953,6 +953,7 @@
"updateReady": "更新已准备好"
},
"upload": {
"addToFavorites": "添加当前图床到快速切换",
"changePicBed": "切换图床",
"clickToUpload": "点击上传",
"clipboardPicture": "剪贴板图片",
@@ -965,9 +966,13 @@
"inputValidUrl": "请输入合法的URL",
"invalidUrlsFound": "发现无效的URL: {urls}",
"linkFormat": "链接格式",
"longPressToRemoveFromFavorites": "长按移除",
"multipleUrlsHint": "多个URL请分行输入",
"outputFormat": "输出格式",
"picbedAddedToFavorites": "已添加到快速切换",
"picbedSwitched": "已切换到 {name}",
"quickUpload": "快捷上传",
"removeFromFavorites": "从快速切换中移除",
"taskQueue": {
"addFiles": "添加文件",
"allCancelled": "所有任务已取消",

View File

@@ -953,6 +953,7 @@
"updateReady": "更新已準備好"
},
"upload": {
"addToFavorites": "添加當前圖床到快速切換",
"changePicBed": "切換圖床",
"clickToUpload": "點擊上傳",
"clipboardPicture": "剪貼板圖片",
@@ -965,9 +966,13 @@
"inputValidUrl": "請輸入合法的URL",
"invalidUrlsFound": "發現無效的URL: {urls}",
"linkFormat": "連結格式",
"longPressToRemoveFromFavorites": "長按移除",
"multipleUrlsHint": "多個URL請分行輸入",
"outputFormat": "輸出格式",
"picbedAddedToFavorites": "已添加到快速切換",
"picbedSwitched": "已切換到 {name}",
"quickUpload": "快捷上傳",
"removeFromFavorites": "從快速切換中移除",
"taskQueue": {
"addFiles": "添加文件",
"allCancelled": "所有任務已取消",

View File

@@ -15,6 +15,45 @@
</div>
<EditIcon :size="16" class="provider-arrow" />
</button>
<div
class="add-favorite-button"
:title="t('pages.upload.addToFavorites')"
:class="{ disabled: favoritePicbeds.length >= MAX_FAVORITE_PICBEDS || isCurrentPicBedInFavorites }"
@click="addCurrentPicbedToFavorites"
>
<PlusIcon :size="12" />
</div>
<transition-group
name="badges-slide"
tag="div"
class="favorite-picbeds-container"
:class="{ 'has-many': favoritePicbeds.length >= 4 }"
>
<button
v-for="picbedType in favoritePicbeds"
:key="picbedType.id"
class="picbed-badge"
:class="{ 'is-active': isCurrentPicbed(picbedType), 'show-delete': longPressedBadge === picbedType.id }"
:title="t('pages.upload.longPressToRemoveFromFavorites') + getPicbedName(picbedType)"
@click="handleBadgeClick(picbedType)"
@mousedown="handleBadgeMouseDown(picbedType)"
@mouseup="handleBadgeMouseUp"
@mouseleave="handleBadgeMouseUp"
@touchstart="handleBadgeTouchStart(picbedType, $event)"
@touchend="handleBadgeTouchEnd"
@touchcancel="handleBadgeTouchEnd"
>
<span class="badge-name">{{ getAbbreviatedName(picbedType) }}</span>
<button
v-if="longPressedBadge === picbedType.id"
class="badge-remove"
:title="t('pages.upload.removeFromFavorites')"
@click.stop="removePicbedFromFavorites(picbedType)"
>
<XIcon :size="12" />
</button>
</button>
</transition-group>
</div>
<div class="header-actions">
<div class="segmented-button-group">
@@ -623,6 +662,16 @@ const PicBedId = ref('')
const fileInput = useTemplateRef('fileInput')
const uploadInterval = ref(1000)
const favoritePicbeds = useStorage<IFavoritePicbedItem[]>('favorite-picbeds', [])
const MAX_FAVORITE_PICBEDS = 6
const longPressedBadge = ref<string | null>(null)
let longPressTimer: NodeJS.Timeout | null = null
const LONG_PRESS_DURATION = 500
const isCurrentPicBedInFavorites = computed(() => {
const result = favoritePicbeds.value.some(item => item.id === defaultIdG.value)
return result
})
// New task queue settings
const showTaskSettings = useStorage('upload-task-queue-show-settings', true)
const taskSearchQuery = ref('')
@@ -833,6 +882,135 @@ function updateUrlType(shortUrl: boolean) {
})
}
async function valideFavoritePicbeds() {
if (!favoritePicbeds.value.length) return
const allUploaders = (await getConfig<IStringKeyMap>(configPaths.uploader)) || {}
const availableFavorites = favoritePicbeds.value.filter(item => {
return (
Object.keys(allUploaders).includes(item.type) &&
allUploaders[item.type]?.configList.some((cfg: any) => cfg._id === item.id && cfg._configName === item.configName)
)
})
if (JSON.stringify(availableFavorites) !== JSON.stringify(favoritePicbeds.value)) {
favoritePicbeds.value = availableFavorites
}
}
watch(favoritePicbeds, valideFavoritePicbeds, { immediate: true })
function addCurrentPicbedToFavorites() {
favoritePicbeds.value.push({
id: defaultIdG.value,
type: defaultPicBedG.value,
configName: defaultConfigNameG.value,
})
message.success(t('pages.upload.picbedAddedToFavorites'))
}
function removePicbedFromFavorites(picbedType: IFavoritePicbedItem) {
const index = favoritePicbeds.value.findIndex(
item => item.type === picbedType.type && item.id === picbedType.id && item.configName === picbedType.configName,
)
if (index === -1) return
favoritePicbeds.value.splice(index, 1)
}
async function switchToPicbed(picbedType: IFavoritePicbedItem) {
if (!picbedType.id || !picbedType.type || !picbedType.configName) {
return
}
const uploaders = (await getConfig<IStringKeyMap>(`uploader.${picbedType.type}`)) || {}
const targetConfig = uploaders?.configList.find(
(cfg: any) => cfg._id === picbedType.id && cfg._configName === picbedType.configName,
)
if (!targetConfig) {
return
}
saveConfig({
[`uploader.${picbedType.type}.defaultId`]: picbedType.id,
[`picBed.${picbedType.type}`]: targetConfig,
[configPaths.picBed.current]: picbedType.type,
[configPaths.picBed.uploader]: picbedType.type,
})
await updatePicBeds()
const name = getPicbedName(picbedType).split('-')[0]
window.electron.sendRPC(IRPCActionType.TRAY_SET_TOOL_TIP, `${name} ${targetConfig._configName}`)
message.success(t('pages.upload.picbedSwitched', { name: getPicbedName(picbedType) }))
}
function getPicbedName(picbedType: IFavoritePicbedItem): string {
if (!picBedG.value || picBedG.value.length === 0) {
return picbedType.configName
}
const target = picBedG.value.find(item => item.type === picbedType.type)
return `${target ? target.name : picbedType.type}-${picbedType.configName}`
}
const truncatePart = (part: string): string => {
let partCount = 0
let res = ''
for (const char of part) {
const isDoubleByte = char.charCodeAt(0) > 127
const nextCount = partCount + (isDoubleByte ? 2 : 1)
if (nextCount > 4) {
return res
}
res += char
partCount = nextCount
}
return res
}
function getAbbreviatedName(picbedType: IFavoritePicbedItem): string {
const name = getPicbedName(picbedType)
return name.split('-').map(truncatePart).join('-')
}
function isCurrentPicbed(picbedType: IFavoritePicbedItem): boolean {
return defaultIdG.value === picbedType.id
}
function handleBadgeClick(picbedType: IFavoritePicbedItem) {
if (longPressedBadge.value === picbedType.id) {
return
}
switchToPicbed(picbedType)
}
function handleBadgeMouseDown(picbedType: IFavoritePicbedItem) {
longPressTimer = setTimeout(() => {
longPressedBadge.value = picbedType.id
}, LONG_PRESS_DURATION)
}
function handleBadgeMouseUp() {
if (longPressTimer) {
clearTimeout(longPressTimer)
longPressTimer = null
}
setTimeout(() => {
longPressedBadge.value = null
}, 10000)
}
function handleBadgeTouchStart(picbedType: IFavoritePicbedItem, event: TouchEvent) {
longPressTimer = setTimeout(() => {
longPressedBadge.value = picbedType.id
event.preventDefault()
}, LONG_PRESS_DURATION)
}
function handleBadgeTouchEnd() {
if (longPressTimer) {
clearTimeout(longPressTimer)
longPressTimer = null
}
setTimeout(() => {
longPressedBadge.value = null
}, 10000)
}
function uploadClipboardFiles() {
window.electron.sendRPC(IRPCActionType.UPLOAD_CLIPBOARD_FILES_FROM_UPLOAD_PAGE)
}
@@ -1072,14 +1250,12 @@ onBeforeUnmount(() => {
removeTaskQueueUpdateListenerCallback()
})
onBeforeMount(() => {
onBeforeMount(async () => {
removeUploadProgressListenerCallback = window.electron.ipcRendererOn('uploadProgress', uploadProgressHandler)
removeSyncPicBedListenerCallback = window.electron.ipcRendererOn('syncPicBed', syncPicBedHandler)
removeTaskQueueUpdateListenerCallback = window.electron.ipcRendererOn('uploadTaskQueueUpdate', taskQueueUpdateHandler)
$bus.on(SHOW_INPUT_BOX_RESPONSE, handleInputBoxValue)
getUseShortUrl()
getPasteStyle()
refreshTaskStatus()
await Promise.all([getUseShortUrl(), getPasteStyle(), refreshTaskStatus()])
})
</script>

View File

@@ -54,6 +54,11 @@ html, body {
.provider-section {
flex: 1;
display: flex;
align-items: center;
gap: 0.5rem;
flex-wrap: wrap;
max-width: calc(100% - 300px); /* Leave space for header-actions */
}
.provider-button {
@@ -79,6 +84,159 @@ html, body {
transform: translateY(-1px);
}
.add-favorite-button {
display: flex;
align-items: center;
justify-content: center;
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
width: 20px;
height: 20px;
font-family: inherit;
color: var(--color-text-secondary);
background: var(--color-surface-elevated);
transition: var(--transition-fast);
cursor: pointer;
flex-shrink: 0;
}
.add-favorite-button.disabled {
cursor: not-allowed;
opacity: 0.5;
pointer-events: none;
}
.add-favorite-button:hover:not(:disabled) {
border-color: var(--color-accent);
color: var(--color-accent);
background: var(--color-surface);
transform: translateY(-1px);
}
.add-favorite-button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.favorite-picbeds-container {
display: flex;
align-items: center;
gap: 0.2rem;
flex-wrap: wrap;
max-width: 400px;
}
.favorite-picbeds-container .picbed-badge {
width: 85px;
font-size: 0.8125rem;
}
.favorite-picbeds-container.has-many {
max-width: 300px;
}
.picbed-badge {
display: flex;
align-items: center;
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
padding: 0.375rem 0.5rem 0.375rem 0.75rem;
width: 85px;
font-family: inherit;
font-weight: 500;
color: var(--color-text-secondary);
background: var(--color-surface-elevated);
transition: all var(--transition-fast);
gap: 0.5rem;
cursor: pointer;
white-space: nowrap;
flex-shrink: 0;
user-select: none;
position: relative;
}
.picbed-badge:hover {
border-color: var(--color-accent);
color: var(--color-accent);
background: var(--color-surface);
transform: translateY(-1px);
}
.picbed-badge.is-active {
border-color: var(--color-accent);
color: var(--color-accent);
background: var(--color-accent-soft, rgb(59 130 246 / 10%));
font-weight: 600;
}
.picbed-badge.show-delete {
padding-right: 0.5rem;
}
.badge-name {
line-height: 1;
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.badge-remove {
display: flex;
align-items: center;
justify-content: center;
border: none;
border-radius: 50%;
padding: 0.125rem;
color: inherit;
background: transparent;
transition: var(--transition-fast);
cursor: pointer;
flex-shrink: 0;
animation: fade-in 0.2s ease-in;
}
.badge-remove:hover {
color: var(--color-error, #ef4444);
background: var(--color-error-soft, rgb(239 68 68 / 10%));
}
@keyframes fade-in {
from {
opacity: 0;
transform: scale(0.8);
}
to {
opacity: 1;
transform: scale(1);
}
}
/* Badges slide animation */
.badges-slide-enter-active {
transition: all 0.3s ease;
}
.badges-slide-leave-active {
transition: all 0.3s ease;
position: absolute;
}
.badges-slide-enter-from {
opacity: 0;
transform: translateX(-10px) scale(0.9);
}
.badges-slide-leave-to {
opacity: 0;
transform: translateX(10px) scale(0.9);
}
.badges-slide-move {
transition: transform 0.3s ease;
}
.provider-info {
display: flex;
flex-direction: column;

View File

@@ -521,3 +521,9 @@ interface IBuildInListItem {
// settings.rename
manualRename?: boolean
}
interface IFavoritePicbedItem {
id: string
type: string
configName: string
}