🐛 Fix(custom): fix drag upload of upload page

This commit is contained in:
Kuingsmile
2025-08-11 14:59:25 +08:00
parent 112c08d92c
commit 0bf435a0da
10 changed files with 4189 additions and 4200 deletions

View File

@@ -1,65 +1,66 @@
<template>
<div
id="app"
:key="pageReloadCount"
>
<router-view />
<UIServiceProvider />
</div>
</template>
<script lang="ts" setup>
import type { IConfig } from 'piclist'
import { onBeforeMount, onMounted } from 'vue'
import UIServiceProvider from '@/components/ui/UIServiceProvider.vue'
import { useATagClick } from '@/hooks/useATagClick'
import { useStore } from '@/hooks/useStore'
import { getConfig } from '@/utils/dataSender'
import { pageReloadCount } from '@/utils/global'
import { useAppStore } from './hooks/useAppStore'
useATagClick()
const store = useStore()
const appStore = useAppStore()
onBeforeMount(async () => {
const config = await getConfig<IConfig>()
if (config) {
store?.setDefaultPicBed(config?.picBed?.uploader || config?.picBed?.current || 'smms')
}
})
onMounted(async () => {
try {
appStore.init()
} catch (error) {
console.error('Failed to load settings:', error)
}
})
</script>
<script lang="ts">
export default {
name: 'PicGoApp'
}
</script>
<style lang="stylus">
body,
html
padding 0
margin 0
height 100%
font-family "Helvetica Neue",Helvetica,"PingFang SC","Hiragino Sans GB","Microsoft YaHei","微软雅黑",Arial,sans-serif
#app
height 100%
user-select none
overflow hidden
.el-button-group
width 100%
.el-button
width 50%
</style>
<template>
<div
id="app"
:key="pageReloadCount"
>
<router-view />
<UIServiceProvider />
</div>
</template>
<script lang="ts" setup>
import type { IConfig } from 'piclist'
import { onBeforeMount, onMounted } from 'vue'
import UIServiceProvider from '@/components/ui/UIServiceProvider.vue'
import { useATagClick } from '@/hooks/useATagClick'
import { useStore } from '@/hooks/useStore'
import { getConfig } from '@/utils/dataSender'
import { pageReloadCount } from '@/utils/global'
import { useAppStore } from './hooks/useAppStore'
useATagClick()
const store = useStore()
const appStore = useAppStore()
onBeforeMount(async () => {
const config = await getConfig<IConfig>()
if (config) {
store?.setDefaultPicBed(config?.picBed?.uploader || config?.picBed?.current || 'smms')
}
})
onMounted(async () => {
try {
appStore.init()
} catch (error) {
console.error('Failed to load settings:', error)
}
})
</script>
<script lang="ts">
export default {
name: 'PicGoApp'
}
</script>
<style lang="stylus">
body,
html
padding 0
margin 0
height 100%
font-family "Helvetica Neue",Helvetica,"PingFang SC","Hiragino Sans GB","Microsoft YaHei","微软雅黑",Arial,sans-serif
#app
height 100%
user-select none
overflow hidden
.el-button-group
width 100%
.el-button
width 50%
</style>

View File

