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