🐛 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,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

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, '/')

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
} }
}

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>

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

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
}

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>