🐛 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
+277 -277
View File
@@ -1,277 +1,277 @@
import path from 'node:path' import path from 'node:path'
import { fileURLToPath } from 'node:url' import { fileURLToPath } from 'node:url'
import bus from '@core/bus' import bus from '@core/bus'
import { CREATE_APP_MENU } from '@core/bus/constants' import { CREATE_APP_MENU } from '@core/bus/constants'
import db from '@core/datastore' import db from '@core/datastore'
import { app, BrowserWindow, Rectangle } from 'electron' import { app, BrowserWindow, Rectangle } from 'electron'
import type { IWindowListItem } from '#/types/electron' import type { IWindowListItem } from '#/types/electron'
import type { IBrowserWindowOptions } from '#/types/types' import type { IBrowserWindowOptions } from '#/types/types'
import { TOGGLE_SHORTKEY_MODIFIED_MODE } from '~/events/constant' import { TOGGLE_SHORTKEY_MODIFIED_MODE } from '~/events/constant'
import { T as $t } from '~/i18n' import { T as $t } from '~/i18n'
import { configPaths } from '~/utils/configPaths' import { configPaths } from '~/utils/configPaths'
import { IWindowList } from '~/utils/enum' import { IWindowList } from '~/utils/enum'
import logo from '../../../../../resources/logo.png?asset' import logo from '../../../../../resources/logo.png?asset'
const windowList = new Map<string, IWindowListItem>() const windowList = new Map<string, IWindowListItem>()
const getDefaultWindowSizes = (): { width: number; height: number } => { const getDefaultWindowSizes = (): { width: number; height: number } => {
const [mainWindowWidth, mainWindowHeight] = db.get([ const [mainWindowWidth, mainWindowHeight] = db.get([
configPaths.settings.mainWindowWidth, configPaths.settings.mainWindowWidth,
configPaths.settings.mainWindowHeight configPaths.settings.mainWindowHeight
]) ])
return { return {
width: mainWindowWidth || 1200, width: mainWindowWidth || 1200,
height: mainWindowHeight || 800 height: mainWindowHeight || 800
} }
} }
function setMiniWindowShape (win: BrowserWindow) { function setMiniWindowShape (win: BrowserWindow) {
const radius = 32 const radius = 32
const shape: Rectangle[] = [] const shape: Rectangle[] = []
for (let y = -radius; y <= radius; y++) { for (let y = -radius; y <= radius; y++) {
for (let x = -radius; x <= radius; x++) { for (let x = -radius; x <= radius; x++) {
if (x * x + y * y <= radius * radius) { if (x * x + y * y <= radius * radius) {
shape.push({ x: radius + x, y: radius + y, width: 1, height: 1 }) shape.push({ x: radius + x, y: radius + y, width: 1, height: 1 })
} }
} }
} }
win.setShape(shape) win.setShape(shape)
} }
const __dirname = path.dirname(fileURLToPath(import.meta.url)) const __dirname = path.dirname(fileURLToPath(import.meta.url))
const preloadPath = fileURLToPath(new URL('../preload/index.mjs', import.meta.url)) const preloadPath = fileURLToPath(new URL('../preload/index.mjs', import.meta.url))
const { width: defaultWindowWidth, height: defaultWindowHeight } = getDefaultWindowSizes() const { width: defaultWindowWidth, height: defaultWindowHeight } = getDefaultWindowSizes()
const trayWindowOptions = { const trayWindowOptions = {
height: 350, height: 350,
width: 196, width: 196,
show: false, show: false,
frame: false, frame: false,
fullscreenable: false, fullscreenable: false,
resizable: false, resizable: false,
transparent: true, transparent: true,
vibrancy: 'ultra-dark', vibrancy: 'ultra-dark',
webPreferences: { webPreferences: {
sandbox: false, sandbox: false,
preload: preloadPath, preload: preloadPath,
nodeIntegration: false, nodeIntegration: false,
contextIsolation: true, contextIsolation: true,
nodeIntegrationInWorker: true, nodeIntegrationInWorker: true,
backgroundThrottling: false, backgroundThrottling: false,
webSecurity: false webSecurity: false
} }
} }
const settingWindowOptions = { const settingWindowOptions = {
height: defaultWindowHeight, height: defaultWindowHeight,
width: defaultWindowWidth, width: defaultWindowWidth,
show: false, show: false,
frame: true, frame: true,
center: true, center: true,
fullscreenable: true, fullscreenable: true,
resizable: true, resizable: true,
title: 'PicList', title: 'PicList',
transparent: false, transparent: false,
backgroundColor: '#ebeef5', backgroundColor: '#ebeef5',
titleBarStyle: 'hidden', titleBarStyle: 'hidden',
webPreferences: { webPreferences: {
sandbox: false, sandbox: false,
webviewTag: true, webviewTag: true,
backgroundThrottling: false, backgroundThrottling: false,
preload: preloadPath, preload: preloadPath,
nodeIntegration: false, nodeIntegration: false,
contextIsolation: true, contextIsolation: true,
nodeIntegrationInWorker: true, nodeIntegrationInWorker: true,
webSecurity: false webSecurity: false
} }
} as IBrowserWindowOptions } as IBrowserWindowOptions
if (process.platform !== 'darwin') { if (process.platform !== 'darwin') {
settingWindowOptions.frame = false settingWindowOptions.frame = false
settingWindowOptions.icon = '../../../../../resources/logo.png' settingWindowOptions.icon = '../../../../../resources/logo.png'
} }
const miniWindowOptions = { const miniWindowOptions = {
height: 64, height: 64,
width: 64, width: 64,
show: process.platform === 'linux', show: process.platform === 'linux',
frame: false, frame: false,
fullscreenable: false, fullscreenable: false,
skipTaskbar: true, skipTaskbar: true,
resizable: false, resizable: false,
transparent: process.platform !== 'linux', transparent: process.platform !== 'linux',
icon: logo, icon: logo,
webPreferences: { webPreferences: {
sandbox: false, sandbox: false,
preload: preloadPath, preload: preloadPath,
nodeIntegration: false, nodeIntegration: false,
contextIsolation: true, contextIsolation: true,
backgroundThrottling: false, backgroundThrottling: false,
nodeIntegrationInWorker: true nodeIntegrationInWorker: true
} }
} as IBrowserWindowOptions } as IBrowserWindowOptions
if (db.get(configPaths.settings.miniWindowOntop)) { if (db.get(configPaths.settings.miniWindowOntop)) {
miniWindowOptions.alwaysOnTop = true miniWindowOptions.alwaysOnTop = true
} }
const renameWindowOptions = { const renameWindowOptions = {
height: 250, height: 270,
width: 350, width: 350,
show: true, show: true,
fullscreenable: false, fullscreenable: false,
icon: logo, icon: logo,
resizable: true, resizable: true,
webPreferences: { webPreferences: {
sandbox: false, sandbox: false,
preload: preloadPath, preload: preloadPath,
nodeIntegration: false, nodeIntegration: false,
contextIsolation: true, contextIsolation: true,
nodeIntegrationInWorker: true, nodeIntegrationInWorker: true,
backgroundThrottling: false backgroundThrottling: false
} }
} as IBrowserWindowOptions } as IBrowserWindowOptions
if (process.platform !== 'darwin') { if (process.platform !== 'darwin') {
renameWindowOptions.show = true renameWindowOptions.show = true
renameWindowOptions.backgroundColor = '#3f3c37' renameWindowOptions.backgroundColor = '#3f3c37'
renameWindowOptions.autoHideMenuBar = true renameWindowOptions.autoHideMenuBar = true
renameWindowOptions.transparent = false renameWindowOptions.transparent = false
} }
const toolboxWindowOptions = { const toolboxWindowOptions = {
height: 450, height: 450,
width: 800, width: 800,
show: false, show: false,
frame: true, frame: true,
center: true, center: true,
fullscreenable: false, fullscreenable: false,
resizable: false, resizable: false,
title: `PicList ${$t('TOOLBOX')}`, title: `PicList ${$t('TOOLBOX')}`,
backgroundColor: '#ebeef5', backgroundColor: '#ebeef5',
icon: logo, icon: logo,
webPreferences: { webPreferences: {
sandbox: false, sandbox: false,
backgroundThrottling: false, backgroundThrottling: false,
preload: preloadPath, preload: preloadPath,
nodeIntegration: false, nodeIntegration: false,
contextIsolation: true, contextIsolation: true,
nodeIntegrationInWorker: true, nodeIntegrationInWorker: true,
webSecurity: false webSecurity: false
} }
} as IBrowserWindowOptions } as IBrowserWindowOptions
if (process.platform !== 'darwin') { if (process.platform !== 'darwin') {
toolboxWindowOptions.backgroundColor = '#3f3c37' toolboxWindowOptions.backgroundColor = '#3f3c37'
toolboxWindowOptions.autoHideMenuBar = true toolboxWindowOptions.autoHideMenuBar = true
toolboxWindowOptions.transparent = false toolboxWindowOptions.transparent = false
} }
windowList.set(IWindowList.TRAY_WINDOW, { windowList.set(IWindowList.TRAY_WINDOW, {
isValid: process.platform !== 'linux', isValid: process.platform !== 'linux',
multiple: false, multiple: false,
options: () => trayWindowOptions, options: () => trayWindowOptions,
callback (window) { callback (window) {
if (!app.isPackaged && process.env.ELECTRON_RENDERER_URL) { if (!app.isPackaged && process.env.ELECTRON_RENDERER_URL) {
window.loadURL(process.env.ELECTRON_RENDERER_URL) window.loadURL(process.env.ELECTRON_RENDERER_URL)
} else { } else {
window.loadFile(path.join(__dirname, '../render/index.html')) window.loadFile(path.join(__dirname, '../render/index.html'))
} }
window.on('blur', () => { window.on('blur', () => {
window.hide() window.hide()
}) })
} }
}) })
windowList.set(IWindowList.SETTING_WINDOW, { windowList.set(IWindowList.SETTING_WINDOW, {
isValid: true, isValid: true,
multiple: false, multiple: false,
options: () => settingWindowOptions, options: () => settingWindowOptions,
callback (window, windowManager) { callback (window, windowManager) {
if (!app.isPackaged && process.env.ELECTRON_RENDERER_URL) { if (!app.isPackaged && process.env.ELECTRON_RENDERER_URL) {
window.loadURL(`${process.env.ELECTRON_RENDERER_URL}#main-page/upload`) window.loadURL(`${process.env.ELECTRON_RENDERER_URL}#main-page/upload`)
} else { } else {
window.loadFile(path.join(__dirname, '../render/index.html'), { window.loadFile(path.join(__dirname, '../render/index.html'), {
hash: 'main-page/upload' hash: 'main-page/upload'
}) })
} }
window.on('closed', () => { window.on('closed', () => {
bus.emit(TOGGLE_SHORTKEY_MODIFIED_MODE, false) bus.emit(TOGGLE_SHORTKEY_MODIFIED_MODE, false)
if (process.platform === 'linux') { if (process.platform === 'linux') {
process.nextTick(() => { process.nextTick(() => {
app.quit() app.quit()
}) })
} }
}) })
bus.emit(CREATE_APP_MENU) bus.emit(CREATE_APP_MENU)
windowManager.create(IWindowList.MINI_WINDOW) windowManager.create(IWindowList.MINI_WINDOW)
} }
}) })
windowList.set(IWindowList.MINI_WINDOW, { windowList.set(IWindowList.MINI_WINDOW, {
isValid: process.platform !== 'darwin', isValid: process.platform !== 'darwin',
multiple: false, multiple: false,
options: () => miniWindowOptions, options: () => miniWindowOptions,
callback (window) { callback (window) {
if (!app.isPackaged && process.env.ELECTRON_RENDERER_URL) { if (!app.isPackaged && process.env.ELECTRON_RENDERER_URL) {
window.loadURL(`${process.env.ELECTRON_RENDERER_URL}#mini-page`) window.loadURL(`${process.env.ELECTRON_RENDERER_URL}#mini-page`)
} else { } else {
window.loadFile(path.join(__dirname, '../render/index.html'), { window.loadFile(path.join(__dirname, '../render/index.html'), {
hash: 'mini-page' hash: 'mini-page'
}) })
} }
setMiniWindowShape(window) setMiniWindowShape(window)
} }
}) })
windowList.set(IWindowList.RENAME_WINDOW, { windowList.set(IWindowList.RENAME_WINDOW, {
isValid: true, isValid: true,
multiple: true, multiple: true,
options: () => renameWindowOptions, options: () => renameWindowOptions,
async callback (window, windowManager) { async callback (window, windowManager) {
if (!app.isPackaged && process.env.ELECTRON_RENDERER_URL) { if (!app.isPackaged && process.env.ELECTRON_RENDERER_URL) {
window.loadURL(`${process.env.ELECTRON_RENDERER_URL}#rename-page`) window.loadURL(`${process.env.ELECTRON_RENDERER_URL}#rename-page`)
} else { } else {
window.loadFile(path.join(__dirname, '../render/index.html'), { window.loadFile(path.join(__dirname, '../render/index.html'), {
hash: 'rename-page' hash: 'rename-page'
}) })
} }
const currentWindow = windowManager.getAvailableWindow(true) const currentWindow = windowManager.getAvailableWindow(true)
if (currentWindow && currentWindow.isVisible()) { if (currentWindow && currentWindow.isVisible()) {
const { x, y, width, height } = currentWindow.getBounds() const { x, y, width, height } = currentWindow.getBounds()
const positionX = Math.floor(x + width / 2 - 150) const positionX = Math.floor(x + width / 2 - 150)
const positionY = Math.floor(y + height / 2 - (height > 400 ? 88 : 0)) const positionY = Math.floor(y + height / 2 - (height > 400 ? 88 : 0))
window.setPosition(positionX, positionY, false) window.setPosition(positionX, positionY, false)
} }
} }
}) })
windowList.set(IWindowList.TOOLBOX_WINDOW, { windowList.set(IWindowList.TOOLBOX_WINDOW, {
isValid: true, isValid: true,
multiple: false, multiple: false,
options: () => toolboxWindowOptions, options: () => toolboxWindowOptions,
async callback (window, windowManager) { async callback (window, windowManager) {
if (!app.isPackaged && process.env.ELECTRON_RENDERER_URL) { if (!app.isPackaged && process.env.ELECTRON_RENDERER_URL) {
window.loadURL(`${process.env.ELECTRON_RENDERER_URL}#toolbox-page`) window.loadURL(`${process.env.ELECTRON_RENDERER_URL}#toolbox-page`)
} else { } else {
window.loadFile(path.join(__dirname, '../render/index.html'), { window.loadFile(path.join(__dirname, '../render/index.html'), {
hash: 'toolbox-page' hash: 'toolbox-page'
}) })
} }
const currentWindow = windowManager.getAvailableWindow(true) const currentWindow = windowManager.getAvailableWindow(true)
if (currentWindow && currentWindow.isVisible()) { if (currentWindow && currentWindow.isVisible()) {
const { x, y, width, height } = currentWindow.getBounds() const { x, y, width, height } = currentWindow.getBounds()
const positionX = Math.floor(x + width / 2 - 400) const positionX = Math.floor(x + width / 2 - 400)
const positionY = Math.floor(y + height / 2 - (height > 400 ? 225 : 0)) const positionY = Math.floor(y + height / 2 - (height > 400 ? 225 : 0))
window.setPosition(positionX, positionY, false) window.setPosition(positionX, positionY, false)
} }
} }
}) })
export default windowList export default windowList
+324 -335
View File
@@ -1,335 +1,324 @@
import path from 'node:path' import path from 'node:path'
import db from '@core/datastore' import db from '@core/datastore'
import logger from '@core/picgo/logger' import logger from '@core/picgo/logger'
import axios from 'axios' import axios from 'axios'
import { clipboard, dialog, Notification, Tray } from 'electron' import { clipboard, Notification, Tray } from 'electron'
import FormData from 'form-data' import FormData from 'form-data'
import fs from 'fs-extra' import fs from 'fs-extra'
import { isReactive, isRef, toRaw, unref } from 'vue' import { isReactive, isRef, toRaw, unref } from 'vue'
import type { IHTTPProxy, IPrivateShowNotificationOption, IShowMessageBoxResult, IStringKeyMap } from '#/types/types' import type { IHTTPProxy, IPrivateShowNotificationOption, IStringKeyMap } from '#/types/types'
import { configPaths } from '~/utils/configPaths' import { configPaths } from '~/utils/configPaths'
import { IShortUrlServer } from '~/utils/enum' import { IShortUrlServer } from '~/utils/enum'
/** /**
* get raw data from reactive or ref * get raw data from reactive or ref
*/ */
export const getRawData = (args: any): any => { export const getRawData = (args: any): any => {
if (isRef(args)) return unref(args) if (isRef(args)) return unref(args)
if (isReactive(args)) return toRaw(args) if (isReactive(args)) return toRaw(args)
if (Array.isArray(args)) return args.map(getRawData) if (Array.isArray(args)) return args.map(getRawData)
if (typeof args === 'object' && args !== null) { if (typeof args === 'object' && args !== null) {
const data = {} as Record<string, any> const data = {} as Record<string, any>
for (const key in args) { for (const key in args) {
data[key] = getRawData(args[key]) data[key] = getRawData(args[key])
} }
return data return data
} }
return args return args
} }
const getExtension = (fileName: string) => path.extname(fileName).slice(1) const getExtension = (fileName: string) => path.extname(fileName).slice(1)
export const isImage = (fileName: string) => export const isImage = (fileName: string) =>
['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp', 'ico', 'svg', 'avif'].includes(getExtension(fileName)) ['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp', 'ico', 'svg', 'avif'].includes(getExtension(fileName))
export let tray: Tray export let tray: Tray
export const setTray = (t: Tray) => { export const setTray = (t: Tray) => {
tray = t tray = t
} }
export const getTray = () => tray export const getTray = () => tray
export function setTrayToolTip (title: string): void { export function setTrayToolTip (title: string): void {
if (tray) { if (tray) {
tray.setToolTip(title) tray.setToolTip(title)
} }
} }
export const handleCopyUrl = (str: string): void => { export const handleCopyUrl = (str: string): void => {
if (db.get(configPaths.settings.autoCopy) !== false) { if (db.get(configPaths.settings.autoCopy) !== false) {
clipboard.writeText(str) clipboard.writeText(str)
} }
} }
/** /**
* show notification * show notification
* @param options * @param options
*/ */
export const showNotification = ( export const showNotification = (
options: IPrivateShowNotificationOption = { options: IPrivateShowNotificationOption = {
title: '', title: '',
body: '', body: '',
clickToCopy: false, clickToCopy: false,
copyContent: '', copyContent: '',
clickFn: () => {} clickFn: () => {}
} }
) => { ) => {
const notification = new Notification({ const notification = new Notification({
title: options.title, title: options.title,
body: options.body body: options.body
// icon: options.icon || undefined // icon: options.icon || undefined
}) })
const handleClick = () => { const handleClick = () => {
if (options.clickToCopy) { if (options.clickToCopy) {
clipboard.writeText(options.copyContent || options.body) clipboard.writeText(options.copyContent || options.body)
} }
if (options.clickFn) { if (options.clickFn) {
options.clickFn() options.clickFn()
} }
} }
notification.once('click', handleClick) notification.once('click', handleClick)
notification.once('close', () => { notification.once('close', () => {
notification.removeListener('click', handleClick) notification.removeListener('click', handleClick)
}) })
notification.show() notification.show()
} }
export const showMessageBox = (options: any) => { /**
return new Promise<IShowMessageBoxResult>(resolve => { * macOS public.file-url will get encoded file path,
dialog.showMessageBox(options).then(res => { * so we need to decode it
resolve({ */
result: res.response, export const ensureFilePath = (filePath: string, prefix = 'file://'): string => {
checkboxChecked: res.checkboxChecked filePath = filePath.replace(prefix, '')
}) if (fs.existsSync(filePath)) {
}) return `${prefix}${filePath}`
}) }
} filePath = decodeURIComponent(filePath)
if (fs.existsSync(filePath)) {
/** return `${prefix}${filePath}`
* macOS public.file-url will get encoded file path, }
* so we need to decode it return ''
*/ }
export const ensureFilePath = (filePath: string, prefix = 'file://'): string => {
filePath = filePath.replace(prefix, '') /**
if (fs.existsSync(filePath)) { * for builtin clipboard to get image path from clipboard
return `${prefix}${filePath}` * @returns
} */
filePath = decodeURIComponent(filePath) export const getClipboardFilePath = (): string => {
if (fs.existsSync(filePath)) { // TODO: linux support
return `${prefix}${filePath}` const img = clipboard.readImage()
} const platform = process.platform
return ''
} if (!img.isEmpty() && platform === 'darwin') {
let imgPath = clipboard.read('public.file-url') // will get file://xxx/xxx
/** imgPath = ensureFilePath(imgPath)
* for builtin clipboard to get image path from clipboard return imgPath ? imgPath.replace('file://', '') : ''
* @returns }
*/
export const getClipboardFilePath = (): string => { if (img.isEmpty() && platform === 'win32') {
// TODO: linux support const imgPath = clipboard
const img = clipboard.readImage() .readBuffer('FileNameW')
const platform = process.platform ?.toString('ucs2')
?.replace(RegExp(String.fromCharCode(0), 'g'), '')
if (!img.isEmpty() && platform === 'darwin') { return imgPath || ''
let imgPath = clipboard.read('public.file-url') // will get file://xxx/xxx }
imgPath = ensureFilePath(imgPath)
return imgPath ? imgPath.replace('file://', '') : '' return ''
} }
if (img.isEmpty() && platform === 'win32') { const c1nApi = 'https://c1n.cn/link/short'
const imgPath = clipboard
.readBuffer('FileNameW') const createC1NShortUrl = async (url: string) => {
?.toString('ucs2') const c1nToken = db.get(configPaths.settings.c1nToken) || ''
?.replace(RegExp(String.fromCharCode(0), 'g'), '') if (!c1nToken) {
return imgPath || '' logger.warn('c1n token is not set')
} return url
}
return '' try {
} const form = new FormData()
form.append('url', url)
const c1nApi = 'https://c1n.cn/link/short' const res = await axios.post(c1nApi, form, {
headers: {
const generateC1NShortUrl = async (url: string) => { token: c1nToken
const c1nToken = db.get(configPaths.settings.c1nToken) || '' }
if (!c1nToken) { })
logger.warn('c1n token is not set') if (res.status >= 200 && res.status < 300 && res.data?.code === 0) {
return url return res.data.data
} }
try { } catch (e: any) {
const form = new FormData() logger.error(e)
form.append('url', url) }
const res = await axios.post(c1nApi, form, { return url
headers: { }
token: c1nToken
} const createYOURLSShortLink = async (url: string) => {
}) let domain = db.get(configPaths.settings.yourlsDomain) || ''
if (res.status >= 200 && res.status < 300 && res.data?.code === 0) { const signature = db.get(configPaths.settings.yourlsSignature) || ''
return res.data.data
} if (!domain || !signature) {
} catch (e: any) { logger.warn('Yourls server or signature is not set')
logger.error(e) return url
} }
return url if (!/^https?:\/\//.test(domain)) {
} domain = `http://${domain}`
}
const generateYOURLSShortUrl = async (url: string) => { const params = new URLSearchParams({
let domain = db.get(configPaths.settings.yourlsDomain) || '' signature,
const signature = db.get(configPaths.settings.yourlsSignature) || '' action: 'shorturl',
format: 'json',
if (!domain || !signature) { url
logger.warn('Yourls server or signature is not set') })
return url try {
} const res = await axios.get(`${domain}/yourls-api.php?${params.toString()}`)
if (!/^https?:\/\//.test(domain)) { if (res.data?.shorturl) {
domain = `http://${domain}` return res.data.shorturl
} }
const params = new URLSearchParams({ } catch (e: any) {
signature, if (e.response?.data?.message?.includes('already exists in database')) {
action: 'shorturl', return e.response.data.shorturl
format: 'json', }
url logger.error(e)
}) }
try {
const res = await axios.get(`${domain}/yourls-api.php?${params.toString()}`) return url
if (res.data?.shorturl) { }
return res.data.shorturl
} const createShortUrlForCFWorker = async (url: string) => {
} catch (e: any) { let cfWorkerHost = db.get(configPaths.settings.cfWorkerHost) || ''
if (e.response?.data?.message?.includes('already exists in database')) { cfWorkerHost = cfWorkerHost.replace(/\/$/, '')
return e.response.data.shorturl if (!cfWorkerHost) {
} logger.warn('CF Worker host is not set')
logger.error(e) return url
} }
return url try {
} const res = await axios.post(cfWorkerHost, { url })
if (res.data?.status === 200 && res.data?.key?.startsWith('/')) {
const generateCFWORKERShortUrl = async (url: string) => { return `${cfWorkerHost}${res.data.key}`
let cfWorkerHost = db.get(configPaths.settings.cfWorkerHost) || '' }
cfWorkerHost = cfWorkerHost.replace(/\/$/, '') } catch (e: any) {
if (!cfWorkerHost) { logger.error(e)
logger.warn('CF Worker host is not set') }
return url
} return url
}
try {
const res = await axios.post(cfWorkerHost, { url }) const createShortUrlFromSink = async (url: string) => {
if (res.data?.status === 200 && res.data?.key?.startsWith('/')) { let sinkDomain = db.get(configPaths.settings.sinkDomain) || ''
return `${cfWorkerHost}${res.data.key}` const sinkToken = db.get(configPaths.settings.sinkToken) || ''
} if (!sinkDomain || !sinkToken) {
} catch (e: any) { logger.warn('Sink domain or token is not set')
logger.error(e) return url
} }
if (!/^https?:\/\//.test(sinkDomain)) {
return url sinkDomain = `http://${sinkDomain}`
} }
if (sinkDomain.endsWith('/')) {
const generateSinkShortUrl = async (url: string) => { sinkDomain = sinkDomain.slice(0, -1)
let sinkDomain = db.get(configPaths.settings.sinkDomain) || '' }
const sinkToken = db.get(configPaths.settings.sinkToken) || '' try {
if (!sinkDomain || !sinkToken) { const res = await axios.post(
logger.warn('Sink domain or token is not set') `${sinkDomain}/api/link/create`,
return url { url },
} { headers: { Authorization: `Bearer ${sinkToken}` } }
if (!/^https?:\/\//.test(sinkDomain)) { )
sinkDomain = `http://${sinkDomain}` if (res.data?.link?.slug) {
} return `${sinkDomain}/${res.data.link.slug}`
if (sinkDomain.endsWith('/')) { }
sinkDomain = sinkDomain.slice(0, -1) } catch (e: any) {
} logger.error(e)
try { }
const res = await axios.post( return url
`${sinkDomain}/api/link/create`, }
{ url },
{ headers: { Authorization: `Bearer ${sinkToken}` } } export const generateShortUrl = async (url: string) => {
) const server = db.get(configPaths.settings.shortUrlServer) || IShortUrlServer.C1N
if (res.data?.link?.slug) { switch (server) {
return `${sinkDomain}/${res.data.link.slug}` case IShortUrlServer.C1N:
} return createC1NShortUrl(url)
} catch (e: any) { case IShortUrlServer.YOURLS:
logger.error(e) return createYOURLSShortLink(url)
} case IShortUrlServer.CFWORKER:
return url return createShortUrlForCFWorker(url)
} case IShortUrlServer.SINK:
return createShortUrlFromSink(url)
export const generateShortUrl = async (url: string) => { default:
const server = db.get(configPaths.settings.shortUrlServer) || IShortUrlServer.C1N return url
switch (server) { }
case IShortUrlServer.C1N: }
return generateC1NShortUrl(url)
case IShortUrlServer.YOURLS: export const isUrl = (url: string): boolean => {
return generateYOURLSShortUrl(url) try {
case IShortUrlServer.CFWORKER: return Boolean(new URL(url))
return generateCFWORKERShortUrl(url) } catch {
case IShortUrlServer.SINK: return false
return generateSinkShortUrl(url) }
default: }
return url
} export const isUrlEncode = (url: string): boolean => {
} url = url || ''
try {
export const isUrl = (url: string): boolean => { return url !== decodeURI(url)
try { } catch {
return Boolean(new URL(url)) return false
} catch { }
return false }
}
} export const handleUrlEncode = (url: string): string => (isUrlEncode(url) ? url : encodeURI(url))
export const isUrlEncode = (url: string): boolean => { export const handleUrlEncodeWithSetting = (url: string) =>
url = url || '' db.get(configPaths.settings.encodeOutputURL) ? handleUrlEncode(url) : url
try {
return url !== decodeURI(url) export const handleStreamlinePluginName = (name: string) => name.replace(/(@[^/]+\/)?picgo-plugin-/, '')
} catch { export const simpleClone = (obj: any) => JSON.parse(JSON.stringify(obj))
return false export const enforceNumber = (num: number | string) => (isNaN(+num) ? 0 : +num)
}
} export const trimValues = <T extends IStringKeyMap>(
obj: T
export const handleUrlEncode = (url: string): string => (isUrlEncode(url) ? url : encodeURI(url)) ): { [K in keyof T]: T[K] extends string ? string : T[K] } => {
return Object.fromEntries(
export const handleUrlEncodeWithSetting = (url: string) => Object.entries(obj).map(([key, value]) => [key, typeof value === 'string' ? value.trim() : value])
db.get(configPaths.settings.encodeOutputURL) ? handleUrlEncode(url) : url ) as { [K in keyof T]: T[K] extends string ? string : T[K] }
}
export const handleStreamlinePluginName = (name: string) => name.replace(/(@[^/]+\/)?picgo-plugin-/, '')
export const simpleClone = (obj: any) => JSON.parse(JSON.stringify(obj)) export const formatEndpoint = (endpoint: string, sslEnabled: boolean): string => {
export const enforceNumber = (num: number | string) => (isNaN(+num) ? 0 : +num) const hasProtocol = /^https?:\/\//.test(endpoint)
if (!hasProtocol) {
export const trimValues = <T extends IStringKeyMap>( return `${sslEnabled ? 'https' : 'http'}://${endpoint}`
obj: T }
): { [K in keyof T]: T[K] extends string ? string : T[K] } => { return sslEnabled ? endpoint.replace(/^http:\/\//, 'https://') : endpoint.replace(/^https:\/\//, 'http://')
return Object.fromEntries( }
Object.entries(obj).map(([key, value]) => [key, typeof value === 'string' ? value.trim() : value])
) as { [K in keyof T]: T[K] extends string ? string : T[K] } export const formatHttpProxy = (
} proxy: string | undefined,
type: 'object' | 'string'
export const formatEndpoint = (endpoint: string, sslEnabled: boolean): string => { ): IHTTPProxy | undefined | string => {
const hasProtocol = /^https?:\/\//.test(endpoint) if (!proxy) return undefined
if (!hasProtocol) { if (/^https?:\/\//.test(proxy)) {
return `${sslEnabled ? 'https' : 'http'}://${endpoint}` const { protocol, hostname, port } = new URL(proxy)
} return type === 'string'
return sslEnabled ? endpoint.replace(/^http:\/\//, 'https://') : endpoint.replace(/^https:\/\//, 'http://') ? `${protocol}//${hostname}:${port}`
} : {
host: hostname,
export const formatHttpProxy = ( port: Number(port),
proxy: string | undefined, protocol: protocol.slice(0, -1)
type: 'object' | 'string' }
): IHTTPProxy | undefined | string => { }
if (!proxy) return undefined const [host, port] = proxy.split(':')
if (/^https?:\/\//.test(proxy)) { return type === 'string'
const { protocol, hostname, port } = new URL(proxy) ? `http://${host}:${port}`
return type === 'string' : {
? `${protocol}//${hostname}:${port}` host,
: { port: port ? Number(port) : 80,
host: hostname, protocol: 'http'
port: Number(port), }
protocol: protocol.slice(0, -1) }
}
} export function encodeFilePath (filePath: string) {
const [host, port] = proxy.split(':') return filePath.replace(/\\/g, '/').split('/').map(encodeURIComponent).join('/')
return type === 'string' }
? `http://${host}:${port}`
: { export const trimPath = (path: string) => path.replace(/^\/+|\/+$/g, '').replace(/\/+/g, '/')
host,
port: port ? Number(port) : 80,
protocol: 'http'
}
}
export function encodeFilePath (filePath: string) {
return filePath.replace(/\\/g, '/').split('/').map(encodeURIComponent).join('/')
}
export const trimPath = (path: string) => path.replace(/^\/+|\/+$/g, '').replace(/\/+/g, '/')
+263 -262
View File
@@ -1,262 +1,263 @@
import crypto from 'node:crypto' import crypto from 'node:crypto'
import http, { AgentOptions } from 'node:http' import http, { AgentOptions } from 'node:http'
import https from 'node:https' import https from 'node:https'
import path from 'node:path' import path from 'node:path'
import querystring from 'node:querystring' import querystring from 'node:querystring'
import { DeleteObjectCommand, S3Client, S3ClientConfig } from '@aws-sdk/client-s3' import type { S3ClientConfig } from '@aws-sdk/client-s3'
import { NodeHttpHandler } from '@smithy/node-http-handler' import { DeleteObjectCommand, S3Client } from '@aws-sdk/client-s3'
import axios from 'axios' import { NodeHttpHandler } from '@smithy/node-http-handler'
import { ISftpPlistConfig } from 'piclist' import axios from 'axios'
import type { ISftpPlistConfig } from 'piclist'
import type { IObj, IStringKeyMap } from '#/types/types'
import { getAgent } from '~/manage/utils/common' import type { IObj, IStringKeyMap } from '#/types/types'
import SSHClient from '~/utils/sshClient' import { getAgent } from '~/manage/utils/common'
import SSHClient from '~/utils/sshClient'
interface DogecloudTokenFull {
Credentials: { interface DogecloudTokenFull {
accessKeyId: string Credentials: {
secretAccessKey: string accessKeyId: string
sessionToken: string secretAccessKey: string
} sessionToken: string
ExpiredAt: number }
Buckets: { ExpiredAt: number
name: string Buckets: {
s3Bucket: string name: string
s3Endpoint: string s3Bucket: string
}[] s3Endpoint: string
} }[]
}
const dogeRegionMap: IStringKeyMap = {
'ap-shanghai': '0', const dogeRegionMap: IStringKeyMap = {
'ap-beijing': '1', 'ap-shanghai': '0',
'ap-guangzhou': '2', 'ap-beijing': '1',
'ap-chengdu': '3' 'ap-guangzhou': '2',
} 'ap-chengdu': '3'
}
async function dogecloudApi (
apiPath: string, async function dogecloudApi (
data = {}, apiPath: string,
jsonMode: boolean = false, data = {},
accessKey: string, jsonMode: boolean = false,
secretKey: string accessKey: string,
) { secretKey: string
const body = jsonMode ? JSON.stringify(data) : querystring.encode(data) ) {
const sign = crypto const body = jsonMode ? JSON.stringify(data) : querystring.encode(data)
.createHmac('sha1', secretKey) const sign = crypto
.update(Buffer.from(apiPath + '\n' + body, 'utf8')) .createHmac('sha1', secretKey)
.digest('hex') .update(Buffer.from(apiPath + '\n' + body, 'utf8'))
const authorization = `TOKEN ${accessKey}:${sign}` .digest('hex')
try { const authorization = `TOKEN ${accessKey}:${sign}`
const res = await axios.request({ try {
url: `https://api.dogecloud.com${apiPath}`, const res = await axios.request({
method: 'POST', url: `https://api.dogecloud.com${apiPath}`,
data: body, method: 'POST',
responseType: 'json', data: body,
headers: { responseType: 'json',
'Content-Type': jsonMode ? 'application/json' : 'application/x-www-form-urlencoded', headers: {
Authorization: authorization 'Content-Type': jsonMode ? 'application/json' : 'application/x-www-form-urlencoded',
} Authorization: authorization
}) }
if (res.data.code !== 200) { })
throw new Error('API Error') if (res.data.code !== 200) {
} throw new Error('API Error')
return res.data.data }
} catch (err: any) { return res.data.data
throw new Error('API Error') } catch (err: any) {
} throw new Error('API Error')
} }
}
async function getDogeToken (accessKey: string, secretKey: string): Promise<IObj | DogecloudTokenFull> {
try { async function getDogeToken (accessKey: string, secretKey: string): Promise<IObj | DogecloudTokenFull> {
const data = await dogecloudApi( try {
'/auth/tmp_token.json', const data = await dogecloudApi(
{ '/auth/tmp_token.json',
channel: 'OSS_FULL', {
scopes: ['*'] channel: 'OSS_FULL',
}, scopes: ['*']
true, },
accessKey, true,
secretKey accessKey,
) secretKey
return data )
} catch (err: any) { return data
console.log(err) } catch (err: any) {
return {} console.log(err)
} return {}
} }
}
export async function removeFileFromS3InMain (configMap: IStringKeyMap, dogeMode: boolean = false) {
try { export async function removeFileFromS3InMain (configMap: IStringKeyMap, dogeMode: boolean = false) {
const { try {
url: rawUrl, const {
type, url: rawUrl,
config: { accessKeyID, secretAccessKey, bucketName, endpoint, pathStyleAccess, rejectUnauthorized, proxy } type,
} = configMap config: { accessKeyID, secretAccessKey, bucketName, endpoint, pathStyleAccess, rejectUnauthorized, proxy }
let { } = configMap
imgUrl, let {
config: { region } imgUrl,
} = configMap config: { region }
if (type === 'aws-s3' || type === 'aws-s3-plist') { } = configMap
imgUrl = rawUrl || imgUrl || '' if (type === 'aws-s3' || type === 'aws-s3-plist') {
} imgUrl = rawUrl || imgUrl || ''
const url = new URL(!/^https?:\/\//.test(imgUrl) ? `http://${imgUrl}` : imgUrl) }
let fileKey = url.pathname.replace(/^\/+/, '') const url = new URL(!/^https?:\/\//.test(imgUrl) ? `http://${imgUrl}` : imgUrl)
if (pathStyleAccess) { let fileKey = url.pathname.replace(/^\/+/, '')
fileKey = fileKey.replace(/^[^/]+\//, '') if (pathStyleAccess) {
} fileKey = fileKey.replace(/^[^/]+\//, '')
const endpointUrl: string | undefined = endpoint }
? /^https?:\/\//.test(endpoint) const endpointUrl: string | undefined = endpoint
? endpoint ? /^https?:\/\//.test(endpoint)
: `http://${endpoint}` ? endpoint
: undefined : `http://${endpoint}`
if (endpointUrl && endpointUrl.includes('cloudflarestorage')) { : undefined
region = region || 'auto' if (endpointUrl && endpointUrl.includes('cloudflarestorage')) {
} region = region || 'auto'
const sslEnabled = endpointUrl ? endpointUrl.startsWith('https') : true }
const agent = getAgent(proxy, sslEnabled) const sslEnabled = endpointUrl ? endpointUrl.startsWith('https') : true
const commonOptions: AgentOptions = { const agent = getAgent(proxy, sslEnabled)
keepAlive: true, const commonOptions: AgentOptions = {
keepAliveMsecs: 1000, keepAlive: true,
scheduling: 'lifo' as 'lifo' | 'fifo' | undefined keepAliveMsecs: 1000,
} scheduling: 'lifo' as 'lifo' | 'fifo' | undefined
const extraOptions = sslEnabled ? { rejectUnauthorized: !!rejectUnauthorized } : {} }
const handler = sslEnabled const extraOptions = sslEnabled ? { rejectUnauthorized: !!rejectUnauthorized } : {}
? new NodeHttpHandler({ const handler = sslEnabled
httpsAgent: agent.https ? new NodeHttpHandler({
? agent.https httpsAgent: agent.https
: new https.Agent({ ? agent.https
...commonOptions, : new https.Agent({
...extraOptions ...commonOptions,
}) ...extraOptions
}) })
: new NodeHttpHandler({ })
httpAgent: agent.http : new NodeHttpHandler({
? agent.http httpAgent: agent.http
: new http.Agent({ ? agent.http
...commonOptions, : new http.Agent({
...extraOptions ...commonOptions,
}) ...extraOptions
}) })
const s3Options: S3ClientConfig = { })
credentials: { const s3Options: S3ClientConfig = {
accessKeyId: accessKeyID, credentials: {
secretAccessKey accessKeyId: accessKeyID,
}, secretAccessKey
endpoint: endpointUrl, },
tls: sslEnabled, endpoint: endpointUrl,
forcePathStyle: pathStyleAccess, tls: sslEnabled,
region, forcePathStyle: pathStyleAccess,
requestHandler: handler region,
} requestHandler: handler
if (dogeMode) { }
s3Options.credentials = { if (dogeMode) {
accessKeyId: configMap.config.accessKeyID, s3Options.credentials = {
secretAccessKey: configMap.config.secretAccessKey, accessKeyId: configMap.config.accessKeyID,
sessionToken: configMap.config.sessionToken secretAccessKey: configMap.config.secretAccessKey,
} sessionToken: configMap.config.sessionToken
} }
let result: any }
try { let result: any
fileKey = decodeURIComponent(fileKey) try {
} catch (err: any) {} fileKey = decodeURIComponent(fileKey)
try { } catch (err: any) {}
const client = new S3Client(s3Options) try {
const command = new DeleteObjectCommand({ const client = new S3Client(s3Options)
Bucket: bucketName, const command = new DeleteObjectCommand({
Key: fileKey Bucket: bucketName,
}) Key: fileKey
result = await client.send(command) })
} catch (err: any) { result = await client.send(command)
s3Options.region = 'us-east-1' } catch (err: any) {
const client = new S3Client(s3Options) s3Options.region = 'us-east-1'
const command = new DeleteObjectCommand({ const client = new S3Client(s3Options)
Bucket: bucketName, const command = new DeleteObjectCommand({
Key: fileKey Bucket: bucketName,
}) Key: fileKey
result = await client.send(command) })
} result = await client.send(command)
return result.$metadata.httpStatusCode === 204 }
} catch (err: any) { return result.$metadata.httpStatusCode === 204
console.log(err) } catch (err: any) {
return false console.log(err)
} return false
} }
}
export async function removeFileFromDogeInMain (configMap: IStringKeyMap) {
try { export async function removeFileFromDogeInMain (configMap: IStringKeyMap) {
const { try {
config: { bucketName, AccessKey, SecretKey } const {
} = configMap config: { bucketName, AccessKey, SecretKey }
const token = (await getDogeToken(AccessKey, SecretKey)) as DogecloudTokenFull } = configMap
const bucket = token.Buckets?.find(item => item.name === bucketName || item.s3Bucket === bucketName) const token = (await getDogeToken(AccessKey, SecretKey)) as DogecloudTokenFull
const newConfigMap = { ...configMap } const bucket = token.Buckets?.find(item => item.name === bucketName || item.s3Bucket === bucketName)
newConfigMap.config = { const newConfigMap = { ...configMap }
...newConfigMap.config, newConfigMap.config = {
accessKeyID: token.Credentials?.accessKeyId, ...newConfigMap.config,
secretAccessKey: token.Credentials?.secretAccessKey, accessKeyID: token.Credentials?.accessKeyId,
sessionToken: token.Credentials?.sessionToken, secretAccessKey: token.Credentials?.secretAccessKey,
endpoint: bucket?.s3Endpoint, sessionToken: token.Credentials?.sessionToken,
region: dogeRegionMap[bucket?.s3Endpoint?.split('.')[1] || 'ap-shanghai'], endpoint: bucket?.s3Endpoint,
bucketName: bucket?.s3Bucket region: dogeRegionMap[bucket?.s3Endpoint?.split('.')[1] || 'ap-shanghai'],
} bucketName: bucket?.s3Bucket
return await removeFileFromS3InMain(newConfigMap, true) }
} catch (err: any) { return await removeFileFromS3InMain(newConfigMap, true)
console.log(err) } catch (err: any) {
return false console.log(err)
} return false
} }
}
function createHuaweiAuthorization (
bucketName: string, function createHuaweiAuthorization (
path: string, bucketName: string,
fileName: string, path: string,
accessKey: string, fileName: string,
secretKey: string, accessKey: string,
date: string = new Date().toUTCString() secretKey: string,
) { date: string = new Date().toUTCString()
const strToSign = `DELETE\n\n\n${date}\n/${bucketName}${path}/${fileName}` ) {
const singature = crypto.createHmac('sha1', secretKey).update(strToSign).digest('base64') const strToSign = `DELETE\n\n\n${date}\n/${bucketName}${path}/${fileName}`
return `OBS ${accessKey}:${singature}` const singature = crypto.createHmac('sha1', secretKey).update(strToSign).digest('base64')
} return `OBS ${accessKey}:${singature}`
}
export async function removeFileFromHuaweiInMain (configMap: IStringKeyMap) {
const { fileName, config } = configMap export async function removeFileFromHuaweiInMain (configMap: IStringKeyMap) {
const { accessKeyId, accessKeySecret, bucketName, endpoint } = config const { fileName, config } = configMap
let path = config.path || '/' const { accessKeyId, accessKeySecret, bucketName, endpoint } = config
path = `/${path.replace(/^\/+|\/+$/, '')}` let path = config.path || '/'
path = path === '/' ? '' : path path = `/${path.replace(/^\/+|\/+$/, '')}`
const date = new Date().toUTCString() path = path === '/' ? '' : path
const authorization = createHuaweiAuthorization(bucketName, path, fileName, accessKeyId, accessKeySecret, date) const date = new Date().toUTCString()
try { const authorization = createHuaweiAuthorization(bucketName, path, fileName, accessKeyId, accessKeySecret, date)
const res = await axios.request({ try {
url: `https://${bucketName}.${endpoint}${encodeURI(path)}/${encodeURIComponent(fileName)}`, const res = await axios.request({
method: 'DELETE', url: `https://${bucketName}.${endpoint}${encodeURI(path)}/${encodeURIComponent(fileName)}`,
responseType: 'json', method: 'DELETE',
headers: { responseType: 'json',
Host: `${bucketName}.${endpoint}`, headers: {
Date: date, Host: `${bucketName}.${endpoint}`,
Authorization: authorization Date: date,
} Authorization: authorization
}) }
return res.status === 204 })
} catch (error) { return res.status === 204
console.log(error) } catch (error) {
return false console.log(error)
} return false
} }
}
export async function removeFileFromSFTPInMain (config: ISftpPlistConfig, fileName: string) {
try { export async function removeFileFromSFTPInMain (config: ISftpPlistConfig, fileName: string) {
const client = SSHClient.instance try {
await client.connect(config) const client = SSHClient.instance
const uploadPath = `/${config.uploadPath || ''}/`.replace(/\/+/g, '/') await client.connect(config)
const remote = path.join(uploadPath, fileName) const uploadPath = `/${config.uploadPath || ''}/`.replace(/\/+/g, '/')
const deleteResult = await client.deleteFileSFTP(config, remote) const remote = path.join(uploadPath, fileName)
client.close() const deleteResult = await client.deleteFileSFTP(config, remote)
return deleteResult client.close()
} catch (err: any) { return deleteResult
console.log(err) } catch (err: any) {
return false console.log(err)
} return false
} }
}
+66 -65
View File
@@ -1,65 +1,66 @@
<template> <template>
<div <div
id="app" id="app"
:key="pageReloadCount" :key="pageReloadCount"
> >
<router-view /> <router-view />
<UIServiceProvider /> <UIServiceProvider />
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import type { IConfig } from 'piclist' import type { IConfig } from 'piclist'
import { onBeforeMount, onMounted } from 'vue' import { onBeforeMount, onMounted } from 'vue'
import UIServiceProvider from '@/components/ui/UIServiceProvider.vue' import UIServiceProvider from '@/components/ui/UIServiceProvider.vue'
import { useATagClick } from '@/hooks/useATagClick' import { useATagClick } from '@/hooks/useATagClick'
import { useStore } from '@/hooks/useStore' import { useStore } from '@/hooks/useStore'
import { getConfig } from '@/utils/dataSender' import { getConfig } from '@/utils/dataSender'
import { pageReloadCount } from '@/utils/global' import { pageReloadCount } from '@/utils/global'
import { useAppStore } from './hooks/useAppStore' import { useAppStore } from './hooks/useAppStore'
useATagClick()
useATagClick()
const store = useStore()
const appStore = useAppStore() const store = useStore()
const appStore = useAppStore()
onBeforeMount(async () => {
const config = await getConfig<IConfig>() onBeforeMount(async () => {
if (config) { const config = await getConfig<IConfig>()
store?.setDefaultPicBed(config?.picBed?.uploader || config?.picBed?.current || 'smms') if (config) {
} store?.setDefaultPicBed(config?.picBed?.uploader || config?.picBed?.current || 'smms')
}) }
})
onMounted(async () => {
try { onMounted(async () => {
appStore.init() try {
} catch (error) { appStore.init()
console.error('Failed to load settings:', error) } catch (error) {
} console.error('Failed to load settings:', error)
}) }
})
</script>
</script>
<script lang="ts">
export default { <script lang="ts">
name: 'PicGoApp' export default {
} name: 'PicGoApp'
</script> }
</script>
<style lang="stylus">
body, <style lang="stylus">
html body,
padding 0 html
margin 0 padding 0
height 100% margin 0
font-family "Helvetica Neue",Helvetica,"PingFang SC","Hiragino Sans GB","Microsoft YaHei","微软雅黑",Arial,sans-serif height 100%
#app font-family "Helvetica Neue",Helvetica,"PingFang SC","Hiragino Sans GB","Microsoft YaHei","微软雅黑",Arial,sans-serif
height 100% #app
user-select none height 100%
overflow hidden user-select none
.el-button-group overflow hidden
width 100% .el-button-group
.el-button width 100%
width 50% .el-button
</style> width 50%
</style>
+24 -23
View File
@@ -1,23 +1,24 @@
import { onMounted, onUnmounted } from 'vue' import { onMounted, onUnmounted } from 'vue'
import { IRPCActionType } from '@/utils/enum' import { IRPCActionType } from '@/utils/enum'
export function useATagClick () { export function useATagClick () {
const handleATagClick = (e: MouseEvent) => { const handleATagClick = (e: MouseEvent) => {
if (e.target instanceof HTMLAnchorElement) { if (e.target instanceof HTMLAnchorElement) {
if (e.target.href) { if (e.target.href) {
// avoid opening localhost development URLs in external browser if (!e.target.href.startsWith('http://localhost:3000')) {
if (!e.target.href.startsWith('http://localhost:3000')) { e.preventDefault()
e.preventDefault() window.electron.sendRPC(IRPCActionType.OPEN_URL, e.target.href)
window.electron.sendRPC(IRPCActionType.OPEN_URL, e.target.href) }
} }
} }
} }
}
onMounted(() => { onMounted(() => {
document.addEventListener('click', handleATagClick) document.addEventListener('click', handleATagClick)
}) })
onUnmounted(() => {
document.removeEventListener('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
+55 -59
View File
@@ -1,59 +1,55 @@
import 'video.js/dist/video-js.css' import 'video.js/dist/video-js.css'
import 'highlight.js/styles/stackoverflow-light.css' import 'highlight.js/styles/stackoverflow-light.css'
import 'highlight.js/lib/common' import 'highlight.js/lib/common'
import hljsVuePlugin from '@highlightjs/vue-plugin' import hljsVuePlugin from '@highlightjs/vue-plugin'
import VueVideoPlayer from '@videojs-player/vue' import VueVideoPlayer from '@videojs-player/vue'
import { createPinia } from 'pinia' import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate' import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
import { createApp } from 'vue' import { createApp } from 'vue'
import { createI18n } from 'vue-i18n' import { createI18n } from 'vue-i18n'
import VueLazyLoad from 'vue3-lazyload' import VueLazyLoad from 'vue3-lazyload'
import App from '@/App.vue' import App from '@/App.vue'
import en from '@/i18n/locales/en.json' import en from '@/i18n/locales/en.json'
import zhCN from '@/i18n/locales/zh-CN.json' import zhCN from '@/i18n/locales/zh-CN.json'
import zhTW from '@/i18n/locales/zh-TW.json' import zhTW from '@/i18n/locales/zh-TW.json'
import router from '@/router' import router from '@/router'
import { store } from '@/store' import { store } from '@/store'
import db from '@/utils/db' import db from '@/utils/db'
type MessageSchema = typeof zhCN type MessageSchema = typeof zhCN
window.electron.setVisualZoomLevelLimits(1, 1) window.electron.setVisualZoomLevelLimits(1, 1)
const app = createApp(App) const app = createApp(App)
app.config.globalProperties.$$db = db app.config.globalProperties.$$db = db
app.config.globalProperties.triggerRPC = window.electron.triggerRPC app.config.globalProperties.triggerRPC = window.electron.triggerRPC
app.config.globalProperties.sendRPC = window.electron.sendRPC app.config.globalProperties.sendRPC = window.electron.sendRPC
app.config.globalProperties.sendToMain = window.electron.sendToMain app.config.globalProperties.sendToMain = window.electron.sendToMain
const i18n = createI18n<[MessageSchema], 'en' | 'zh-CN' | 'zh-TW'>({ const i18n = createI18n<[MessageSchema], 'en' | 'zh-CN' | 'zh-TW'>({
legacy: false, legacy: false,
locale: localStorage.getItem('currentLanguage') || 'zh-CN', locale: localStorage.getItem('currentLanguage') || 'zh-CN',
fallbackLocale: 'zh-CN', fallbackLocale: 'zh-CN',
messages: { messages: {
en, en,
'zh-CN': zhCN, 'zh-CN': zhCN,
'zh-TW': zhTW 'zh-TW': zhTW
} }
}) })
const pinia = createPinia() const pinia = createPinia()
pinia.use(piniaPluginPersistedstate) pinia.use(piniaPluginPersistedstate)
app.use(VueLazyLoad, { app.use(VueLazyLoad, {
loading: '/loading.jpg', loading: '/loading.jpg',
error: '/unknown-file-type.svg', error: '/unknown-file-type.svg',
delay: 500 delay: 500
}) })
app.use(i18n) app.use(i18n)
app.use(router) app.use(router)
app.use(store) app.use(store)
app.use(pinia) app.use(pinia)
app.use(hljsVuePlugin) app.use(hljsVuePlugin)
app.use(VueVideoPlayer) app.use(VueVideoPlayer)
app.mount('#app') app.mount('#app')
export {
i18n
}
+445 -444
View File
@@ -1,444 +1,445 @@
<template> <template>
<div class="upload-container"> <div class="upload-container">
<!-- Header Card --> <!-- Header Card -->
<div class="upload-card header-card"> <div class="upload-card header-card">
<div class="card-header"> <div class="card-header">
<div class="provider-section"> <div class="provider-section">
<button <button
class="provider-button" class="provider-button"
:title="t('pages.upload.uploadViewHint')" :title="t('pages.upload.uploadViewHint')"
@click="handlePicBedNameClick(picBedName, picBedConfigName)" @click="handlePicBedNameClick(picBedName, picBedConfigName)"
> >
<div class="provider-info"> <div class="provider-info">
<span class="provider-name">{{ picBedName }}</span> <span class="provider-name">{{ picBedName }}</span>
<span class="provider-config">{{ picBedConfigName || 'Default' }}</span> <span class="provider-config">{{ picBedConfigName || 'Default' }}</span>
</div> </div>
<ChevronDownIcon <ChevronDownIcon
:size="16" :size="16"
class="provider-arrow" class="provider-arrow"
/> />
</button> </button>
</div> </div>
<div class="header-actions"> <div class="header-actions">
<button <button
class="action-button secondary" class="action-button secondary"
@click="handleImageProcess" @click="handleImageProcess"
> >
<Settings :size="16" /> <Settings :size="16" />
<span>{{ t('pages.upload.imageProcessName') }}</span> <span>{{ t('pages.upload.imageProcessName') }}</span>
</button> </button>
<button <button
class="action-button" class="action-button"
@click="handleChangePicBed" @click="handleChangePicBed"
> >
<DatabaseIcon :size="16" /> <DatabaseIcon :size="16" />
<span>{{ t('pages.upload.changePicBed') }}</span> <span>{{ t('pages.upload.changePicBed') }}</span>
</button> </button>
</div> </div>
</div> </div>
</div> </div>
<!-- Main Upload Card --> <!-- Main Upload Card -->
<div class="upload-card main-card"> <div class="upload-card main-card">
<div <div
class="upload-zone" id="upload-area"
:class="{ 'drag-active': dragover }" class="upload-zone"
@drop.prevent="onDrop" :class="{ 'drag-active': dragover }"
@dragover.prevent="dragover = true" @drop.prevent="onDrop"
@dragleave.prevent="dragover = false" @dragover.prevent="dragover = true"
@click="openUplodWindow" @dragleave.prevent="dragover = false"
> @click="openUplodWindow"
<div class="upload-content"> >
<div class="upload-icon"> <div class="upload-content">
<UploadCloudIcon :size="48" /> <div class="upload-icon">
</div> <UploadCloudIcon :size="48" />
<div class="upload-text"> </div>
<h3 class="upload-title"> <div class="upload-text">
{{ t('pages.upload.dragFileToHere') }} <h3 class="upload-title">
</h3> {{ t('pages.upload.dragFileToHere') }}
<p class="upload-subtitle"> </h3>
{{ t('pages.upload.clickToUpload') }} <p class="upload-subtitle">
</p> {{ t('pages.upload.clickToUpload') }}
<div class="upload-formats"> </p>
<span class="format-label">{{ t('pages.upload.uploadHint') }}</span> <div class="upload-formats">
</div> <span class="format-label">{{ t('pages.upload.uploadHint') }}</span>
</div> </div>
</div> </div>
<input </div>
id="file-uploader" <input
ref="fileInput" id="file-uploader"
type="file" ref="fileInput"
multiple type="file"
style="display: none" multiple
@change="onChange" style="display: none"
> @change="onChange"
</div> >
</div>
<!-- Progress Bar -->
<transition name="progress"> <!-- Progress Bar -->
<div <transition name="progress">
v-if="showProgress" <div
class="progress-container" v-if="showProgress"
> class="progress-container"
<div class="progress-bar"> >
<div <div class="progress-bar">
class="progress-fill" <div
:class="{ 'progress-error': showError }" class="progress-fill"
:style="{ width: `${progress}%` }" :class="{ 'progress-error': showError }"
/> :style="{ width: `${progress}%` }"
</div> />
<span class="progress-text"> </div>
{{ showError ? t('pages.upload.uploadFailed') : `${progress}%` }} <span class="progress-text">
</span> {{ showError ? t('pages.upload.uploadFailed') : `${progress}%` }}
</div> </span>
</transition> </div>
</div> </transition>
</div>
<!-- Quick Actions Card -->
<div class="upload-card actions-card"> <!-- Quick Actions Card -->
<div class="card-header"> <div class="upload-card actions-card">
<h4 class="card-title"> <div class="card-header">
{{ t('pages.upload.quickUpload') }} <h4 class="card-title">
</h4> {{ t('pages.upload.quickUpload') }}
</div> </h4>
<div class="quick-actions"> </div>
<button <div class="quick-actions">
class="quick-action-button" <button
@click="uploadClipboardFiles" class="quick-action-button"
> @click="uploadClipboardFiles"
<ClipboardIcon :size="20" /> >
<span>{{ t('pages.upload.clipboardPicture') }}</span> <ClipboardIcon :size="20" />
</button> <span>{{ t('pages.upload.clipboardPicture') }}</span>
<button </button>
class="quick-action-button" <button
@click="uploadURLFiles" class="quick-action-button"
> @click="uploadURLFiles"
<LinkIcon :size="20" /> >
<span>{{ t('pages.upload.urlUpload') }}</span> <LinkIcon :size="20" />
</button> <span>{{ t('pages.upload.urlUpload') }}</span>
</div> </button>
</div> </div>
</div>
<!-- Settings Card -->
<div class="upload-card settings-card"> <!-- Settings Card -->
<div class="card-header"> <div class="upload-card settings-card">
<h4 class="card-title"> <div class="card-header">
{{ t('pages.upload.linkFormat') }} <h4 class="card-title">
</h4> {{ t('pages.upload.linkFormat') }}
</div> </h4>
<div class="settings-content"> </div>
<!-- Format Options --> <div class="settings-content">
<div class="setting-group"> <!-- Format Options -->
<label class="setting-label">{{ t('pages.upload.outputFormat') }}</label> <div class="setting-group">
<div class="format-buttons"> <label class="setting-label">{{ t('pages.upload.outputFormat') }}</label>
<button <div class="format-buttons">
v-for="(format, key) in pasteFormatList" <button
:key="key" v-for="(format, key) in pasteFormatList"
class="format-button" :key="key"
:class="{ active: pasteStyle === key }" class="format-button"
:title="format" :class="{ active: pasteStyle === key }"
@click="updatePasteStyle(key)" :title="format"
> @click="updatePasteStyle(key)"
{{ key }} >
</button> {{ key }}
</div> </button>
</div> </div>
</div>
<!-- URL Length Options -->
<div class="setting-group"> <!-- URL Length Options -->
<label class="setting-label">{{ t('pages.upload.urlType.title') }}</label> <div class="setting-group">
<div class="url-toggle"> <label class="setting-label">{{ t('pages.upload.urlType.title') }}</label>
<button <div class="url-toggle">
class="toggle-button" <button
:class="{ active: !useShortUrl }" class="toggle-button"
@click="updateUrlType(false)" :class="{ active: !useShortUrl }"
> @click="updateUrlType(false)"
<span>{{ t('pages.upload.urlType.normal') }}</span> >
</button> <span>{{ t('pages.upload.urlType.normal') }}</span>
<button </button>
class="toggle-button" <button
:class="{ active: useShortUrl }" class="toggle-button"
@click="updateUrlType(true)" :class="{ active: useShortUrl }"
> @click="updateUrlType(true)"
<span>{{ t('pages.upload.urlType.short') }}</span> >
</button> <span>{{ t('pages.upload.urlType.short') }}</span>
</div> </button>
</div> </div>
</div> </div>
</div> </div>
</div>
<!-- Image Process Dialog -->
<transition name="modal"> <!-- Image Process Dialog -->
<div <transition name="modal">
v-if="imageProcessDialogVisible" <div
class="modal-overlay" v-if="imageProcessDialogVisible"
@click="imageProcessDialogVisible = false" class="modal-overlay"
> @click="imageProcessDialogVisible = false"
<div >
class="modal-container" <div
@click.stop class="modal-container"
> @click.stop
<div class="modal-header"> >
<h3 class="modal-title"> <div class="modal-header">
{{ t('pages.imageProcess.title') }} <h3 class="modal-title">
</h3> {{ t('pages.imageProcess.title') }}
<button </h3>
class="modal-close" <button
@click="imageProcessDialogVisible = false" class="modal-close"
> @click="imageProcessDialogVisible = false"
<XIcon :size="20" /> >
</button> <XIcon :size="20" />
</div> </button>
<div class="modal-content"> </div>
<ImageProcessSetting v-model="imageProcessDialogVisible" /> <div class="modal-content">
</div> <ImageProcessSetting v-model="imageProcessDialogVisible" />
</div> </div>
</div> </div>
</transition> </div>
</div> </transition>
</template> </div>
</template>
<script lang="ts" setup>
import type { IpcRendererEvent } from 'electron' <script lang="ts" setup>
import { ChevronDownIcon, ClipboardIcon, DatabaseIcon, LinkIcon, Settings, UploadCloudIcon, XIcon } from 'lucide-vue-next' import type { IpcRendererEvent } from 'electron'
import { onBeforeMount, onBeforeUnmount, ref, watch } from 'vue' import { ChevronDownIcon, ClipboardIcon, DatabaseIcon, LinkIcon, Settings, UploadCloudIcon, XIcon } from 'lucide-vue-next'
import { useI18n } from 'vue-i18n' import { onBeforeMount, onBeforeUnmount, ref, watch } from 'vue'
import { useRouter } from 'vue-router' import { useI18n } from 'vue-i18n'
import { useRouter } from 'vue-router'
import ImageProcessSetting from '@/components/ImageProcessSetting.vue'
import useMessage from '@/hooks/useMessage' import ImageProcessSetting from '@/components/ImageProcessSetting.vue'
import { PICBEDS_PAGE } from '@/router/config' import useMessage from '@/hooks/useMessage'
import $bus from '@/utils/bus' import { PICBEDS_PAGE } from '@/router/config'
import { isUrl } from '@/utils/common' import $bus from '@/utils/bus'
import { configPaths } from '@/utils/configPaths' import { isUrl } from '@/utils/common'
import { SHOW_INPUT_BOX, SHOW_INPUT_BOX_RESPONSE } from '@/utils/constant' import { configPaths } from '@/utils/configPaths'
import { getConfig, saveConfig } from '@/utils/dataSender' import { SHOW_INPUT_BOX, SHOW_INPUT_BOX_RESPONSE } from '@/utils/constant'
import { useDragEventListeners } from '@/utils/drag' import { getConfig, saveConfig } from '@/utils/dataSender'
import { IPasteStyle, IRPCActionType } from '@/utils/enum' import { useDragEventListeners } from '@/utils/drag'
import { picBedGlobal, updatePicBedGlobal } from '@/utils/global' import { IPasteStyle, IRPCActionType } from '@/utils/enum'
import type { IFileWithPath, IUploaderConfigItem } from '#/types/types' import { picBedGlobal, updatePicBedGlobal } from '@/utils/global'
import type { IFileWithPath, IUploaderConfigItem } from '#/types/types'
useDragEventListeners()
const $router = useRouter() useDragEventListeners()
const { t } = useI18n() const $router = useRouter()
const message = useMessage() const { t } = useI18n()
const message = useMessage()
const imageProcessDialogVisible = ref(false)
const useShortUrl = ref(false) const imageProcessDialogVisible = ref(false)
const dragover = ref(false) const useShortUrl = ref(false)
const progress = ref(0) const dragover = ref(false)
const showProgress = ref(false) const progress = ref(0)
const showError = ref(false) const showProgress = ref(false)
const pasteStyle = ref('') const showError = ref(false)
const picBedName = ref('') const pasteStyle = ref('')
const picBedConfigName = ref('') const picBedName = ref('')
const fileInput = ref<HTMLInputElement>() const picBedConfigName = ref('')
const fileInput = ref<HTMLInputElement>()
const pasteFormatList = ref<Record<string, string>>({
[IPasteStyle.MARKDOWN]: '![alt](url)', const pasteFormatList = ref<Record<string, string>>({
[IPasteStyle.HTML]: '<img src="url"/>', [IPasteStyle.MARKDOWN]: '![alt](url)',
[IPasteStyle.URL]: 'http://test.com/test.png', [IPasteStyle.HTML]: '<img src="url"/>',
[IPasteStyle.UBB]: '[img]url[/img]', [IPasteStyle.URL]: 'http://test.com/test.png',
[IPasteStyle.CUSTOM]: '' [IPasteStyle.UBB]: '[img]url[/img]',
}) [IPasteStyle.CUSTOM]: ''
})
watch(picBedGlobal, () => {
getDefaultPicBed() watch(picBedGlobal, () => {
}) getDefaultPicBed()
})
const uploadProgressHandler = (_event: IpcRendererEvent, _progress: number) => {
if (_progress !== -1) { const uploadProgressHandler = (_event: IpcRendererEvent, _progress: number) => {
showProgress.value = true if (_progress !== -1) {
progress.value = _progress showProgress.value = true
} else { progress.value = _progress
progress.value = 100 } else {
showError.value = true progress.value = 100
} showError.value = true
} }
}
const syncPicBedHandler = () => {
getDefaultPicBed() const syncPicBedHandler = () => {
} getDefaultPicBed()
}
onBeforeMount(() => {
updatePicBedGlobal() onBeforeMount(() => {
window.electron.ipcRendererOn('uploadProgress', uploadProgressHandler) updatePicBedGlobal()
getUseShortUrl() window.electron.ipcRendererOn('uploadProgress', uploadProgressHandler)
getPasteStyle() getUseShortUrl()
getDefaultPicBed() getPasteStyle()
window.electron.ipcRendererOn('syncPicBed', syncPicBedHandler) getDefaultPicBed()
$bus.on(SHOW_INPUT_BOX_RESPONSE, handleInputBoxValue) window.electron.ipcRendererOn('syncPicBed', syncPicBedHandler)
}) $bus.on(SHOW_INPUT_BOX_RESPONSE, handleInputBoxValue)
})
const handleImageProcess = () => {
imageProcessDialogVisible.value = true const handleImageProcess = () => {
} imageProcessDialogVisible.value = true
}
watch(progress, onProgressChange)
watch(progress, onProgressChange)
function onProgressChange (val: number) {
if (val === 100) { function onProgressChange (val: number) {
setTimeout(() => { if (val === 100) {
showProgress.value = false setTimeout(() => {
showError.value = false showProgress.value = false
}, 1000) showError.value = false
setTimeout(() => { }, 1000)
progress.value = 0 setTimeout(() => {
}, 1200) progress.value = 0
} }, 1200)
} }
}
async function handlePicBedNameClick (_picBedName: string, picBedConfigName: string | undefined) {
const formatedpicBedConfigName = picBedConfigName || 'Default' async function handlePicBedNameClick (_picBedName: string, picBedConfigName: string | undefined) {
const currentPicBed = await getConfig<string>(configPaths.picBed.current) const formatedpicBedConfigName = picBedConfigName || 'Default'
const currentPicBedConfig = ((await getConfig<any[]>(`uploader.${currentPicBed}`)) as any) || {} const currentPicBed = await getConfig<string>(configPaths.picBed.current)
const configList = await window.electron.triggerRPC<IUploaderConfigItem>(IRPCActionType.PICBED_GET_CONFIG_LIST, currentPicBed) const currentPicBedConfig = ((await getConfig<any[]>(`uploader.${currentPicBed}`)) as any) || {}
const currentConfigList = configList?.configList ?? [] const configList = await window.electron.triggerRPC<IUploaderConfigItem>(IRPCActionType.PICBED_GET_CONFIG_LIST, currentPicBed)
const config = currentConfigList.find((item: any) => item._configName === formatedpicBedConfigName) const currentConfigList = configList?.configList ?? []
$router.push({ const config = currentConfigList.find((item: any) => item._configName === formatedpicBedConfigName)
name: PICBEDS_PAGE, $router.push({
params: { name: PICBEDS_PAGE,
type: currentPicBed, params: {
configId: config?._id || '' type: currentPicBed,
}, configId: config?._id || ''
query: { },
defaultConfigId: currentPicBedConfig.defaultId || '' query: {
} defaultConfigId: currentPicBedConfig.defaultId || ''
}) }
} })
}
onBeforeUnmount(() => {
$bus.off(SHOW_INPUT_BOX_RESPONSE) onBeforeUnmount(() => {
window.electron.ipcRendererRemoveListener('uploadProgress', uploadProgressHandler) $bus.off(SHOW_INPUT_BOX_RESPONSE)
window.electron.ipcRendererRemoveListener('syncPicBed', syncPicBedHandler) window.electron.ipcRendererRemoveListener('uploadProgress', uploadProgressHandler)
}) window.electron.ipcRendererRemoveListener('syncPicBed', syncPicBedHandler)
})
function onDrop (e: DragEvent) {
dragover.value = false function onDrop (e: DragEvent) {
dragover.value = false
// send files first
if (e.dataTransfer?.files?.length) { // send files first
ipcSendFiles(e.dataTransfer.files) if (e.dataTransfer?.files?.length) {
} else if (e.dataTransfer?.items) { ipcSendFiles(e.dataTransfer.files)
const items = e.dataTransfer.items } else if (e.dataTransfer?.items) {
if (items.length === 2 && items[0].type === 'text/uri-list') { const items = e.dataTransfer.items
handleURLDrag(items, e.dataTransfer) if (items.length === 2 && items[0].type === 'text/uri-list') {
} else if (items[0].type === 'text/plain') { handleURLDrag(items, e.dataTransfer)
const str = e.dataTransfer.getData(items[0].type) } else if (items[0].type === 'text/plain') {
if (isUrl(str)) { const str = e.dataTransfer.getData(items[0].type)
window.electron.sendRPC(IRPCActionType.UPLOAD_CHOOSED_FILES, [{ path: str }]) if (isUrl(str)) {
} else { window.electron.sendRPC(IRPCActionType.UPLOAD_CHOOSED_FILES, [{ path: str }])
message.error(t('pages.upload.dragValidPictureOrUrl')) } else {
} message.error(t('pages.upload.dragValidPictureOrUrl'))
} }
} }
} }
}
function handleURLDrag (items: DataTransferItemList, dataTransfer: DataTransfer) {
// text/html function handleURLDrag (items: DataTransferItemList, dataTransfer: DataTransfer) {
// Use this data to get a more precise URL // text/html
const urlString = dataTransfer.getData(items[1].type) // Use this data to get a more precise URL
const urlMatch = urlString.match(/<img.*src="(.*?)"/) const urlString = dataTransfer.getData(items[1].type)
if (urlMatch) { const urlMatch = urlString.match(/<img.*src="(.*?)"/)
window.electron.sendRPC(IRPCActionType.UPLOAD_CHOOSED_FILES, [ if (urlMatch) {
{ window.electron.sendRPC(IRPCActionType.UPLOAD_CHOOSED_FILES, [
path: urlMatch[1] {
} path: urlMatch[1]
]) }
} else { ])
message.error(t('pages.upload.dragValidPictureOrUrl')) } else {
} message.error(t('pages.upload.dragValidPictureOrUrl'))
} }
}
function openUplodWindow () {
fileInput.value?.click() function openUplodWindow () {
} fileInput.value?.click()
}
function onChange (e: any) {
ipcSendFiles(e.target.files) function onChange (e: any) {
;(fileInput.value as HTMLInputElement).value = '' ipcSendFiles(e.target.files)
} ;(fileInput.value as HTMLInputElement).value = ''
}
function ipcSendFiles (files: FileList) {
const sendFiles: IFileWithPath[] = [] function ipcSendFiles (files: FileList) {
Array.from(files).forEach(item => { const sendFiles: IFileWithPath[] = []
const obj = { Array.from(files).forEach(item => {
name: item.name, const obj = {
path: window.electron.showFilePath(item) name: item.name,
} path: window.electron.showFilePath(item)
sendFiles.push(obj) }
}) sendFiles.push(obj)
window.electron.sendRPC(IRPCActionType.UPLOAD_CHOOSED_FILES, sendFiles) })
} window.electron.sendRPC(IRPCActionType.UPLOAD_CHOOSED_FILES, sendFiles)
}
async function getPasteStyle () {
pasteStyle.value = (await getConfig(configPaths.settings.pasteStyle)) || IPasteStyle.MARKDOWN async function getPasteStyle () {
pasteFormatList.value.Custom = (await getConfig(configPaths.settings.customLink)) || '![$fileName]($url)' 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 async function getUseShortUrl () {
} useShortUrl.value = (await getConfig(configPaths.settings.useShortUrl)) || false
}
function updatePasteStyle (style: string) {
pasteStyle.value = style function updatePasteStyle (style: string) {
saveConfig({ pasteStyle.value = style
[configPaths.settings.pasteStyle]: style || IPasteStyle.MARKDOWN saveConfig({
}) [configPaths.settings.pasteStyle]: style || IPasteStyle.MARKDOWN
} })
}
function updateUrlType (shortUrl: boolean) {
useShortUrl.value = shortUrl function updateUrlType (shortUrl: boolean) {
saveConfig({ useShortUrl.value = shortUrl
[configPaths.settings.useShortUrl]: shortUrl saveConfig({
}) [configPaths.settings.useShortUrl]: shortUrl
} })
}
function uploadClipboardFiles () {
window.electron.sendRPC(IRPCActionType.UPLOAD_CLIPBOARD_FILES_FROM_UPLOAD_PAGE) function uploadClipboardFiles () {
} window.electron.sendRPC(IRPCActionType.UPLOAD_CLIPBOARD_FILES_FROM_UPLOAD_PAGE)
}
async function uploadURLFiles () {
const str = await navigator.clipboard.readText() async function uploadURLFiles () {
$bus.emit(SHOW_INPUT_BOX, { const str = await navigator.clipboard.readText()
value: isUrl(str) ? str : '', $bus.emit(SHOW_INPUT_BOX, {
title: t('pages.upload.inputUrlTip'), value: isUrl(str) ? str : '',
placeholder: t('pages.upload.httpPrefixTip') title: t('pages.upload.inputUrlTip'),
}) placeholder: t('pages.upload.httpPrefixTip')
} })
}
function handleInputBoxValue (val: string) {
if (val === '') return function handleInputBoxValue (val: string) {
if (isUrl(val)) { if (val === '') return
window.electron.sendRPC(IRPCActionType.UPLOAD_CHOOSED_FILES, [ if (isUrl(val)) {
{ window.electron.sendRPC(IRPCActionType.UPLOAD_CHOOSED_FILES, [
path: val {
} path: val
]) }
} else { ])
message.error(t('pages.upload.inputValidUrl')) } else {
} message.error(t('pages.upload.inputValidUrl'))
} }
}
async function getDefaultPicBed () {
const currentPicBed = await getConfig<string>(configPaths.picBed.current) async function getDefaultPicBed () {
picBedGlobal.value.forEach(item => { const currentPicBed = await getConfig<string>(configPaths.picBed.current)
if (item.type === currentPicBed) { picBedGlobal.value.forEach(item => {
picBedName.value = item.name if (item.type === currentPicBed) {
} picBedName.value = item.name
}) }
picBedConfigName.value = (await getConfig<string>(`picBed.${currentPicBed}._configName`)) || '' })
} picBedConfigName.value = (await getConfig<string>(`picBed.${currentPicBed}._configName`)) || ''
}
async function handleChangePicBed () {
window.electron.sendRPC(IRPCActionType.SHOW_UPLOAD_PAGE_MENU) async function handleChangePicBed () {
} window.electron.sendRPC(IRPCActionType.SHOW_UPLOAD_PAGE_MENU)
</script> }
</script>
<script lang="ts">
export default { <script lang="ts">
name: 'UploadPage' export default {
} name: 'UploadPage'
</script> }
</script>
<style scoped src="./css/UploadPage.css"></style>
<style scoped src="./css/UploadPage.css"></style>