🚧 WIP(custom): v3.0.0 migrate to vite and esm

This commit is contained in:
Kuingsmile
2025-07-31 17:37:30 +08:00
parent cd76bc7c10
commit 054f4b4cff
597 changed files with 197292 additions and 13329 deletions

View File

@@ -1,12 +1,14 @@
import path from 'node:path'
import windowManager from 'apis/app/window/windowManager'
import axios from 'axios'
import { app, clipboard, dialog, shell } from 'electron'
import fs from 'fs-extra'
import path from 'path'
import { gte, lte } from 'semver'
import windowManager from 'apis/app/window/windowManager'
import { showNotification } from '~/utils/common'
import { IRemoteNoticeActionType, IRemoteNoticeTriggerCount, IRemoteNoticeTriggerHook } from '#/types/enum'
import { IRemoteNotice, IRemoteNoticeAction, IRemoteNoticeLocalCountStorage } from '#/types/types'
import { showNotification } from '~/utils/common'
// for test
const REMOTE_NOTICE_URL = 'https://release.piclist.cn/remote-notice.json'
@@ -21,12 +23,12 @@ class RemoteNoticeHandler {
private remoteNotice: IRemoteNotice | null = null
private remoteNoticeLocalCountStorage: IRemoteNoticeLocalCountStorage | null = null
async init() {
async init () {
this.remoteNotice = await this.getRemoteNoticeInfo()
this.initLocalCountStorage()
}
private initLocalCountStorage() {
private initLocalCountStorage () {
const localCountStorage = {}
if (!fs.existsSync(REMOTE_NOTICE_LOCAL_STORAGE_PATH)) {
fs.writeFileSync(REMOTE_NOTICE_LOCAL_STORAGE_PATH, JSON.stringify({}))
@@ -42,14 +44,14 @@ class RemoteNoticeHandler {
}
}
private saveLocalCountStorage(newData?: IRemoteNoticeLocalCountStorage) {
private saveLocalCountStorage (newData?: IRemoteNoticeLocalCountStorage) {
if (newData) {
this.remoteNoticeLocalCountStorage = newData
}
fs.writeFileSync(REMOTE_NOTICE_LOCAL_STORAGE_PATH, JSON.stringify(this.remoteNoticeLocalCountStorage))
}
private async getRemoteNoticeInfo(): Promise<IRemoteNotice | null> {
private async getRemoteNoticeInfo (): Promise<IRemoteNotice | null> {
try {
const noticeInfo = (await axios({
method: 'get',
@@ -66,7 +68,7 @@ class RemoteNoticeHandler {
* if the notice is not shown or is always shown, then show the notice
* @param action
*/
private checkActionCount(action: IRemoteNoticeAction) {
private checkActionCount (action: IRemoteNoticeAction) {
try {
if (!this.remoteNoticeLocalCountStorage) {
return true
@@ -100,7 +102,7 @@ class RemoteNoticeHandler {
}
}
private async doActions(actions: IRemoteNoticeAction[]) {
private async doActions (actions: IRemoteNoticeAction[]) {
for (const action of actions) {
if (this.checkActionCount(action)) {
switch (action.type) {
@@ -115,7 +117,7 @@ class RemoteNoticeHandler {
body: action.data?.content || '',
clickToCopy: !!action.data?.copyToClipboard,
copyContent: action.data?.copyToClipboard || '',
clickFn() {
clickFn () {
if (action.data?.url) {
shell.openExternal(action.data.url)
}
@@ -161,7 +163,7 @@ class RemoteNoticeHandler {
}
}
triggerHook(hook: IRemoteNoticeTriggerHook) {
triggerHook (hook: IRemoteNoticeTriggerHook) {
if (!this.remoteNotice || !this.remoteNotice.list) {
return
}

View File

@@ -1,30 +1,29 @@
import { globalShortcut } from 'electron'
import shortKeyService from 'apis/app/shortKey/shortKeyService'
import GuiApi from 'apis/gui'
import bus from '@core/bus'
import db from '@core/datastore'
import logger from '@core/picgo/logger'
import picgo from '@core/picgo'
import logger from '@core/picgo/logger'
import shortKeyService from 'apis/app/shortKey/shortKeyService'
import GuiApi from 'apis/gui'
import { globalShortcut } from 'electron'
import { TOGGLE_SHORTKEY_MODIFIED_MODE } from '#/events/constants'
import { IKeyCommandType, IPluginShortKeyConfig, IShortKeyConfig, IShortKeyConfigs, IShortKeyHandler } from '#/types/types'
import { configPaths } from '#/utils/configPaths'
class ShortKeyHandler {
private isInModifiedMode: boolean = false
constructor() {
constructor () {
bus.on(TOGGLE_SHORTKEY_MODIFIED_MODE, flag => {
this.isInModifiedMode = flag
})
}
init() {
async init () {
this.initBuiltInShortKey()
this.initPluginsShortKey()
await this.initPluginsShortKey()
}
private initBuiltInShortKey() {
private initBuiltInShortKey () {
const commands = db.get(configPaths.settings.shortKey._path) as IShortKeyConfigs
Object.keys(commands)
.filter(item => item.includes('picgo:'))
@@ -39,11 +38,11 @@ class ShortKeyHandler {
})
}
private initPluginsShortKey() {
private async initPluginsShortKey () {
// get enabled plugin
const pluginList = picgo.pluginLoader.getList()
for (const item of pluginList) {
const plugin = picgo.pluginLoader.getPlugin(item)
const plugin = await picgo.pluginLoader.getPlugin(item)
// if a plugin has commands
if (plugin && plugin.commands) {
if (typeof plugin.commands !== 'function') {
@@ -69,7 +68,7 @@ class ShortKeyHandler {
}
}
private registerShortKey(
private registerShortKey (
config: IShortKeyConfig | IPluginShortKeyConfig,
command: string,
handler: IShortKeyHandler,
@@ -98,7 +97,7 @@ class ShortKeyHandler {
}
// enable or disable shortKey
bindOrUnbindShortKey(item: IShortKeyConfig, from: string): boolean {
bindOrUnbindShortKey (item: IShortKeyConfig, from: string): boolean {
const command = `${from}:${item.name}`
if (item.enable === false) {
globalShortcut.unregister(item.key)
@@ -122,7 +121,7 @@ class ShortKeyHandler {
}
// update shortKey bindings
updateShortKey(item: IShortKeyConfig, oldKey: string, from: string): boolean {
updateShortKey (item: IShortKeyConfig, oldKey: string, from: string): boolean {
const command = `${from}:${item.name}`
if (globalShortcut.isRegistered(item.key)) return false
globalShortcut.unregister(oldKey)
@@ -135,7 +134,7 @@ class ShortKeyHandler {
return true
}
private async handler(command: string) {
private async handler (command: string) {
if (this.isInModifiedMode) {
return
}
@@ -151,8 +150,8 @@ class ShortKeyHandler {
}
}
registerPluginShortKey(pluginName: string) {
const plugin = picgo.pluginLoader.getPlugin(pluginName)
async registerPluginShortKey (pluginName: string) {
const plugin = await picgo.pluginLoader.getPlugin(pluginName)
if (plugin && plugin.commands) {
if (typeof plugin.commands !== 'function') {
logger.warn(`${pluginName}'s commands is not a function`)
@@ -171,7 +170,7 @@ class ShortKeyHandler {
}
}
unregisterPluginShortKey(pluginName: string) {
unregisterPluginShortKey (pluginName: string) {
const commands = db.get(configPaths.settings.shortKey._path) as IShortKeyConfigs
const keyList = Object.keys(commands)
.filter(command => command.includes(pluginName))

View File

@@ -1,23 +1,25 @@
import logger from '@core/picgo/logger'
import { IShortKeyHandler } from '#/types/types'
class ShortKeyService {
private commandList: Map<string, IShortKeyHandler> = new Map()
registerCommand(command: string, handler: IShortKeyHandler) {
registerCommand (command: string, handler: IShortKeyHandler) {
this.commandList.set(command, handler)
}
unregisterCommand(command: string) {
unregisterCommand (command: string) {
this.commandList.delete(command)
}
getShortKeyHandler(command: string): IShortKeyHandler | null {
getShortKeyHandler (command: string): IShortKeyHandler | null {
const handler = this.commandList.get(command)
if (handler) return handler
logger.warn(`cannot find command: ${command}`)
return null
}
getCommandList() {
getCommandList () {
return [...this.commandList.keys()]
}
}

View File

@@ -1,3 +1,8 @@
import db, { GalleryDB } from '@core/datastore'
import picgo from '@core/picgo'
import uploader from 'apis/app/uploader'
import { handleSecondaryUpload, uploadClipboardFiles } from 'apis/app/uploader/apis'
import windowManager from 'apis/app/window/windowManager'
import {
app,
clipboard,
@@ -10,31 +15,23 @@ import {
Tray
} from 'electron'
import fs from 'fs-extra'
import { cloneDeep } from 'lodash'
import db, { GalleryDB } from '@core/datastore'
import picgo from '@core/picgo'
import uploader from 'apis/app/uploader'
import { handleSecondaryUpload, uploadClipboardFiles } from 'apis/app/uploader/apis'
import windowManager from 'apis/app/window/windowManager'
import { cloneDeep } from 'lodash-es'
import pkg from 'root/package.json'
import { IPasteStyle, IWindowList } from '#/types/enum'
import { IBounds, ImgInfo } from '#/types/types'
import { configPaths } from '#/utils/configPaths'
import { buildPicBedListMenu } from '~/events/remotes/menu'
import { T } from '~/i18n'
import clipboardPoll from '~/utils/clipboardPoll'
import { ensureFilePath, handleCopyUrl, setTray, tray } from '~/utils/common'
import { isMacOSVersionGreaterThanOrEqualTo } from '~/utils/getMacOSVersion'
import pasteTemplate from '~/utils/pasteTemplate'
import { configPaths } from '#/utils/configPaths'
import { IPasteStyle, IWindowList } from '#/types/enum'
import pkg from 'root/package.json'
import { hideMiniWindow, openMainWindow, openMiniWindow } from '~/utils/windowHelper'
let contextMenu: Menu | null
export function setDockMenu() {
export function setDockMenu () {
const isListeningClipboard = db.get(configPaths.settings.isListeningClipboard) || false
const dockMenu = Menu.buildFromTemplate([
{
@@ -43,7 +40,7 @@ export function setDockMenu() {
},
{
label: T('START_WATCH_CLIPBOARD'),
click() {
click () {
db.set(configPaths.settings.isListeningClipboard, true)
clipboardPoll.startListening()
clipboardPoll.on('change', () => {
@@ -56,7 +53,7 @@ export function setDockMenu() {
},
{
label: T('STOP_WATCH_CLIPBOARD'),
click() {
click () {
db.set(configPaths.settings.isListeningClipboard, false)
clipboardPoll.stopListening()
clipboardPoll.removeAllListeners()
@@ -65,10 +62,10 @@ export function setDockMenu() {
visible: isListeningClipboard
}
])
app.dock.setMenu(dockMenu)
app.dock?.setMenu(dockMenu)
}
export function createMenu() {
export function createMenu () {
const submenu = buildPicBedListMenu()
const appMenu = Menu.buildFromTemplate([
{
@@ -77,7 +74,7 @@ export function createMenu() {
{ label: T('OPEN_MAIN_WINDOW'), click: openMainWindow },
{
label: T('RELOAD_APP'),
click() {
click () {
app.relaunch()
app.exit(0)
}
@@ -105,7 +102,7 @@ export function createMenu() {
Menu.setApplicationMenu(appMenu)
}
export function createContextMenu() {
export function createContextMenu () {
const ClipboardWatcher = clipboardPoll
const isListeningClipboard = db.get(configPaths.settings.isListeningClipboard) || false
const isMiniWindowVisible =
@@ -130,7 +127,7 @@ export function createContextMenu() {
if (process.platform === 'darwin' || process.platform === 'win32') {
const submenu = buildPicBedListMenu()
const template: Array<MenuItemConstructorOptions | MenuItem> = [
const template: (MenuItemConstructorOptions | MenuItem)[] = [
{ label: T('OPEN_MAIN_WINDOW'), click: openMainWindow },
{ label: T('CHOOSE_DEFAULT_PICBED'), type: 'submenu', submenu },
{
@@ -145,7 +142,7 @@ export function createContextMenu() {
},
{
label: T('RELOAD_APP'),
click() {
click () {
app.relaunch()
app.exit(0)
}
@@ -158,7 +155,7 @@ export function createContextMenu() {
0,
{
label: T('OPEN_MINI_WINDOW'),
click() {
click () {
openMiniWindow(false)
},
visible: !isMiniWindowVisible
@@ -183,7 +180,7 @@ export function createContextMenu() {
{ label: T('OPEN_MAIN_WINDOW'), click: openMainWindow },
{
label: T('OPEN_MINI_WINDOW'),
click() {
click () {
openMiniWindow(false)
},
visible: !isMiniWindowVisible
@@ -205,7 +202,7 @@ export function createContextMenu() {
},
{
label: T('ABOUT'),
click() {
click () {
dialog.showMessageBox({
title: 'PicList',
message: 'PicList',
@@ -222,13 +219,13 @@ export function createContextMenu() {
const getTrayIcon = () => {
if (process.platform === 'darwin') {
const isMacOSGreaterThan11 = isMacOSVersionGreaterThanOrEqualTo('11')
return isMacOSGreaterThan11 ? `${__static}/menubar-newdarwinTemplate.png` : `${__static}/menubar.png`
return isMacOSGreaterThan11 ? './resources/menubar-newdarwinTemplate.png' : './resources/menubar.png'
} else {
return `${__static}/menubar-nodarwin.png`
return './resources/menubar-nodarwin.png'
}
}
export function createTray(tooltip: string) {
export function createTray (tooltip: string) {
const menubarPic = getTrayIcon()
setTray(new Tray(menubarPic))
tray.setToolTip(tooltip)
@@ -241,6 +238,7 @@ export function createTray(tooltip: string) {
createContextMenu()
tray!.popUpContextMenu(contextMenu!)
})
tray.on('click', (_, bounds) => {
if (process.platform === 'darwin') {
toggleWindow(bounds)
@@ -293,9 +291,9 @@ export function createTray(tooltip: string) {
tray.on('drag-enter', () => {
if (nativeTheme.shouldUseDarkColors) {
tray!.setImage(`${__static}/upload-dark.png`)
tray!.setImage('./resources/upload-dark.png')
} else {
tray!.setImage(`${__static}/upload.png`)
tray!.setImage('./resources/upload.png')
}
})
@@ -305,54 +303,56 @@ export function createTray(tooltip: string) {
// drop-files only be supported in macOS
// so the tray window must be available
tray.on('drop-files', async (_: Event, files: string[]) => {
const pasteStyle = db.get(configPaths.settings.pasteStyle) || IPasteStyle.MARKDOWN
const rawInput = cloneDeep(files)
const trayWindow = windowManager.get(IWindowList.TRAY_WINDOW)!
const { needRestore, ctx } = await handleSecondaryUpload(trayWindow.webContents, files, 'tray')
let imgs: ImgInfo[] | false = false
if (needRestore) {
const res = await uploader
.setWebContents(trayWindow.webContents)
.uploadReturnCtx(ctx ? ctx.processedInput : files, true)
imgs = res ? res.output : false
} else {
imgs = await uploader.setWebContents(trayWindow.webContents).upload(files)
}
const deleteLocalFile = db.get(configPaths.settings.deleteLocalFile) || false
if (imgs !== false) {
const pasteText: string[] = []
for (let i = 0; i < imgs.length; i++) {
if (deleteLocalFile) {
await fs.remove(rawInput[i])
}
const [pasteTextItem, shortUrl] = await pasteTemplate(
pasteStyle,
imgs[i],
db.get(configPaths.settings.customLink)
)
imgs[i].shortUrl = shortUrl
pasteText.push(pasteTextItem)
const isShowResultNotification =
if (process.platform === 'darwin') {
(tray as any).on('drop-files', async (_: Event, files: string[]) => {
const pasteStyle = db.get(configPaths.settings.pasteStyle) || IPasteStyle.MARKDOWN
const rawInput = cloneDeep(files)
const trayWindow = windowManager.get(IWindowList.TRAY_WINDOW)!
const { needRestore, ctx } = await handleSecondaryUpload(trayWindow.webContents, files, 'tray')
let imgs: ImgInfo[] | false = false
if (needRestore) {
const res = await uploader
.setWebContents(trayWindow.webContents)
.uploadReturnCtx(ctx ? ctx.processedInput : files, true)
imgs = res ? res.output : false
} else {
imgs = await uploader.setWebContents(trayWindow.webContents).upload(files)
}
const deleteLocalFile = db.get(configPaths.settings.deleteLocalFile) || false
if (imgs !== false) {
const pasteText: string[] = []
for (let i = 0; i < imgs.length; i++) {
if (deleteLocalFile) {
await fs.remove(rawInput[i])
}
const [pasteTextItem, shortUrl] = await pasteTemplate(
pasteStyle,
imgs[i],
db.get(configPaths.settings.customLink)
)
imgs[i].shortUrl = shortUrl
pasteText.push(pasteTextItem)
const isShowResultNotification =
db.get(configPaths.settings.uploadResultNotification) === undefined
? true
: !!db.get(configPaths.settings.uploadResultNotification)
if (isShowResultNotification) {
const notification = new Notification({
title: T('UPLOAD_SUCCEED'),
body: shortUrl || imgs[i].imgUrl!
if (isShowResultNotification) {
const notification = new Notification({
title: T('UPLOAD_SUCCEED'),
body: shortUrl || imgs[i].imgUrl!
// icon: files[i]
})
setTimeout(() => {
notification.show()
}, i * 100)
})
setTimeout(() => {
notification.show()
}, i * 100)
}
await GalleryDB.getInstance().insert(imgs[i])
}
await GalleryDB.getInstance().insert(imgs[i])
handleCopyUrl(pasteText.join('\n'))
trayWindow.webContents.send('dragFiles', imgs)
}
handleCopyUrl(pasteText.join('\n'))
trayWindow.webContents.send('dragFiles', imgs)
}
})
})
}
// toggleWindow()
} else if (process.platform === 'linux') {
// click事件在Ubuntu上无法触发Unity不支持在Mac和Windows上可以触发

View File

@@ -1,21 +1,19 @@
import { Notification, WebContents } from 'electron'
import fs from 'fs-extra'
import { cloneDeep } from 'lodash'
import picgo from '@core/picgo'
import db, { GalleryDB } from '@core/datastore'
import picgo from '@core/picgo'
import uploader from 'apis/app/uploader'
import windowManager from 'apis/app/window/windowManager'
import { T } from '~/i18n/index'
import { handleCopyUrl, handleUrlEncodeWithSetting } from '~/utils/common'
import pasteTemplate from '~/utils/pasteTemplate'
import { Notification, WebContents } from 'electron'
import fs from 'fs-extra'
import { cloneDeep } from 'lodash-es'
import { IPicGo } from 'piclist'
import { IPasteStyle, IWindowList } from '#/types/enum'
import { IFileWithPath, ImgInfo, IStringKeyMap, IUploadOption } from '#/types/types'
import { configPaths } from '#/utils/configPaths'
import { T } from '~/i18n/index'
import { handleCopyUrl, handleUrlEncodeWithSetting } from '~/utils/common'
import { changeCurrentUploader } from '~/utils/handleUploaderConfig'
import { IPicGo } from 'piclist'
import pasteTemplate from '~/utils/pasteTemplate'
const handleClipboardUploading = async (): Promise<false | ImgInfo[]> => {
const useBuiltinClipboard =
@@ -213,8 +211,8 @@ export const handleSecondaryUpload = async (
trayWindow?.webContents?.send('uploadFiles', secondImgs)
}
} else {
for (let i = 0; i < secondImgs.length; i++) {
await GalleryDB.getInstance().insert(secondImgs[i])
for (const secondImgsItem of secondImgs) {
await GalleryDB.getInstance().insert(secondImgsItem)
}
if (uploadType === 'tray') {
trayWindow?.webContents?.send('dragFiles', secondImgs)

View File

@@ -1,28 +1,27 @@
import dayjs from 'dayjs'
import { BrowserWindow, clipboard, ipcMain, Notification, WebContents } from 'electron'
import fs from 'fs-extra'
import path from 'path'
import { IPicGo } from 'piclist'
import util from 'util'
import writeFile from 'write-file-atomic'
import windowManager from 'apis/app/window/windowManager'
import path from 'node:path'
import util from 'node:util'
import db from '@core/datastore'
import picgo from '@core/picgo'
import logger from '@core/picgo/logger'
import { T } from '~/i18n'
import { showNotification, getClipboardFilePath, calcDurationRange } from '~/utils/common'
import windowManager from 'apis/app/window/windowManager'
import dayjs from 'dayjs'
import { BrowserWindow, clipboard, ipcMain, IpcMainEvent, Notification, WebContents } from 'electron'
import fs from 'fs-extra'
import { IPicGo } from 'piclist'
import writeFile from 'write-file-atomic'
import { GET_RENAME_FILE_NAME, RENAME_FILE_NAME, TALKING_DATA_EVENT } from '#/events/constants'
import { ICOREBuildInEvent, IWindowList } from '#/types/enum'
import { IAnalyticsData, ImgInfo, ITalkingDataOptions, IUploadOption } from '#/types/types'
import { configPaths } from '#/utils/configPaths'
import { CLIPBOARD_IMAGE_FOLDER } from '#/utils/static'
import { T } from '~/i18n'
import { calcDurationRange, getClipboardFilePath, showNotification } from '~/utils/common'
const waitForRename = (window: BrowserWindow, id: number): Promise<string | null> => {
return new Promise(resolve => {
ipcMain.once(`${RENAME_FILE_NAME}${id}`, (_: Event, newName: string) => {
ipcMain.once(`${RENAME_FILE_NAME}${id}`, (_: IpcMainEvent, newName: string) => {
resolve(newName)
window.close()
})
@@ -52,12 +51,12 @@ const handleTalkingData = (webContents: WebContents, options: IAnalyticsData) =>
class Uploader {
private webContents: WebContents | null = null
constructor() {
constructor () {
this.init()
}
init() {
picgo.on(ICOREBuildInEvent.NOTIFICATION, (message: Electron.NotificationConstructorOptions | undefined) => {
init () {
picgo.on(ICOREBuildInEvent.NOTIFICATION, (message: any) => {
new Notification(message).show()
})
@@ -88,7 +87,7 @@ class Uploader {
: item.fileName
if (rename) {
const window = windowManager.create(IWindowList.RENAME_WINDOW)!
ipcMain.on(GET_RENAME_FILE_NAME, evt => {
ipcMain.on(GET_RENAME_FILE_NAME, (evt, _) => {
try {
if (evt.sender.id === window.webContents.id) {
logger.info('rename window ready, wait for rename...')
@@ -108,12 +107,12 @@ class Uploader {
})
}
setWebContents(webContents: WebContents) {
setWebContents (webContents: WebContents) {
this.webContents = webContents
return this
}
private async getClipboardImagePath(): Promise<string | false> {
private async getClipboardImagePath (): Promise<string | false> {
const imgPath = getClipboardFilePath()
if (imgPath) return imgPath
@@ -131,7 +130,7 @@ class Uploader {
/**
* use electron's clipboard image to upload
*/
async uploadWithBuildInClipboard(): Promise<ImgInfo[] | false> {
async uploadWithBuildInClipboard (): Promise<ImgInfo[] | false> {
let imgPath: string | false = false
try {
imgPath = await this.getClipboardImagePath()
@@ -147,7 +146,7 @@ class Uploader {
}
}
async uploadWithBuildInClipboardReturnCtx(img?: IUploadOption, skipProcess = false): Promise<IPicGo | false> {
async uploadWithBuildInClipboardReturnCtx (img?: IUploadOption, skipProcess = false): Promise<IPicGo | false> {
let imgPath: string | false = false
try {
imgPath = await this.getClipboardImagePath()
@@ -163,7 +162,7 @@ class Uploader {
}
}
async uploadReturnCtx(img?: IUploadOption, skipProcess = false): Promise<IPicGo | false> {
async uploadReturnCtx (img?: IUploadOption, skipProcess = false): Promise<IPicGo | false> {
try {
const startTime = Date.now()
const ctx = await picgo.uploadReturnCtx(img, skipProcess)
@@ -198,7 +197,7 @@ class Uploader {
}
}
async upload(img?: IUploadOption): Promise<ImgInfo[] | false> {
async upload (img?: IUploadOption): Promise<ImgInfo[] | false> {
try {
const startTime = Date.now()
const output = await picgo.upload(img)

View File

@@ -2,25 +2,27 @@ const isDevelopment = process.env.NODE_ENV !== 'production'
export const MANUAL_WINDOW_URL =
process.env.NODE_ENV === 'development'
? `${process.env.WEBPACK_DEV_SERVER_URL as string}#documents`
: 'picgo://./index.html#documents'
? 'http://localhost:3000#documents'
: 'index.html#documents'
export const MINI_WINDOW_URL = isDevelopment
? `${process.env.WEBPACK_DEV_SERVER_URL as string}#mini-page`
: 'picgo://./index.html#mini-page'
? 'http://localhost:3000#mini-page'
: 'index.html#mini-page'
export const RENAME_WINDOW_URL =
process.env.NODE_ENV === 'development'
? `${process.env.WEBPACK_DEV_SERVER_URL as string}#rename-page`
: 'picgo://./index.html#rename-page'
? 'http://localhost:3000#rename-page'
: 'index.html#rename-page'
export const SETTING_WINDOW_URL = isDevelopment
? `${process.env.WEBPACK_DEV_SERVER_URL as string}#main-page/upload`
: 'picgo://./index.html#main-page/upload'
? 'http://localhost:3000#main-page/upload'
: 'index.html#main-page/upload'
export const TRAY_WINDOW_URL = isDevelopment ? (process.env.WEBPACK_DEV_SERVER_URL as string) : 'picgo://./index.html'
export const TRAY_WINDOW_URL = isDevelopment ? 'http://localhost:3000' : 'index.html'
console.log(TRAY_WINDOW_URL)
export const TOOLBOX_WINDOW_URL =
process.env.NODE_ENV === 'development'
? `${process.env.WEBPACK_DEV_SERVER_URL as string}#toolbox-page`
: 'picgo://./index.html#toolbox-page'
? 'http://localhost:3000#toolbox-page'
: 'index.html#toolbox-page'

View File

@@ -1,24 +1,26 @@
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 } from 'electron'
import { TOGGLE_SHORTKEY_MODIFIED_MODE } from '#/events/constants'
import { IWindowListItem } from '#/types/electron'
import { IWindowList } from '#/types/enum'
import { IBrowserWindowOptions } from '#/types/types'
import { configPaths } from '#/utils/configPaths'
import { T } from '~/i18n'
import {
MANUAL_WINDOW_URL,
MINI_WINDOW_URL,
RENAME_WINDOW_URL,
SETTING_WINDOW_URL,
TRAY_WINDOW_URL,
TOOLBOX_WINDOW_URL
TOOLBOX_WINDOW_URL,
TRAY_WINDOW_URL
} from './constants'
import bus from '@core/bus'
import { CREATE_APP_MENU } from '@core/bus/constants'
import db from '@core/datastore'
import { T } from '~/i18n'
import { TOGGLE_SHORTKEY_MODIFIED_MODE } from '#/events/constants'
import { IWindowList } from '#/types/enum'
import { configPaths } from '#/utils/configPaths'
const windowList = new Map<IWindowList, IWindowListItem>()
const handleWindowParams = (windowURL: string) => windowURL
@@ -33,6 +35,7 @@ const getDefaultWindowSizes = (): { width: number; height: number } => {
height: mainWindowHeight || 800
}
}
const preloadPath = fileURLToPath(new URL('../preload/index.mjs', import.meta.url))
const { width: defaultWindowWidth, height: defaultWindowHeight } = getDefaultWindowSizes()
@@ -46,8 +49,10 @@ const trayWindowOptions = {
transparent: true,
vibrancy: 'ultra-dark',
webPreferences: {
nodeIntegration: !!process.env.ELECTRON_NODE_INTEGRATION,
contextIsolation: !process.env.ELECTRON_NODE_INTEGRATION,
sandbox: false,
preload: preloadPath,
nodeIntegration: false,
contextIsolation: true,
nodeIntegrationInWorker: true,
backgroundThrottling: false,
webSecurity: false
@@ -66,10 +71,12 @@ const manualWindowOptions = {
vibrancy: 'ultra-dark',
transparent: false,
webPreferences: {
sandbox: false,
webviewTag: true,
backgroundThrottling: false,
nodeIntegration: !!process.env.ELECTRON_NODE_INTEGRATION,
contextIsolation: !process.env.ELECTRON_NODE_INTEGRATION,
preload: preloadPath,
nodeIntegration: false,
contextIsolation: true,
nodeIntegrationInWorker: true,
webSecurity: false
}
@@ -88,10 +95,12 @@ const settingWindowOptions = {
transparent: true,
titleBarStyle: 'hidden',
webPreferences: {
sandbox: false,
webviewTag: true,
backgroundThrottling: false,
nodeIntegration: !!process.env.ELECTRON_NODE_INTEGRATION,
contextIsolation: !process.env.ELECTRON_NODE_INTEGRATION,
preload: preloadPath,
nodeIntegration: false,
contextIsolation: true,
nodeIntegrationInWorker: true,
webSecurity: false
}
@@ -102,7 +111,7 @@ if (process.platform !== 'darwin') {
settingWindowOptions.frame = false
settingWindowOptions.backgroundColor = '#3f3c37'
settingWindowOptions.transparent = false
settingWindowOptions.icon = `${__static}/logo.png`
settingWindowOptions.icon = '.resources/logo.png'
}
const miniWindowOptions = {
@@ -114,11 +123,13 @@ const miniWindowOptions = {
skipTaskbar: true,
resizable: false,
transparent: process.platform !== 'linux',
icon: `${__static}/logo.png`,
icon: './resources/logo.png',
webPreferences: {
sandbox: false,
preload: preloadPath,
nodeIntegration: false,
contextIsolation: true,
backgroundThrottling: false,
nodeIntegration: !!process.env.ELECTRON_NODE_INTEGRATION,
contextIsolation: !process.env.ELECTRON_NODE_INTEGRATION,
nodeIntegrationInWorker: true
}
} as IBrowserWindowOptions
@@ -135,8 +146,10 @@ const renameWindowOptions = {
resizable: false,
vibrancy: 'ultra-dark',
webPreferences: {
nodeIntegration: !!process.env.ELECTRON_NODE_INTEGRATION,
contextIsolation: !process.env.ELECTRON_NODE_INTEGRATION,
sandbox: false,
preload: preloadPath,
nodeIntegration: false,
contextIsolation: true,
nodeIntegrationInWorker: true,
backgroundThrottling: false
}
@@ -159,11 +172,13 @@ const toolboxWindowOptions = {
resizable: false,
title: `PicList ${T('TOOLBOX')}`,
vibrancy: 'ultra-dark',
icon: `${__static}/logo.png`,
icon: './resources/logo.png',
webPreferences: {
sandbox: false,
backgroundThrottling: false,
nodeIntegration: !!process.env.ELECTRON_NODE_INTEGRATION,
contextIsolation: !process.env.ELECTRON_NODE_INTEGRATION,
preload: preloadPath,
nodeIntegration: false,
contextIsolation: true,
nodeIntegrationInWorker: true,
webSecurity: false
}
@@ -179,7 +194,7 @@ windowList.set(IWindowList.TRAY_WINDOW, {
isValid: process.platform !== 'linux',
multiple: false,
options: () => trayWindowOptions,
callback(window) {
callback (window) {
window.loadURL(handleWindowParams(TRAY_WINDOW_URL))
window.on('blur', () => {
window.hide()
@@ -191,7 +206,7 @@ windowList.set(IWindowList.MANUAL_WINDOW, {
isValid: true,
multiple: false,
options: () => manualWindowOptions,
callback(window) {
callback (window) {
window.loadURL(handleWindowParams(MANUAL_WINDOW_URL))
window.focus()
}
@@ -201,8 +216,9 @@ windowList.set(IWindowList.SETTING_WINDOW, {
isValid: true,
multiple: false,
options: () => settingWindowOptions,
callback(window, windowManager) {
callback (window, windowManager) {
window.loadURL(handleWindowParams(SETTING_WINDOW_URL))
window.webContents.openDevTools({ mode: 'detach' })
window.on('closed', () => {
bus.emit(TOGGLE_SHORTKEY_MODIFIED_MODE, false)
if (process.platform === 'linux') {
@@ -220,7 +236,7 @@ windowList.set(IWindowList.MINI_WINDOW, {
isValid: process.platform !== 'darwin',
multiple: false,
options: () => miniWindowOptions,
callback(window) {
callback (window) {
window.loadURL(handleWindowParams(MINI_WINDOW_URL))
}
})
@@ -229,7 +245,7 @@ windowList.set(IWindowList.RENAME_WINDOW, {
isValid: true,
multiple: true,
options: () => renameWindowOptions,
async callback(window, windowManager) {
async callback (window, windowManager) {
window.loadURL(handleWindowParams(RENAME_WINDOW_URL))
const currentWindow = windowManager.getAvailableWindow(true)
if (currentWindow && currentWindow.isVisible()) {
@@ -245,7 +261,7 @@ windowList.set(IWindowList.TOOLBOX_WINDOW, {
isValid: true,
multiple: false,
options: () => toolboxWindowOptions,
async callback(window, windowManager) {
async callback (window, windowManager) {
window.loadURL(TOOLBOX_WINDOW_URL)
const currentWindow = windowManager.getAvailableWindow(true)
if (currentWindow && currentWindow.isVisible()) {

View File

@@ -1,13 +1,14 @@
import windowList from 'apis/app/window/windowList'
import { BrowserWindow } from 'electron'
import windowList from 'apis/app/window/windowList'
import { IWindowListItem, IWindowManager } from '#/types/electron'
import { IWindowList } from '#/types/enum'
class WindowManager implements IWindowManager {
#windowMap: Map<IWindowList | string, BrowserWindow> = new Map()
#windowIdMap: Map<number, IWindowList | string> = new Map()
create(name: IWindowList) {
create (name: IWindowList) {
const windowConfig: IWindowListItem = windowList.get(name)!
if (!windowConfig.isValid) return null
@@ -29,14 +30,14 @@ class WindowManager implements IWindowManager {
return window
}
get(name: IWindowList) {
get (name: IWindowList) {
if (this.has(name)) {
return this.#windowMap.get(name)!
}
return this.create(name)
}
has(name: IWindowList) {
has (name: IWindowList) {
return this.#windowMap.has(name)
}
@@ -48,7 +49,7 @@ class WindowManager implements IWindowManager {
}
}
getAvailableWindow(isSkipMiniWindow = false) {
getAvailableWindow (isSkipMiniWindow = false) {
const miniWindow = this.#windowMap.get(IWindowList.MINI_WINDOW)
if (miniWindow && miniWindow.isVisible() && !isSkipMiniWindow) {
return miniWindow

View File

@@ -1,4 +1,3 @@
import bus from '@core/bus/index'
import {
GET_SETTING_WINDOW_ID,
GET_SETTING_WINDOW_ID_RESPONSE,
@@ -9,6 +8,9 @@ import {
UPLOAD_WITH_FILES,
UPLOAD_WITH_FILES_RESPONSE
} from '@core/bus/constants'
import bus from '@core/bus/index'
import { IFileWithPath } from '#/types/types'
export const uploadWithClipboardFiles = (): Promise<{
success: boolean

View File

@@ -1,4 +1,4 @@
import { EventEmitter } from 'events'
import { EventEmitter } from 'node:events'
const bus = new EventEmitter()

View File

@@ -1,11 +1,12 @@
import { app } from 'electron'
import fs from 'fs-extra'
import dayjs from 'dayjs'
import path from 'path'
import writeFile from 'write-file-atomic'
import path from 'node:path'
import { getLogger } from '@core/utils/localLogger'
import dayjs from 'dayjs'
import { app } from 'electron'
import fs from 'fs-extra'
import writeFile from 'write-file-atomic'
import { notificationList } from '#/utils/notification'
import { T } from '~/i18n'
const STORE_PATH = app.getPath('userData')
@@ -22,10 +23,7 @@ const errorMsg = {
brokenButBackup: T('TIPS_PICGO_CONFIG_FILE_BROKEN_WITH_BACKUP')
}
/** ensure notification list */
if (!global.notificationList) global.notificationList = []
function dbChecker() {
function dbChecker () {
if (process.type !== 'renderer') {
// db save bak
try {
@@ -63,16 +61,16 @@ function dbChecker() {
optionsTpl.body = `${errorMsg.brokenButBackup}\n${T('TIPS_PICGO_BACKUP_FILE_VERSION', {
v: dayjs(stats.mtime).format('YYYY-MM-DD HH:mm:ss')
})}`
global.notificationList?.push(optionsTpl)
notificationList.push(optionsTpl)
return
} catch (e) {
optionsTpl.body = errorMsg.broken
global.notificationList?.push(optionsTpl)
notificationList.push(optionsTpl)
return
}
}
optionsTpl.body = errorMsg.broken
global.notificationList?.push(optionsTpl)
notificationList.push(optionsTpl)
return
}
writeFile.sync(configFileBackupPath, configFile, { encoding: 'utf-8' })
@@ -82,7 +80,7 @@ function dbChecker() {
/**
* Get config path
*/
function dbPathChecker(): string {
function dbPathChecker (): string {
if (_configFilePath) {
return _configFilePath
}
@@ -113,7 +111,7 @@ function dbPathChecker(): string {
title: T('TIPS_NOTICE'),
body: T('TIPS_CUSTOM_CONFIG_FILE_PATH_ERROR')
}
global.notificationList?.push(optionsTpl)
notificationList.push(optionsTpl)
hasCheckPath = true
}
logger('error', e)
@@ -122,11 +120,11 @@ function dbPathChecker(): string {
}
}
function dbPathDir() {
function dbPathDir () {
return path.dirname(dbPathChecker())
}
function getGalleryDBPath(): {
function getGalleryDBPath (): {
dbPath: string
dbBackupPath: string
} {

View File

@@ -1,13 +1,13 @@
import fs from 'fs-extra'
import { DBStore, JSONStore } from '@picgo/store'
import { dbPathChecker, dbPathDir, getGalleryDBPath } from '@core/datastore/dbChecker'
import { T } from '~/i18n'
import { configPaths } from '#/utils/configPaths'
import { IJSON } from '@picgo/store/dist/types'
import { DBStore, JSONStore } from '@piclist/store'
import fs from 'fs-extra'
import { IConfig } from 'piclist'
import { configPaths } from '#/utils/configPaths'
import { T } from '~/i18n'
interface IJSON {
[propsName: string]: string | number | IJSON
}
const STORE_PATH = dbPathDir()
if (!fs.pathExistsSync(STORE_PATH)) {
@@ -19,7 +19,7 @@ export const DB_PATH: string = getGalleryDBPath().dbPath
class ConfigStore {
#db: JSONStore
constructor() {
constructor () {
this.#db = new JSONStore(CONFIG_PATH)
if (!this.#db.has('picBed')) {
@@ -43,11 +43,11 @@ class ConfigStore {
this.read()
}
read(flush?: boolean): IJSON {
read (flush?: boolean): IJSON {
return this.#db.read(flush)
}
getSingle(key = ''): any {
getSingle (key = ''): any {
if (key === '') {
return this.#db.read(true)
}
@@ -55,43 +55,43 @@ class ConfigStore {
return this.#db.get(key)
}
get(key: string): any
get(key: string[]): any[]
get(key: string | string[] = ''): any {
get (key: string): any
get (key: string[]): any[]
get (key: string | string[] = ''): any {
if (Array.isArray(key)) {
return key.map(k => this.getSingle(k))
}
return this.getSingle(key)
}
set(key: string, value: any): void {
set (key: string, value: any): void {
this.read(true)
return this.#db.set(key, value)
}
has(key: string) {
has (key: string) {
this.read(true)
return this.#db.has(key)
}
unset(key: string, value: any): boolean {
unset (key: string, value: any): boolean {
this.read(true)
return this.#db.unset(key, value)
}
saveConfig(config: Partial<IConfig>): void {
saveConfig (config: Partial<IConfig>): void {
Object.keys(config).forEach((name: string) => {
this.set(name, config[name])
})
}
removeConfig(config: IConfig): void {
removeConfig (config: IConfig): void {
Object.keys(config).forEach((name: string) => {
this.unset(name, config[name])
})
}
getConfigPath() {
getConfigPath () {
return CONFIG_PATH
}
}
@@ -103,11 +103,11 @@ export default db
// v2.3.0 add gallery db
class GalleryDB {
static #instance: DBStore
private constructor() {
private constructor () {
console.log('init gallery db')
}
static getInstance(): DBStore {
static getInstance (): DBStore {
if (!GalleryDB.#instance) {
GalleryDB.#instance = new DBStore(DB_PATH, 'gallery')
}

View File

@@ -1,27 +1,27 @@
import debounce from 'lodash/debounce'
import { PicGo } from 'piclist'
import db from '@core/datastore'
import { dbChecker, dbPathChecker } from '@core/datastore/dbChecker'
import { debounce } from 'lodash-es'
import { PicGo } from 'piclist'
import pkg from 'root/package.json'
import { IStringKeyMap } from '#/types/types'
const CONFIG_PATH = dbPathChecker()
dbChecker()
const picgo = new PicGo(CONFIG_PATH)
const picgo = await PicGo.create(CONFIG_PATH)
picgo.saveConfig({
debug: true,
PICGO_ENV: 'GUI'
})
global.PICGO_GUI_VERSION = pkg.version
picgo.GUI_VERSION = global.PICGO_GUI_VERSION
picgo.GUI_VERSION = pkg.version
const originPicGoSaveConfig = picgo.saveConfig.bind(picgo)
function flushDB() {
function flushDB () {
db.read(true)
}

View File

@@ -1,6 +1,9 @@
import fs from 'fs-extra'
import util from 'node:util'
import dayjs from 'dayjs'
import util from 'util'
import fs from 'fs-extra'
import { ILogArgvTypeWithError } from '#/types/types'
const MB = 1024 * 1024
const DEFAULT_LOG_FILE_SIZE_LIMIT = 10 * MB

View File

@@ -0,0 +1,49 @@
import path from 'node:path'
import axios from 'axios'
import { deleteFailedLog, deleteLog } from '~/utils/deleteLog'
interface IConfigMap {
fileName: string
config: {
version: string
url: string
uploadPath: string
token: string
}
}
export default class AlistApi {
static async delete (configMap: IConfigMap): Promise<boolean> {
const { fileName, config } = configMap
try {
const { version, url, uploadPath, token } = config
if (String(version) === '2') {
deleteLog(fileName, 'Alist', false, 'Alist version 2 is not supported, deletion is skipped')
return true
}
const result = await axios.request({
method: 'post',
url: `${url}/api/fs/remove`,
headers: {
'Content-Type': 'application/json',
Authorization: token
},
data: {
dir: path.join('/', uploadPath, path.dirname(fileName)),
names: [path.basename(fileName)]
}
})
if (result.data.code === 200) {
deleteLog(fileName, 'Alist')
return true
}
deleteLog(fileName, 'Alist', false)
return false
} catch (error: any) {
deleteFailedLog(fileName, 'Alist', error)
return false
}
}
}

View File

@@ -0,0 +1,64 @@
import path from 'node:path'
import axios from 'axios'
import { deleteFailedLog, deleteLog } from '~/utils/deleteLog'
interface IConfigMap {
fileName: string
config: {
url: string
username: string
password: string
uploadPath: string
token: string
}
}
const getAListToken = async (url: string, username: string, password: string) => {
const res = await axios.post(`${url}/api/auth/login`, {
username,
password
})
if (res.data.code === 200 && res.data.message === 'success') {
return res.data.data.token
}
}
export default class AListplistApi {
static async delete (configMap: IConfigMap): Promise<boolean> {
const { fileName, config } = configMap
try {
const { url, username, password, uploadPath } = config
let token = config.token
if (!token) {
token = await getAListToken(url, username, password)
}
if (!url || !(token || (username && password))) {
deleteFailedLog(fileName, 'Alist', 'No valid token or username/password provided')
return false
}
const result = await axios.request({
method: 'post',
url: `${url}/api/fs/remove`,
headers: {
'Content-Type': 'application/json',
Authorization: token
},
data: {
dir: path.join('/', uploadPath, path.dirname(fileName)),
names: [path.basename(fileName)]
}
})
if (result.data.code === 200) {
deleteLog(fileName, 'Alist')
return true
}
deleteLog(fileName, 'Alist', false)
return false
} catch (error: any) {
deleteFailedLog(fileName, 'Alist', error)
return false
}
}
}

View File

@@ -0,0 +1,33 @@
import OSS from 'ali-oss'
import { IAliYunConfig, PartialKeys } from '#/types/types'
import { deleteFailedLog, deleteLog } from '~/utils/deleteLog'
interface IConfigMap {
fileName: string
config: PartialKeys<IAliYunConfig, 'path'>
}
export default class AliyunApi {
static #getKey (fileName: string, path?: string): string {
return path && path !== '/' ? `${path.replace(/^\/+|\/+$/, '')}/${fileName}` : fileName
}
static async delete (configMap: IConfigMap): Promise<boolean> {
const { fileName, config } = configMap
try {
const client = new OSS({ ...config, region: config.area })
const key = AliyunApi.#getKey(fileName, config.path)
const result = await client.delete(key)
if (result.res.status === 204) {
deleteLog(fileName, 'Aliyun')
return true
}
deleteLog(fileName, 'Aliyun', false)
return false
} catch (error: any) {
deleteFailedLog(fileName, 'Aliyun', error)
return false
}
}
}

View File

@@ -0,0 +1,46 @@
import { IStringKeyMap } from '#/types/types'
import AlistApi from '~/apis/delete/alist'
import AlistplistApi from '~/apis/delete/alistplist'
import AliyunApi from '~/apis/delete/aliyun'
import AwsS3Api from '~/apis/delete/awss3'
import DogeCloudApi from '~/apis/delete/dogecloud'
import GithubApi from '~/apis/delete/github'
import HuaweicloudApi from '~/apis/delete/huaweiyun'
import ImgurApi from '~/apis/delete/imgur'
import LocalApi from '~/apis/delete/local'
import LskyplistApi from '~/apis/delete/lskyplist'
import PiclistApi from '~/apis/delete/piclist'
import QiniuApi from '~/apis/delete/qiniu'
import SftpPlistApi from '~/apis/delete/sftpplist'
import SmmsApi from '~/apis/delete/smms'
import TcyunApi from '~/apis/delete/tcyun'
import UpyunApi from '~/apis/delete/upyun'
import WebdavApi from '~/apis/delete/webdav'
const apiMap: IStringKeyMap = {
alist: AlistApi,
alistplist: AlistplistApi,
aliyun: AliyunApi,
'aws-s3': AwsS3Api,
'aws-s3-plist': AwsS3Api,
dogecloud: DogeCloudApi,
github: GithubApi,
'huaweicloud-uploader': HuaweicloudApi,
imgur: ImgurApi,
local: LocalApi,
lskyplist: LskyplistApi,
piclist: PiclistApi,
qiniu: QiniuApi,
sftpplist: SftpPlistApi,
smms: SmmsApi,
tcyun: TcyunApi,
upyun: UpyunApi,
webdavplist: WebdavApi
}
export default class ALLApi {
static async delete (configMap: IStringKeyMap): Promise<boolean> {
const api = apiMap[configMap.type]
return api ? await api.delete(configMap) : false
}
}

View File

@@ -0,0 +1,15 @@
import { getRawData } from '@/utils/common'
import { IStringKeyMap } from '#/types/types'
import { removeFileFromS3InMain } from '~/utils/deleteFunc'
import { deleteFailedLog } from '~/utils/deleteLog'
export default class AwsS3Api {
static async delete (configMap: IStringKeyMap): Promise<boolean> {
try {
return await removeFileFromS3InMain(getRawData(configMap))
} catch (error: any) {
deleteFailedLog(configMap.fileName, 'AWS S3', error)
return false
}
}
}

View File

@@ -0,0 +1,15 @@
import { getRawData } from '@/utils/common'
import { IStringKeyMap } from '#/types/types'
import { removeFileFromDogeInMain } from '~/utils/deleteFunc'
import { deleteFailedLog } from '~/utils/deleteLog'
export default class AwsS3Api {
static async delete (configMap: IStringKeyMap): Promise<boolean> {
try {
return await removeFileFromDogeInMain(getRawData(configMap))
} catch (error: any) {
deleteFailedLog(configMap.fileName, 'DogeCloud', error)
return false
}
}
}

View File

@@ -0,0 +1,53 @@
import { Octokit } from '@octokit/rest'
import { IGitHubConfig, PartialKeys } from '#/types/types'
import { deleteFailedLog, deleteLog } from '~/utils/deleteLog'
interface IConfigMap {
fileName: string
hash: string
config: PartialKeys<IGitHubConfig, 'path'>
}
export default class GithubApi {
static #createOctokit (token: string) {
return new Octokit({
auth: token
})
}
static #createKey (path: string | undefined, fileName: string): string {
const formatedFileName = fileName.replace(/%2F/g, '/')
return path && path !== '/' ? `${path.replace(/^\/+|\/+$/, '')}/${formatedFileName}` : formatedFileName
}
static async delete (configMap: IConfigMap): Promise<boolean> {
const {
fileName,
hash,
config: { repo, token, branch, path }
} = configMap
const [owner, repoName] = repo.split('/')
const octokit = GithubApi.#createOctokit(token)
const key = GithubApi.#createKey(path, fileName)
try {
const { status } = await octokit.rest.repos.deleteFile({
owner,
repo: repoName,
path: key,
message: `delete ${fileName} by PicList`,
sha: hash,
branch
})
if (status === 200) {
deleteLog(fileName, 'GitHub')
return true
}
deleteLog(fileName, 'GitHub', false)
return false
} catch (error: any) {
deleteFailedLog(fileName, 'GitHub', error)
return false
}
}
}

View File

@@ -0,0 +1,15 @@
import { getRawData } from '@/utils/common'
import { IStringKeyMap } from '#/types/types'
import { removeFileFromHuaweiInMain } from '~/utils/deleteFunc'
import { deleteFailedLog } from '~/utils/deleteLog'
export default class HuaweicloudApi {
static async delete (configMap: IStringKeyMap): Promise<boolean> {
try {
return await removeFileFromHuaweiInMain(getRawData(configMap))
} catch (error: any) {
deleteFailedLog(configMap.fileName, 'HuaweiCloud', error)
return false
}
}
}

View File

@@ -0,0 +1,44 @@
import axios, { AxiosResponse } from 'axios'
import { IImgurConfig } from '#/types/types'
import { deleteFailedLog, deleteLog } from '~/utils/deleteLog'
interface IConfigMap {
config?: Partial<IImgurConfig>
hash?: string
}
export default class ImgurApi {
static #baseUrl = 'https://api.imgur.com/3'
static async delete (configMap: IConfigMap): Promise<boolean> {
const { config: { clientId = '', username = '', accessToken = '' } = {}, hash = '' } = configMap
let Authorization: string, apiUrl: string
if (username && accessToken) {
Authorization = `Bearer ${accessToken}`
apiUrl = `${ImgurApi.#baseUrl}/account/${username}/image/${hash}`
} else if (clientId) {
Authorization = `Client-ID ${clientId}`
apiUrl = `${ImgurApi.#baseUrl}/image/${hash}`
} else {
deleteLog(hash, 'Imgur', false, 'No credentials found')
return false
}
try {
const response: AxiosResponse = await axios.delete(apiUrl, {
headers: { Authorization },
timeout: 30000
})
if (response.status === 200) {
deleteLog(hash, 'Imgur')
return true
}
deleteLog(hash, 'Imgur', false)
return false
} catch (error: any) {
deleteFailedLog(hash, 'Imgur', error)
return false
}
}
}

View File

@@ -0,0 +1,26 @@
import fs from 'fs-extra'
import { deleteFailedLog, deleteLog } from '~/utils/deleteLog'
interface IConfigMap {
hash: string
}
export default class LocalApi {
static async delete (configMap: IConfigMap): Promise<boolean> {
const { hash } = configMap
if (!hash) {
deleteLog(hash, 'Local', false, 'Local.delete: invalid params')
return false
}
try {
await fs.remove(hash)
deleteLog(hash, 'Local')
return true
} catch (error: any) {
deleteFailedLog(hash, 'Local', error)
return false
}
}
}

View File

@@ -0,0 +1,47 @@
import https from 'node:https'
import axios, { AxiosResponse } from 'axios'
import { IStringKeyMap } from '#/types/types'
import { deleteFailedLog, deleteLog } from '~/utils/deleteLog'
export default class LskyplistApi {
static async delete (configMap: IStringKeyMap): Promise<boolean> {
const { hash, config } = configMap
if (!hash || !config || !config.token) {
deleteLog(hash, 'Lskyplist', false, 'LskyplistApi.delete: invalid params')
return false
}
const { host, token, version } = config
if (version !== 'V2') {
deleteLog(hash, 'Lskyplist', false, 'LskyplistApi.delete: invalid version')
return false
}
const v2Headers = {
Accept: 'application/json',
Authorization: token || undefined
}
const requestAgent = new https.Agent({
rejectUnauthorized: false
})
try {
const response: AxiosResponse = await axios.delete(`${host}/api/v1/images/${hash}`, {
headers: v2Headers,
timeout: 30000,
httpsAgent: requestAgent
})
if (response.status === 200 && response.data.status === true) {
deleteLog(hash, 'Lskyplist')
return true
}
deleteLog(hash, 'Lskyplist', false)
return false
} catch (error: any) {
deleteFailedLog(hash, 'Lskyplist', error)
return false
}
}
}

View File

@@ -0,0 +1,34 @@
import axios, { AxiosResponse } from 'axios'
import { IStringKeyMap } from '#/types/types'
import { deleteFailedLog, deleteLog } from '~/utils/deleteLog'
export default class PiclistApi {
static async delete (configMap: IStringKeyMap): Promise<boolean> {
const { config, fullResult } = configMap
const { host, port } = config
if (!fullResult) return true
if (!host) {
deleteLog(fullResult, 'Piclist', false, 'PiclistApi.delete: invalid params')
return false
}
const url = `http://${host || '127.0.0.1'}:${port || 36677}/delete`
try {
const response: AxiosResponse = await axios.post(url, {
list: [fullResult]
})
if (response.status === 200 && response.data?.success) {
deleteLog(fullResult, 'Piclist')
return true
}
deleteLog(fullResult, 'Piclist', false)
return false
} catch (error: any) {
deleteFailedLog(fullResult, 'Piclist', error)
return false
}
}
}

View File

@@ -0,0 +1,44 @@
import { IQiniuConfig, PartialKeys } from '#/types/types'
import { deleteFailedLog, deleteLog } from '~/utils/deleteLog'
interface IConfigMap {
fileName: string
config: PartialKeys<IQiniuConfig, 'path'>
}
export default class QiniuApi {
static async delete (configMap: IConfigMap): Promise<boolean> {
const {
fileName,
config: { accessKey, secretKey, bucket, path }
} = configMap
const mac = new window.node.qiniu.auth.digest.Mac(accessKey, secretKey)
const qiniuConfig = new window.node.qiniu.conf.Config()
try {
const bucketManager = new window.node.qiniu.rs.BucketManager(mac, qiniuConfig)
const formattedPath = path?.replace(/^\/+|\/+$/, '') || ''
const key = path === '/' || !path ? fileName : `${formattedPath}/${fileName}`
const res = (await new Promise((resolve, reject) => {
bucketManager.delete(bucket, key, (err, respBody, respInfo) => {
if (err) {
reject(err)
} else {
resolve({
respBody,
respInfo
})
}
})
})) as any
if (res?.respInfo?.statusCode === 200) {
deleteLog(fileName, 'Qiniu')
return true
}
deleteLog(fileName, 'Qiniu', false)
return false
} catch (error: any) {
deleteFailedLog(fileName, 'Qiniu', error)
return false
}
}
}

View File

@@ -0,0 +1,16 @@
import { getRawData } from '@/utils/common'
import { IStringKeyMap } from '#/types/types'
import { removeFileFromSFTPInMain } from '~/utils/deleteFunc'
import { deleteFailedLog } from '~/utils/deleteLog'
export default class SftpPlistApi {
static async delete (configMap: IStringKeyMap): Promise<boolean> {
const { fileName, config } = configMap
try {
return await removeFileFromSFTPInMain(getRawData(config), fileName)
} catch (error: any) {
deleteFailedLog(fileName, 'SFTP', error)
return false
}
}
}

View File

@@ -0,0 +1,45 @@
import axios, { AxiosResponse } from 'axios'
import { ISMMSConfig } from '#/types/types'
import { deleteFailedLog, deleteLog } from '~/utils/deleteLog'
interface IConfigMap {
hash?: string
config?: Partial<ISMMSConfig>
}
export default class SmmsApi {
static readonly #baseUrl = 'https://smms.app/api/v2'
static async delete (configMap: IConfigMap): Promise<boolean> {
const { hash, config } = configMap
if (!hash || !config || !config.token) {
deleteLog(hash, 'Smms', false, 'SmmsApi.delete: invalid params')
return false
}
const { token } = config
try {
const response: AxiosResponse = await axios.get(`${SmmsApi.#baseUrl}/delete/${hash}`, {
headers: {
Authorization: token
},
params: {
hash,
format: 'json'
},
timeout: 30000
})
if (response.status === 200) {
deleteLog(hash, 'Smms')
return true
}
deleteLog(hash, 'Smms', false)
return false
} catch (error: any) {
deleteFailedLog(hash, 'Smms', error)
return false
}
}
}

View File

@@ -0,0 +1,46 @@
import COS from 'cos-nodejs-sdk-v5'
import { ITcYunConfig, PartialKeys } from '#/types/types'
import { deleteFailedLog, deleteLog } from '~/utils/deleteLog'
interface IConfigMap {
fileName: string
config: PartialKeys<ITcYunConfig, 'path'>
}
export default class TcyunApi {
static #createCOS (SecretId: string, SecretKey: string): COS {
return new COS({
SecretId,
SecretKey
})
}
static async delete (configMap: IConfigMap): Promise<boolean> {
const {
fileName,
config: { secretId, secretKey, bucket, area, path }
} = configMap
try {
const cos = TcyunApi.#createCOS(secretId, secretKey)
let key
if (path === '/' || !path) {
key = `/${fileName}`
} else {
key = `/${path.replace(/^\/+|\/+$/, '')}/${fileName}`
}
const result = await cos.deleteObject({
Bucket: bucket,
Region: area,
Key: key
})
if (result.statusCode === 204) {
deleteLog(fileName, 'Tcyun')
return true
}
deleteLog(fileName, 'Tcyun', false)
return false
} catch (error: any) {
deleteFailedLog(fileName, 'Tcyun', error)
return false
}
}
}

View File

@@ -0,0 +1,38 @@
import Upyun from 'upyun'
import { IUpYunConfig, PartialKeys } from '#/types/types'
import { deleteFailedLog, deleteLog } from '~/utils/deleteLog'
interface IConfigMap {
fileName: string
config: PartialKeys<IUpYunConfig, 'path'>
}
export default class UpyunApi {
static async delete (configMap: IConfigMap): Promise<boolean> {
const {
fileName,
config: { bucket, operator, password, path }
} = configMap
try {
const service = new Upyun.Service(bucket, operator, password)
const client = new Upyun.Client(service)
let key
if (path === '/' || !path) {
key = fileName
} else {
key = `${path.replace(/^\/+|\/+$/, '')}/${fileName}`
}
const result = await client.deleteFile(key)
if (result) {
deleteLog(fileName, 'Upyun')
return true
}
deleteLog(fileName, 'Upyun', false)
return false
} catch (error: any) {
deleteFailedLog(fileName, 'Upyun', error)
return false
}
}
}

View File

@@ -0,0 +1,42 @@
import { AuthType, createClient, WebDAVClientOptions } from 'webdav'
import { IWebdavPlistConfig, PartialKeys } from '#/types/types'
import { formatEndpoint } from '#/utils/common'
import { deleteFailedLog, deleteLog } from '~/utils/deleteLog'
interface IConfigMap {
fileName: string
config: PartialKeys<IWebdavPlistConfig, 'path'>
}
export default class WebdavApi {
static async delete (configMap: IConfigMap): Promise<boolean> {
const {
fileName,
config: { host, username, password, path, sslEnabled, authType }
} = configMap
const endpoint = formatEndpoint(host, sslEnabled)
const options: WebDAVClientOptions = {
username,
password
}
if (authType === 'digest') {
options.authType = AuthType.Digest
}
const ctx = createClient(endpoint, options)
let key
if (path === '/' || !path) {
key = fileName
} else {
key = `${path.replace(/^\/+|\/+$/, '')}/${fileName}`
}
try {
await ctx.deleteFile(key)
deleteLog(fileName, 'WebDAV')
return true
} catch (error: any) {
deleteFailedLog(fileName, 'WebDAV', error)
return false
}
}
}

View File

@@ -1,42 +1,38 @@
import { BrowserWindow, dialog, ipcMain, Notification } from 'electron'
import fs from 'fs-extra'
import { cloneDeep } from 'lodash'
import { DBStore } from '@picgo/store'
import { getWindowId, getSettingWindowId } from '@core/bus/apis'
import { getSettingWindowId, getWindowId } from '@core/bus/apis'
import db, { GalleryDB } from '@core/datastore'
import { dbPathChecker, defaultConfigPath, getGalleryDBPath } from '@core/datastore/dbChecker'
import { DBStore } from '@piclist/store'
import uploader from 'apis/app/uploader'
import { handleSecondaryUpload } from 'apis/app/uploader/apis'
import { BrowserWindow, dialog, ipcMain, IpcMainEvent, MessageBoxOptions, Notification } from 'electron'
import fs from 'fs-extra'
import { cloneDeep } from 'lodash-es'
import { SHOW_INPUT_BOX } from '#/events/constants'
import { IPasteStyle } from '#/types/enum'
import { IGuiApi, ImgInfo, IShowFileExplorerOption, IShowInputBoxOption, IShowMessageBoxOption, IShowMessageBoxResult, IShowNotificationOption, IUploadOption } from '#/types/types'
import { configPaths } from '#/utils/configPaths'
import { T } from '~/i18n'
import { handleCopyUrl } from '~/utils/common'
import pasteTemplate from '~/utils/pasteTemplate'
import { SHOW_INPUT_BOX } from '#/events/constants'
import { IPasteStyle } from '#/types/enum'
import { configPaths } from '#/utils/configPaths'
import { handleSecondaryUpload } from '../app/uploader/apis'
// Cross-process support may be required in the future
class GuiApi implements IGuiApi {
// eslint-disable-next-line no-use-before-define
private static instance: GuiApi
private windowId: number = -1
private settingWindowId: number = -1
private constructor() {
private constructor () {
console.log('init guiapi')
}
static getInstance(): GuiApi {
static getInstance (): GuiApi {
if (!GuiApi.instance) {
GuiApi.instance = new GuiApi()
}
return GuiApi.instance
}
private async showSettingWindow() {
private async showSettingWindow () {
this.settingWindowId = await getSettingWindowId()
const settingWindow = BrowserWindow.fromId(this.settingWindowId)
if (settingWindow?.isVisible()) {
@@ -50,11 +46,11 @@ class GuiApi implements IGuiApi {
})
}
private getWebcontentsByWindowId(id: number) {
private getWebcontentsByWindowId (id: number) {
return BrowserWindow.fromId(id)?.webContents
}
async showInputBox(
async showInputBox (
options: IShowInputBoxOption = {
title: '',
placeholder: ''
@@ -63,19 +59,19 @@ class GuiApi implements IGuiApi {
await this.showSettingWindow()
this.getWebcontentsByWindowId(this.settingWindowId)?.send(SHOW_INPUT_BOX, options)
return new Promise<string>(resolve => {
ipcMain.once(SHOW_INPUT_BOX, (_: Event, value: string) => {
ipcMain.once(SHOW_INPUT_BOX, (_: IpcMainEvent, value: string) => {
resolve(value)
})
})
}
async showFileExplorer(options: IShowFileExplorerOption = {}) {
async showFileExplorer (options: IShowFileExplorerOption = {}) {
this.windowId = await getWindowId()
const res = await dialog.showOpenDialog(BrowserWindow.fromId(this.windowId)!, options)
return res.filePaths || []
}
async upload(input: IUploadOption) {
async upload (input: IUploadOption) {
this.windowId = await getWindowId()
const webContents = this.getWebcontentsByWindowId(this.windowId)
const rawInput = cloneDeep(input)
@@ -126,7 +122,7 @@ class GuiApi implements IGuiApi {
return []
}
showNotification(
showNotification (
options: IShowNotificationOption = {
title: '',
body: ''
@@ -139,7 +135,7 @@ class GuiApi implements IGuiApi {
notification.show()
}
showMessageBox(
showMessageBox (
options: IShowMessageBoxOption = {
title: '',
message: '',
@@ -147,12 +143,14 @@ class GuiApi implements IGuiApi {
buttons: ['Yes', 'No']
}
) {
return new Promise<IShowMessageBoxResult>(async resolve => {
this.windowId = await getWindowId()
dialog.showMessageBox(BrowserWindow.fromId(this.windowId)!, options).then(res => {
resolve({
result: res.response,
checkboxChecked: res.checkboxChecked
return new Promise<IShowMessageBoxResult>(resolve => {
getWindowId().then(id => {
this.windowId = id
dialog.showMessageBox(BrowserWindow.fromId(id)!, options as MessageBoxOptions).then(res => {
resolve({
result: res.response,
checkboxChecked: res.checkboxChecked
})
})
})
})
@@ -161,7 +159,7 @@ class GuiApi implements IGuiApi {
/**
* get picgo config/data path
*/
async getConfigPath() {
async getConfigPath () {
const currentConfigPath = dbPathChecker()
const galleryDBPath = getGalleryDBPath().dbPath
return {
@@ -171,12 +169,12 @@ class GuiApi implements IGuiApi {
}
}
get galleryDB(): DBStore {
get galleryDB (): DBStore {
return new Proxy<DBStore>(GalleryDB.getInstance(), {
get(target, prop: keyof DBStore) {
get (target, prop: keyof DBStore) {
if (prop === 'overwrite') {
return new Proxy(GalleryDB.getInstance().overwrite, {
apply(target, ctx, args) {
apply (target, ctx, args) {
return new Promise(resolve => {
const guiApi = GuiApi.getInstance()
guiApi
@@ -199,7 +197,7 @@ class GuiApi implements IGuiApi {
}
if (prop === 'removeById') {
return new Proxy(GalleryDB.getInstance().removeById, {
apply(target, ctx, args) {
apply (target, ctx, args) {
return new Promise(resolve => {
const guiApi = GuiApi.getInstance()
guiApi