@@ -1,23 +1,24 @@
import { onMounted, onUnmounted } from 'vue'
import { IRPCActionType } from '@/utils/enum'
export function useATagClick () {
const handleATagClick = (e: MouseEvent) => {
if (e.target instanceof HTMLAnchorElement) {
if (e.target.href) {
// avoid opening localhost development URLs in external browser
if (!e.target.href.startsWith('http://localhost:3000')) {
e.preventDefault()
window.electron.sendRPC(IRPCActionType.OPEN_URL, e.target.href)
}
}
}
}
onMounted(() => {
document.addEventListener('click', handleATagClick)
})
onUnmounted(() => {
document.removeEventListener('click', handleATagClick)
})
}
import { onMounted, onUnmounted } from 'vue'
import { IRPCActionType } from '@/utils/enum'
export function useATagClick () {
const handleATagClick = (e: MouseEvent) => {
if (e.target instanceof HTMLAnchorElement) {
if (e.target.href) {
if (!e.target.href.startsWith('http://localhost:3000')) {
e.preventDefault()
window.electron.sendRPC(IRPCActionType.OPEN_URL, e.target.href)
}
}
}
}
onMounted(() => {
document.addEventListener('click', handleATagClick)
})
onUnmounted(() => {
document.removeEventListener('click', handleATagClick)
})
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,59 +1,55 @@
import 'video.js/dist/video-js.css'
import 'highlight.js/styles/stackoverflow-light.css'
import 'highlight.js/lib/common'
import hljsVuePlugin from '@highlightjs/vue-plugin'
import VueVideoPlayer from '@videojs-player/vue'
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
import { createApp } from 'vue'
import { createI18n } from 'vue-i18n'
import VueLazyLoad from 'vue3-lazyload'
import App from '@/App.vue'
import en from '@/i18n/locales/en.json'
import zhCN from '@/i18n/locales/zh-CN.json'
import zhTW from '@/i18n/locales/zh-TW.json'
import router from '@/router'
import { store } from '@/store'
import db from '@/utils/db'
type MessageSchema = typeof zhCN
window.electron.setVisualZoomLevelLimits(1, 1)
const app = createApp(App)
app.config.globalProperties.$$db = db
app.config.globalProperties.triggerRPC = window.electron.triggerRPC
app.config.globalProperties.sendRPC = window.electron.sendRPC
app.config.globalProperties.sendToMain = window.electron.sendToMain
const i18n = createI18n<[MessageSchema], 'en' | 'zh-CN' | 'zh-TW'>({
legacy: false,
locale: localStorage.getItem('currentLanguage') || 'zh-CN',
fallbackLocale: 'zh-CN',
messages: {
en,
'zh-CN': zhCN,
'zh-TW': zhTW
}
})
const pinia = createPinia()
pinia.use(piniaPluginPersistedstate)
app.use(VueLazyLoad, {
loading: '/loading.jpg',
error: '/unknown-file-type.svg',
delay: 500
})
app.use(i18n)
app.use(router)
app.use(store)
app.use(pinia)
app.use(hljsVuePlugin)
app.use(VueVideoPlayer)
app.mount('#app')
export {
i18n
}
import 'video.js/dist/video-js.css'
import 'highlight.js/styles/stackoverflow-light.css'
import 'highlight.js/lib/common'
import hljsVuePlugin from '@highlightjs/vue-plugin'
import VueVideoPlayer from '@videojs-player/vue'
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
import { createApp } from 'vue'
import { createI18n } from 'vue-i18n'
import VueLazyLoad from 'vue3-lazyload'
import App from '@/App.vue'
import en from '@/i18n/locales/en.json'
import zhCN from '@/i18n/locales/zh-CN.json'
import zhTW from '@/i18n/locales/zh-TW.json'
import router from '@/router'
import { store } from '@/store'
import db from '@/utils/db'
type MessageSchema = typeof zhCN
window.electron.setVisualZoomLevelLimits(1, 1)
const app = createApp(App)
app.config.globalProperties.$$db = db
app.config.globalProperties.triggerRPC = window.electron.triggerRPC
app.config.globalProperties.sendRPC = window.electron.sendRPC
app.config.globalProperties.sendToMain = window.electron.sendToMain
const i18n = createI18n<[MessageSchema], 'en' | 'zh-CN' | 'zh-TW'>({
legacy: false,
locale: localStorage.getItem('currentLanguage') || 'zh-CN',
fallbackLocale: 'zh-CN',
messages: {
en,
'zh-CN': zhCN,
'zh-TW': zhTW
}
})
const pinia = createPinia()
pinia.use(piniaPluginPersistedstate)
app.use(VueLazyLoad, {
loading: '/loading.jpg',
error: '/unknown-file-type.svg',
delay: 500
})
app.use(i18n)
app.use(router)
app.use(store)
app.use(pinia)
app.use(hljsVuePlugin)
app.use(VueVideoPlayer)
app.mount('#app')

View File

@@ -1,444 +1,445 @@
<template>
<div class="upload-container">
<!-- Header Card -->
<div class="upload-card header-card">
<div class="card-header">
<div class="provider-section">
<button
class="provider-button"
:title="t('pages.upload.uploadViewHint')"
@click="handlePicBedNameClick(picBedName, picBedConfigName)"
>
<div class="provider-info">
<span class="provider-name">{{ picBedName }}</span>
<span class="provider-config">{{ picBedConfigName || 'Default' }}</span>
</div>
<ChevronDownIcon
:size="16"
class="provider-arrow"
/>
</button>
</div>
<div class="header-actions">
<button
class="action-button secondary"
@click="handleImageProcess"
>
<Settings :size="16" />
<span>{{ t('pages.upload.imageProcessName') }}</span>
</button>
<button
class="action-button"
@click="handleChangePicBed"
>
<DatabaseIcon :size="16" />
<span>{{ t('pages.upload.changePicBed') }}</span>
</button>
</div>
</div>
</div>
<!-- Main Upload Card -->
<div class="upload-card main-card">
<div
class="upload-zone"
:class="{ 'drag-active': dragover }"
@drop.prevent="onDrop"
@dragover.prevent="dragover = true"
@dragleave.prevent="dragover = false"
@click="openUplodWindow"
>
<div class="upload-content">
<div class="upload-icon">
<UploadCloudIcon :size="48" />
</div>
<div class="upload-text">
<h3 class="upload-title">
{{ t('pages.upload.dragFileToHere') }}
</h3>
<p class="upload-subtitle">
{{ t('pages.upload.clickToUpload') }}
</p>
<div class="upload-formats">
<span class="format-label">{{ t('pages.upload.uploadHint') }}</span>
</div>
</div>
</div>
<input
id="file-uploader"
ref="fileInput"
type="file"
multiple
style="display: none"
@change="onChange"
>
</div>
<!-- Progress Bar -->
<transition name="progress">
<div
v-if="showProgress"
class="progress-container"
>
<div class="progress-bar">
<div
class="progress-fill"
:class="{ 'progress-error': showError }"
:style="{ width: `${progress}%` }"
/>
</div>
<span class="progress-text">
{{ showError ? t('pages.upload.uploadFailed') : `${progress}%` }}
</span>
</div>
</transition>
</div>
<!-- Quick Actions Card -->
<div class="upload-card actions-card">
<div class="card-header">
<h4 class="card-title">
{{ t('pages.upload.quickUpload') }}
</h4>
</div>
<div class="quick-actions">
<button
class="quick-action-button"
@click="uploadClipboardFiles"
>
<ClipboardIcon :size="20" />
<span>{{ t('pages.upload.clipboardPicture') }}</span>
</button>
<button
class="quick-action-button"
@click="uploadURLFiles"
>
<LinkIcon :size="20" />
<span>{{ t('pages.upload.urlUpload') }}</span>
</button>
</div>
</div>
<!-- Settings Card -->
<div class="upload-card settings-card">
<div class="card-header">
<h4 class="card-title">
{{ t('pages.upload.linkFormat') }}
</h4>
</div>
<div class="settings-content">
<!-- Format Options -->
<div class="setting-group">
<label class="setting-label">{{ t('pages.upload.outputFormat') }}</label>
<div class="format-buttons">
<button
v-for="(format, key) in pasteFormatList"
:key="key"
class="format-button"
:class="{ active: pasteStyle === key }"
:title="format"
@click="updatePasteStyle(key)"
>
{{ key }}
</button>
</div>
</div>
<!-- URL Length Options -->
<div class="setting-group">
<label class="setting-label">{{ t('pages.upload.urlType.title') }}</label>
<div class="url-toggle">
<button
class="toggle-button"
:class="{ active: !useShortUrl }"
@click="updateUrlType(false)"
>
<span>{{ t('pages.upload.urlType.normal') }}</span>
</button>
<button
class="toggle-button"
:class="{ active: useShortUrl }"
@click="updateUrlType(true)"
>
<span>{{ t('pages.upload.urlType.short') }}</span>
</button>
</div>
</div>
</div>
</div>
<!-- Image Process Dialog -->
<transition name="modal">
<div
v-if="imageProcessDialogVisible"
class="modal-overlay"
@click="imageProcessDialogVisible = false"
>
<div
class="modal-container"
@click.stop
>
<div class="modal-header">
<h3 class="modal-title">
{{ t('pages.imageProcess.title') }}
</h3>
<button
class="modal-close"
@click="imageProcessDialogVisible = false"
>
<XIcon :size="20" />
</button>
</div>
<div class="modal-content">
<ImageProcessSetting v-model="imageProcessDialogVisible" />
</div>
</div>
</div>
</transition>
</div>
</template>
<script lang="ts" setup>
import type { IpcRendererEvent } from 'electron'
import { ChevronDownIcon, ClipboardIcon, DatabaseIcon, LinkIcon, Settings, UploadCloudIcon, XIcon } from 'lucide-vue-next'
import { onBeforeMount, onBeforeUnmount, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { useRouter } from 'vue-router'
import ImageProcessSetting from '@/components/ImageProcessSetting.vue'
import useMessage from '@/hooks/useMessage'
import { PICBEDS_PAGE } from '@/router/config'
import $bus from '@/utils/bus'
import { isUrl } from '@/utils/common'
import { configPaths } from '@/utils/configPaths'
import { SHOW_INPUT_BOX, SHOW_INPUT_BOX_RESPONSE } from '@/utils/constant'
import { getConfig, saveConfig } from '@/utils/dataSender'
import { useDragEventListeners } from '@/utils/drag'
import { IPasteStyle, IRPCActionType } from '@/utils/enum'
import { picBedGlobal, updatePicBedGlobal } from '@/utils/global'
import type { IFileWithPath, IUploaderConfigItem } from '#/types/types'
useDragEventListeners()
const $router = useRouter()
const { t } = useI18n()
const message = useMessage()
const imageProcessDialogVisible = ref(false)
const useShortUrl = ref(false)
const dragover = ref(false)
const progress = ref(0)
const showProgress = ref(false)
const showError = ref(false)
const pasteStyle = ref('')
const picBedName = ref('')
const picBedConfigName = ref('')
const fileInput = ref<HTMLInputElement>()
const pasteFormatList = ref<Record<string, string>>({
[IPasteStyle.MARKDOWN]: '![alt](url)',
[IPasteStyle.HTML]: '<img src="url"/>',
[IPasteStyle.URL]: 'http://test.com/test.png',
[IPasteStyle.UBB]: '[img]url[/img]',
[IPasteStyle.CUSTOM]: ''
})
watch(picBedGlobal, () => {
getDefaultPicBed()
})
const uploadProgressHandler = (_event: IpcRendererEvent, _progress: number) => {
if (_progress !== -1) {
showProgress.value = true
progress.value = _progress
} else {
progress.value = 100
showError.value = true
}
}
const syncPicBedHandler = () => {
getDefaultPicBed()
}
onBeforeMount(() => {
updatePicBedGlobal()
window.electron.ipcRendererOn('uploadProgress', uploadProgressHandler)
getUseShortUrl()
getPasteStyle()
getDefaultPicBed()
window.electron.ipcRendererOn('syncPicBed', syncPicBedHandler)
$bus.on(SHOW_INPUT_BOX_RESPONSE, handleInputBoxValue)
})
const handleImageProcess = () => {
imageProcessDialogVisible.value = true
}
watch(progress, onProgressChange)
function onProgressChange (val: number) {
if (val === 100) {
setTimeout(() => {
showProgress.value = false
showError.value = false
}, 1000)
setTimeout(() => {
progress.value = 0
}, 1200)
}
}
async function handlePicBedNameClick (_picBedName: string, picBedConfigName: string | undefined) {
const formatedpicBedConfigName = picBedConfigName || 'Default'
const currentPicBed = await getConfig<string>(configPaths.picBed.current)
const currentPicBedConfig = ((await getConfig<any[]>(`uploader.${currentPicBed}`)) as any) || {}
const configList = await window.electron.triggerRPC<IUploaderConfigItem>(IRPCActionType.PICBED_GET_CONFIG_LIST, currentPicBed)
const currentConfigList = configList?.configList ?? []
const config = currentConfigList.find((item: any) => item._configName === formatedpicBedConfigName)
$router.push({
name: PICBEDS_PAGE,
params: {
type: currentPicBed,
configId: config?._id || ''
},
query: {
defaultConfigId: currentPicBedConfig.defaultId || ''
}
})
}
onBeforeUnmount(() => {
$bus.off(SHOW_INPUT_BOX_RESPONSE)
window.electron.ipcRendererRemoveListener('uploadProgress', uploadProgressHandler)
window.electron.ipcRendererRemoveListener('syncPicBed', syncPicBedHandler)
})
function onDrop (e: DragEvent) {
dragover.value = false
// send files first
if (e.dataTransfer?.files?.length) {
ipcSendFiles(e.dataTransfer.files)
} else if (e.dataTransfer?.items) {
const items = e.dataTransfer.items
if (items.length === 2 && items[0].type === 'text/uri-list') {
handleURLDrag(items, e.dataTransfer)
} else if (items[0].type === 'text/plain') {
const str = e.dataTransfer.getData(items[0].type)
if (isUrl(str)) {
window.electron.sendRPC(IRPCActionType.UPLOAD_CHOOSED_FILES, [{ path: str }])
} else {
message.error(t('pages.upload.dragValidPictureOrUrl'))
}
}
}
}
function handleURLDrag (items: DataTransferItemList, dataTransfer: DataTransfer) {
// text/html
// Use this data to get a more precise URL
const urlString = dataTransfer.getData(items[1].type)
const urlMatch = urlString.match(/<img.*src="(.*?)"/)
if (urlMatch) {
window.electron.sendRPC(IRPCActionType.UPLOAD_CHOOSED_FILES, [
{
path: urlMatch[1]
}
])
} else {
message.error(t('pages.upload.dragValidPictureOrUrl'))
}
}
function openUplodWindow () {
fileInput.value?.click()
}
function onChange (e: any) {
ipcSendFiles(e.target.files)
;(fileInput.value as HTMLInputElement).value = ''
}
function ipcSendFiles (files: FileList) {
const sendFiles: IFileWithPath[] = []
Array.from(files).forEach(item => {
const obj = {
name: item.name,
path: window.electron.showFilePath(item)
}
sendFiles.push(obj)
})
window.electron.sendRPC(IRPCActionType.UPLOAD_CHOOSED_FILES, sendFiles)
}
async function getPasteStyle () {
pasteStyle.value = (await getConfig(configPaths.settings.pasteStyle)) || IPasteStyle.MARKDOWN
pasteFormatList.value.Custom = (await getConfig(configPaths.settings.customLink)) || '![$fileName]($url)'
}
async function getUseShortUrl () {
useShortUrl.value = (await getConfig(configPaths.settings.useShortUrl)) || false
}
function updatePasteStyle (style: string) {
pasteStyle.value = style
saveConfig({
[configPaths.settings.pasteStyle]: style || IPasteStyle.MARKDOWN
})
}
function updateUrlType (shortUrl: boolean) {
useShortUrl.value = shortUrl
saveConfig({
[configPaths.settings.useShortUrl]: shortUrl
})
}
function uploadClipboardFiles () {
window.electron.sendRPC(IRPCActionType.UPLOAD_CLIPBOARD_FILES_FROM_UPLOAD_PAGE)
}
async function uploadURLFiles () {
const str = await navigator.clipboard.readText()
$bus.emit(SHOW_INPUT_BOX, {
value: isUrl(str) ? str : '',
title: t('pages.upload.inputUrlTip'),
placeholder: t('pages.upload.httpPrefixTip')
})
}
function handleInputBoxValue (val: string) {
if (val === '') return
if (isUrl(val)) {
window.electron.sendRPC(IRPCActionType.UPLOAD_CHOOSED_FILES, [
{
path: val
}
])
} else {
message.error(t('pages.upload.inputValidUrl'))
}
}
async function getDefaultPicBed () {
const currentPicBed = await getConfig<string>(configPaths.picBed.current)
picBedGlobal.value.forEach(item => {
if (item.type === currentPicBed) {
picBedName.value = item.name
}
})
picBedConfigName.value = (await getConfig<string>(`picBed.${currentPicBed}._configName`)) || ''
}
async function handleChangePicBed () {
window.electron.sendRPC(IRPCActionType.SHOW_UPLOAD_PAGE_MENU)
}
</script>
<script lang="ts">
export default {
name: 'UploadPage'
}
</script>
<style scoped src="./css/UploadPage.css"></style>
<template>
<div class="upload-container">
<!-- Header Card -->
<div class="upload-card header-card">
<div class="card-header">
<div class="provider-section">
<button
class="provider-button"
:title="t('pages.upload.uploadViewHint')"
@click="handlePicBedNameClick(picBedName, picBedConfigName)"
>
<div class="provider-info">
<span class="provider-name">{{ picBedName }}</span>
<span class="provider-config">{{ picBedConfigName || 'Default' }}</span>
</div>
<ChevronDownIcon
:size="16"
class="provider-arrow"
/>
</button>
</div>
<div class="header-actions">
<button
class="action-button secondary"
@click="handleImageProcess"
>
<Settings :size="16" />
<span>{{ t('pages.upload.imageProcessName') }}</span>
</button>
<button
class="action-button"
@click="handleChangePicBed"
>
<DatabaseIcon :size="16" />
<span>{{ t('pages.upload.changePicBed') }}</span>
</button>
</div>
</div>
</div>
<!-- Main Upload Card -->
<div class="upload-card main-card">
<div
id="upload-area"
class="upload-zone"
:class="{ 'drag-active': dragover }"
@drop.prevent="onDrop"
@dragover.prevent="dragover = true"
@dragleave.prevent="dragover = false"
@click="openUplodWindow"
>
<div class="upload-content">
<div class="upload-icon">
<UploadCloudIcon :size="48" />
</div>
<div class="upload-text">
<h3 class="upload-title">
{{ t('pages.upload.dragFileToHere') }}
</h3>
<p class="upload-subtitle">
{{ t('pages.upload.clickToUpload') }}
</p>
<div class="upload-formats">
<span class="format-label">{{ t('pages.upload.uploadHint') }}</span>
</div>
</div>
</div>
<input
id="file-uploader"
ref="fileInput"
type="file"
multiple
style="display: none"
@change="onChange"
>
</div>
<!-- Progress Bar -->
<transition name="progress">
<div
v-if="showProgress"
class="progress-container"
>
<div class="progress-bar">
<div
class="progress-fill"
:class="{ 'progress-error': showError }"
:style="{ width: `${progress}%` }"
/>
</div>
<span class="progress-text">
{{ showError ? t('pages.upload.uploadFailed') : `${progress}%` }}
</span>
</div>
</transition>
</div>
<!-- Quick Actions Card -->
<div class="upload-card actions-card">
<div class="card-header">
<h4 class="card-title">
{{ t('pages.upload.quickUpload') }}
</h4>
</div>
<div class="quick-actions">
<button
class="quick-action-button"
@click="uploadClipboardFiles"
>
<ClipboardIcon :size="20" />
<span>{{ t('pages.upload.clipboardPicture') }}</span>
</button>
<button
class="quick-action-button"
@click="uploadURLFiles"
>
<LinkIcon :size="20" />
<span>{{ t('pages.upload.urlUpload') }}</span>
</button>
</div>
</div>
<!-- Settings Card -->
<div class="upload-card settings-card">
<div class="card-header">
<h4 class="card-title">
{{ t('pages.upload.linkFormat') }}
</h4>
</div>
<div class="settings-content">
<!-- Format Options -->
<div class="setting-group">
<label class="setting-label">{{ t('pages.upload.outputFormat') }}</label>
<div class="format-buttons">
<button
v-for="(format, key) in pasteFormatList"
:key="key"
class="format-button"
:class="{ active: pasteStyle === key }"
:title="format"
@click="updatePasteStyle(key)"
>
{{ key }}
</button>
</div>
</div>
<!-- URL Length Options -->
<div class="setting-group">
<label class="setting-label">{{ t('pages.upload.urlType.title') }}</label>
<div class="url-toggle">
<button
class="toggle-button"
:class="{ active: !useShortUrl }"
@click="updateUrlType(false)"
>
<span>{{ t('pages.upload.urlType.normal') }}</span>
</button>
<button
class="toggle-button"
:class="{ active: useShortUrl }"
@click="updateUrlType(true)"
>
<span>{{ t('pages.upload.urlType.short') }}</span>
</button>
</div>
</div>
</div>
</div>
<!-- Image Process Dialog -->
<transition name="modal">
<div
v-if="imageProcessDialogVisible"
class="modal-overlay"
@click="imageProcessDialogVisible = false"
>
<div
class="modal-container"
@click.stop
>
<div class="modal-header">
<h3 class="modal-title">
{{ t('pages.imageProcess.title') }}
</h3>
<button
class="modal-close"
@click="imageProcessDialogVisible = false"
>
<XIcon :size="20" />
</button>
</div>
<div class="modal-content">
<ImageProcessSetting v-model="imageProcessDialogVisible" />
</div>
</div>
</div>
</transition>
</div>
</template>
<script lang="ts" setup>
import type { IpcRendererEvent } from 'electron'
import { ChevronDownIcon, ClipboardIcon, DatabaseIcon, LinkIcon, Settings, UploadCloudIcon, XIcon } from 'lucide-vue-next'
import { onBeforeMount, onBeforeUnmount, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { useRouter } from 'vue-router'
import ImageProcessSetting from '@/components/ImageProcessSetting.vue'
import useMessage from '@/hooks/useMessage'
import { PICBEDS_PAGE } from '@/router/config'
import $bus from '@/utils/bus'
import { isUrl } from '@/utils/common'
import { configPaths } from '@/utils/configPaths'
import { SHOW_INPUT_BOX, SHOW_INPUT_BOX_RESPONSE } from '@/utils/constant'
import { getConfig, saveConfig } from '@/utils/dataSender'
import { useDragEventListeners } from '@/utils/drag'
import { IPasteStyle, IRPCActionType } from '@/utils/enum'
import { picBedGlobal, updatePicBedGlobal } from '@/utils/global'
import type { IFileWithPath, IUploaderConfigItem } from '#/types/types'
useDragEventListeners()
const $router = useRouter()
const { t } = useI18n()
const message = useMessage()
const imageProcessDialogVisible = ref(false)
const useShortUrl = ref(false)
const dragover = ref(false)
const progress = ref(0)
const showProgress = ref(false)
const showError = ref(false)
const pasteStyle = ref('')
const picBedName = ref('')
const picBedConfigName = ref('')
const fileInput = ref<HTMLInputElement>()
const pasteFormatList = ref<Record<string, string>>({
[IPasteStyle.MARKDOWN]: '![alt](url)',
[IPasteStyle.HTML]: '<img src="url"/>',
[IPasteStyle.URL]: 'http://test.com/test.png',
[IPasteStyle.UBB]: '[img]url[/img]',
[IPasteStyle.CUSTOM]: ''
})
watch(picBedGlobal, () => {
getDefaultPicBed()
})
const uploadProgressHandler = (_event: IpcRendererEvent, _progress: number) => {
if (_progress !== -1) {
showProgress.value = true
progress.value = _progress
} else {
progress.value = 100
showError.value = true
}
}
const syncPicBedHandler = () => {
getDefaultPicBed()
}
onBeforeMount(() => {
updatePicBedGlobal()
window.electron.ipcRendererOn('uploadProgress', uploadProgressHandler)
getUseShortUrl()
getPasteStyle()
getDefaultPicBed()
window.electron.ipcRendererOn('syncPicBed', syncPicBedHandler)
$bus.on(SHOW_INPUT_BOX_RESPONSE, handleInputBoxValue)
})
const handleImageProcess = () => {
imageProcessDialogVisible.value = true
}
watch(progress, onProgressChange)
function onProgressChange (val: number) {
if (val === 100) {
setTimeout(() => {
showProgress.value = false
showError.value = false
}, 1000)
setTimeout(() => {
progress.value = 0
}, 1200)
}
}
async function handlePicBedNameClick (_picBedName: string, picBedConfigName: string | undefined) {
const formatedpicBedConfigName = picBedConfigName || 'Default'
const currentPicBed = await getConfig<string>(configPaths.picBed.current)
const currentPicBedConfig = ((await getConfig<any[]>(`uploader.${currentPicBed}`)) as any) || {}
const configList = await window.electron.triggerRPC<IUploaderConfigItem>(IRPCActionType.PICBED_GET_CONFIG_LIST, currentPicBed)
const currentConfigList = configList?.configList ?? []
const config = currentConfigList.find((item: any) => item._configName === formatedpicBedConfigName)
$router.push({
name: PICBEDS_PAGE,
params: {
type: currentPicBed,
configId: config?._id || ''
},
query: {
defaultConfigId: currentPicBedConfig.defaultId || ''
}
})
}
onBeforeUnmount(() => {
$bus.off(SHOW_INPUT_BOX_RESPONSE)
window.electron.ipcRendererRemoveListener('uploadProgress', uploadProgressHandler)
window.electron.ipcRendererRemoveListener('syncPicBed', syncPicBedHandler)
})
function onDrop (e: DragEvent) {
dragover.value = false
// send files first
if (e.dataTransfer?.files?.length) {
ipcSendFiles(e.dataTransfer.files)
} else if (e.dataTransfer?.items) {
const items = e.dataTransfer.items
if (items.length === 2 && items[0].type === 'text/uri-list') {
handleURLDrag(items, e.dataTransfer)
} else if (items[0].type === 'text/plain') {
const str = e.dataTransfer.getData(items[0].type)
if (isUrl(str)) {
window.electron.sendRPC(IRPCActionType.UPLOAD_CHOOSED_FILES, [{ path: str }])
} else {
message.error(t('pages.upload.dragValidPictureOrUrl'))
}
}
}
}
function handleURLDrag (items: DataTransferItemList, dataTransfer: DataTransfer) {
// text/html
// Use this data to get a more precise URL
const urlString = dataTransfer.getData(items[1].type)
const urlMatch = urlString.match(/<img.*src="(.*?)"/)
if (urlMatch) {
window.electron.sendRPC(IRPCActionType.UPLOAD_CHOOSED_FILES, [
{
path: urlMatch[1]
}
])
} else {
message.error(t('pages.upload.dragValidPictureOrUrl'))
}
}
function openUplodWindow () {
fileInput.value?.click()
}
function onChange (e: any) {
ipcSendFiles(e.target.files)
;(fileInput.value as HTMLInputElement).value = ''
}
function ipcSendFiles (files: FileList) {
const sendFiles: IFileWithPath[] = []
Array.from(files).forEach(item => {
const obj = {
name: item.name,
path: window.electron.showFilePath(item)
}
sendFiles.push(obj)
})
window.electron.sendRPC(IRPCActionType.UPLOAD_CHOOSED_FILES, sendFiles)
}
async function getPasteStyle () {
pasteStyle.value = (await getConfig(configPaths.settings.pasteStyle)) || IPasteStyle.MARKDOWN
pasteFormatList.value.Custom = (await getConfig(configPaths.settings.customLink)) || '![$fileName]($url)'
}
async function getUseShortUrl () {
useShortUrl.value = (await getConfig(configPaths.settings.useShortUrl)) || false
}
function updatePasteStyle (style: string) {
pasteStyle.value = style
saveConfig({
[configPaths.settings.pasteStyle]: style || IPasteStyle.MARKDOWN
})
}
function updateUrlType (shortUrl: boolean) {
useShortUrl.value = shortUrl
saveConfig({
[configPaths.settings.useShortUrl]: shortUrl
})
}
function uploadClipboardFiles () {
window.electron.sendRPC(IRPCActionType.UPLOAD_CLIPBOARD_FILES_FROM_UPLOAD_PAGE)
}
async function uploadURLFiles () {
const str = await navigator.clipboard.readText()
$bus.emit(SHOW_INPUT_BOX, {
value: isUrl(str) ? str : '',
title: t('pages.upload.inputUrlTip'),
placeholder: t('pages.upload.httpPrefixTip')
})
}
function handleInputBoxValue (val: string) {
if (val === '') return
if (isUrl(val)) {
window.electron.sendRPC(IRPCActionType.UPLOAD_CHOOSED_FILES, [
{
path: val
}
])
} else {
message.error(t('pages.upload.inputValidUrl'))
}
}
async function getDefaultPicBed () {
const currentPicBed = await getConfig<string>(configPaths.picBed.current)
picBedGlobal.value.forEach(item => {
if (item.type === currentPicBed) {
picBedName.value = item.name
}
})
picBedConfigName.value = (await getConfig<string>(`picBed.${currentPicBed}._configName`)) || ''
}
async function handleChangePicBed () {
window.electron.sendRPC(IRPCActionType.SHOW_UPLOAD_PAGE_MENU)
}
</script>
<script lang="ts">
export default {
name: 'UploadPage'
}
</script>
<style scoped src="./css/UploadPage.css"></style>