🐛 Fix(custom): fix memory leak issues

This commit is contained in:
Kuingsmile
2025-08-11 17:38:39 +08:00
parent 0bf435a0da
commit 1a35831301
16 changed files with 7505 additions and 7507 deletions

View File

@@ -1,96 +1,101 @@
import crypto from 'node:crypto' import crypto from 'node:crypto'
import path from 'node:path' import path from 'node:path'
import { clipboard, contextBridge, ipcRenderer, webFrame, webUtils } from 'electron' import { clipboard, contextBridge, ipcRenderer, IpcRendererEvent, webFrame, webUtils } from 'electron'
import fs from 'fs-extra' import fs from 'fs-extra'
import yaml from 'js-yaml' import yaml from 'js-yaml'
import mime from 'mime-types' import mime from 'mime-types'
import { isReactive, isRef, toRaw, unref } from 'vue' import { isReactive, isRef, toRaw, unref } from 'vue'
import type { IpcRendererListener } from '#/types/electron' export const getRawData = (args: any): any => {
if (isRef(args)) return unref(args)
export const getRawData = (args: any): any => { if (isReactive(args)) return toRaw(args)
if (isRef(args)) return unref(args) if (Array.isArray(args)) return args.map(getRawData)
if (isReactive(args)) return toRaw(args) if (typeof args === 'object' && args !== null) {
if (Array.isArray(args)) return args.map(getRawData) const data = {} as Record<string, any>
if (typeof args === 'object' && args !== null) { for (const key in args) {
const data = {} as Record<string, any> data[key] = getRawData(args[key])
for (const key in args) { }
data[key] = getRawData(args[key]) return data
} }
return data return args
} }
return args
} function sendToMain (channel: string, ...args: any[]) {
ipcRenderer.send(channel, ...getRawData(args))
function sendToMain (channel: string, ...args: any[]) { }
ipcRenderer.send(channel, ...getRawData(args))
} function sendRPC (action: string, ...args: any[]): void {
ipcRenderer.send('RPC_ACTIONS', action, getRawData(args))
function sendRPC (action: string, ...args: any[]): void { }
ipcRenderer.send('RPC_ACTIONS', action, getRawData(args))
} async function triggerRPC<T> (action: string, ...args: any[]): Promise<T | undefined> {
return await ipcRenderer.invoke('RPC_ACTIONS_INVOKE', action, getRawData(args))
async function triggerRPC<T> (action: string, ...args: any[]): Promise<T | undefined> { }
return await ipcRenderer.invoke('RPC_ACTIONS_INVOKE', action, getRawData(args))
} function sendRpcSync (action: string, ...args: any[]): any {
return ipcRenderer.sendSync('RPC_ACTIONS', action, getRawData(args))
function sendRpcSync (action: string, ...args: any[]): any { }
return ipcRenderer.sendSync('RPC_ACTIONS', action, getRawData(args))
} try {
contextBridge.exposeInMainWorld('electron', {
try { setVisualZoomLevelLimits: (min: number, max: number) => {
contextBridge.exposeInMainWorld('electron', { webFrame.setVisualZoomLevelLimits(min, max)
setVisualZoomLevelLimits: (min: number, max: number) => { },
webFrame.setVisualZoomLevelLimits(min, max) clipboard: {
}, writeText: clipboard.writeText
clipboard: { },
writeText: clipboard.writeText platform: process.platform,
}, sendRpcSync,
platform: process.platform, triggerRPC,
sendRpcSync, sendToMain,
triggerRPC, sendRPC,
sendToMain, ipcRendererOn: (channel: string, listener: (...args: any[]) => void) => {
sendRPC, const subscription = (_: IpcRendererEvent, ...args: any[]) => listener(...args)
ipcRendererOn: (channel: string, listener: IpcRendererListener) => { ipcRenderer.on(channel, subscription)
ipcRenderer.on(channel, listener) return () => {
}, ipcRenderer.removeListener(channel, subscription)
ipcRendererRemoveListener: (channel: string, listener: IpcRendererListener) => { }
ipcRenderer.removeListener(channel, listener) },
}, ipcRendererCountListeners: (channel: string): number => {
showFilePath (file: File) { return ipcRenderer.listenerCount(channel)
return webUtils.getPathForFile(file) },
} ipcRendererRemoveAllListeners: (channel: string) => {
}) ipcRenderer.removeAllListeners(channel)
},
contextBridge.exposeInMainWorld('node', { showFilePath (file: File) {
path: { return webUtils.getPathForFile(file)
join: path.join, }
dirname: path.dirname, })
basename: path.basename,
normalize: path.normalize, contextBridge.exposeInMainWorld('node', {
extname: path.extname, path: {
sep: path.sep, join: path.join,
posix: { dirname: path.dirname,
sep: path.posix.sep basename: path.basename,
} normalize: path.normalize,
}, extname: path.extname,
fs: { sep: path.sep,
remove: fs.remove, posix: {
readFile: fs.readFile, sep: path.posix.sep
statSync: fs.statSync }
}, },
crypto: { fs: {
randomBytes: crypto.randomBytes, remove: fs.remove,
createHash: crypto.createHash readFile: fs.readFile,
}, statSync: fs.statSync
yaml: { },
load: yaml.load crypto: {
}, randomBytes: crypto.randomBytes,
mime: { createHash: crypto.createHash
lookup: mime.lookup },
} yaml: {
}) load: yaml.load
} catch (error) { },
console.error(error) mime: {
} lookup: mime.lookup
}
})
} catch (error) {
console.error(error)
}

View File

@@ -50,7 +50,6 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import type { IpcRendererEvent } from 'electron'
import { X } from 'lucide-vue-next' import { X } from 'lucide-vue-next'
import { onBeforeMount, onBeforeUnmount, reactive, ref } from 'vue' import { onBeforeMount, onBeforeUnmount, reactive, ref } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
@@ -67,12 +66,9 @@ const inputBoxOptions = reactive({
placeholder: '' placeholder: ''
}) })
onBeforeMount(() => { let removeInputBoxListenerCallback: (() => void) = () => {}
window.electron.ipcRendererOn(SHOW_INPUT_BOX, ipcEventHandler)
$bus.on(SHOW_INPUT_BOX, initInputBoxValue)
})
function ipcEventHandler (_: IpcRendererEvent, options: IShowInputBoxOption) { function handleIpcInputBoxEvent (options: IShowInputBoxOption) {
initInputBoxValue(options) initInputBoxValue(options)
} }
@@ -96,16 +92,23 @@ function handleInputBoxConfirm () {
$bus.emit(SHOW_INPUT_BOX_RESPONSE, inputBoxValue.value) $bus.emit(SHOW_INPUT_BOX_RESPONSE, inputBoxValue.value)
} }
onBeforeMount(() => {
removeInputBoxListenerCallback = window.electron.ipcRendererOn(SHOW_INPUT_BOX, handleIpcInputBoxEvent)
$bus.on(SHOW_INPUT_BOX, initInputBoxValue)
})
onBeforeUnmount(() => { onBeforeUnmount(() => {
window.electron.ipcRendererRemoveListener(SHOW_INPUT_BOX, ipcEventHandler) removeInputBoxListenerCallback()
$bus.off(SHOW_INPUT_BOX) $bus.off(SHOW_INPUT_BOX)
}) })
</script> </script>
<script lang="ts"> <script lang="ts">
export default { export default {
name: 'InputBoxDialog' name: 'InputBoxDialog'
} }
</script> </script>
<style scoped> <style scoped>
.inputbox-overlay { .inputbox-overlay {
position: fixed; position: fixed;

View File

@@ -2,7 +2,10 @@
<nav class="navigation"> <nav class="navigation">
<div class="title-bar"> <div class="title-bar">
<div class="app-title"> <div class="app-title">
<div class="app-text"> <div
class="app-text"
@click="openGithubPage"
>
{{ t('app.title') }} {{ t('app.title') }}
</div> </div>
<div class="app-version"> <div class="app-version">
@@ -79,18 +82,6 @@
class="qr-dialog" class="qr-dialog"
@close="qrcodeVisible = false" @close="qrcodeVisible = false"
> >
<TransitionChild
as="template"
enter="duration-300 ease-out"
enter-from="opacity-0"
enter-to="opacity-100"
leave="duration-200 ease-in"
leave-from="opacity-100"
leave-to="opacity-0"
>
<div class="dialog-overlay" />
</TransitionChild>
<div class="dialog-container"> <div class="dialog-container">
<TransitionChild <TransitionChild
as="template" as="template"
@@ -217,7 +208,7 @@ import { pick } from 'lodash-es'
import { CheckIcon, ChevronDownIcon, CopyIcon, DatabaseIcon, FolderIcon, Info, PieChartIcon, PlugIcon, Settings, UploadIcon } from 'lucide-vue-next' import { CheckIcon, ChevronDownIcon, CopyIcon, DatabaseIcon, FolderIcon, Info, PieChartIcon, PlugIcon, Settings, UploadIcon } from 'lucide-vue-next'
import QrcodeVue from 'qrcode.vue' import QrcodeVue from 'qrcode.vue'
import pkg from 'root/package.json' import pkg from 'root/package.json'
import { computed, nextTick, onBeforeMount, reactive, Ref, ref, watch } from 'vue' import { computed, nextTick, onBeforeMount, onBeforeUnmount, reactive, Ref, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import useMessage from '@/hooks/useMessage' import useMessage from '@/hooks/useMessage'
@@ -237,6 +228,8 @@ const qrcodeVisible = ref(false)
const choosedPicBedForQRCode: Ref<string[]> = ref([]) const choosedPicBedForQRCode: Ref<string[]> = ref([])
const picBedConfigString = ref('') const picBedConfigString = ref('')
let removeIpcListener: () => void = () => {}
watch( watch(
() => choosedPicBedForQRCode, () => choosedPicBedForQRCode,
val => { val => {
@@ -280,9 +273,17 @@ const navigationItems = computed(() => [
} }
]) ])
function openGithubPage () {
window.electron.sendRPC(IRPCActionType.OPEN_URL, 'https://github.com/Kuingsmile/PicList')
}
onBeforeMount(() => { onBeforeMount(() => {
updatePicBedGlobal() updatePicBedGlobal()
window.electron.ipcRendererOn(SHOW_MAIN_PAGE_QRCODE, qrCodeHandler) removeIpcListener = window.electron.ipcRendererOn(SHOW_MAIN_PAGE_QRCODE, qrCodeHandler)
})
onBeforeUnmount(() => {
removeIpcListener()
}) })
</script> </script>
@@ -333,6 +334,11 @@ onBeforeMount(() => {
letter-spacing: -0.025em; letter-spacing: -0.025em;
} }
.app-text:hover {
cursor: pointer;
color: var(--color-blue-common);
}
.app-version { .app-version {
font-size: 10px; font-size: 10px;
font-weight: 500; font-weight: 500;
@@ -536,16 +542,11 @@ onBeforeMount(() => {
inset: 0; inset: 0;
z-index: 50; z-index: 50;
display: flex; display: flex;
overflow-y: auto;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
} }
.dialog-overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.5);
}
.dialog-container { .dialog-container {
position: fixed; position: fixed;
inset: 0; inset: 0;
@@ -561,7 +562,7 @@ onBeforeMount(() => {
.dialog-panel { .dialog-panel {
width: 100%; width: 100%;
max-width: 500px; max-width: 500px;
background: var(--color-surface); background: var(--color-background-primary);
border-radius: 16px; border-radius: 16px;
border: 1px solid var(--color-border); border: 1px solid var(--color-border);
box-shadow: var(--shadow-md); box-shadow: var(--shadow-md);
@@ -634,11 +635,11 @@ onBeforeMount(() => {
right: 0; right: 0;
z-index: 10; z-index: 10;
margin-top: 4px; margin-top: 4px;
background: var(--color-surface); background: var(--color-background-primary);
border: 1px solid var(--color-border); border: 1px solid var(--color-border);
border-radius: var(--border-radius); border-radius: var(--border-radius);
box-shadow: var(--shadow-md); box-shadow: var(--shadow-md);
max-height: 200px; max-height: 300px;
overflow-y: auto; overflow-y: auto;
} }

View File

@@ -74,7 +74,6 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import type { IpcRendererEvent } from 'electron'
import { MinusIcon, PinIcon, ShrinkIcon, XIcon } from 'lucide-vue-next' import { MinusIcon, PinIcon, ShrinkIcon, XIcon } from 'lucide-vue-next'
import { onBeforeMount, onBeforeUnmount, ref } from 'vue' import { onBeforeMount, onBeforeUnmount, ref } from 'vue'
@@ -100,7 +99,8 @@ function openMiniWindow () {
function closeWindow () { function closeWindow () {
window.electron.sendRPC(IRPCActionType.CLOSE_WINDOW) window.electron.sendRPC(IRPCActionType.CLOSE_WINDOW)
} }
const uploadProcessHandler = (_event: IpcRendererEvent, data: { progress: number }) => {
const uploadProcessHandler = (data: { progress: number }) => {
isShowprogress.value = data.progress !== 100 && data.progress !== 0 isShowprogress.value = data.progress !== 100 && data.progress !== 0
progress.value = data.progress progress.value = data.progress
} }
@@ -110,7 +110,7 @@ onBeforeMount(() => {
}) })
onBeforeUnmount(() => { onBeforeUnmount(() => {
window.electron.ipcRendererRemoveListener('updateProgress', uploadProcessHandler) window.electron.ipcRendererRemoveAllListeners('updateProgress')
}) })
</script> </script>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,243 +1,244 @@
<template> <template>
<div id="mini-page"> <div id="mini-page">
<div <div
id="upload-area" id="upload-area"
:class="{ :class="{
'is-dragover': dragover, 'is-dragover': dragover,
uploading: isShowingProgress, uploading: isShowingProgress,
linux: osGlobal === 'linux' linux: osGlobal === 'linux'
}" }"
:style="{ backgroundPosition: '0 ' + progress + '%' }" :style="{ backgroundPosition: '0 ' + progress + '%' }"
@drop.prevent="onDrop" @drop.prevent="onDrop"
@dragover.prevent="dragover = true" @dragover.prevent="dragover = true"
@dragleave.prevent="dragover = false" @dragleave.prevent="dragover = false"
> >
<img <img
v-if="!dragover && !isShowingProgress" v-if="!dragover && !isShowingProgress"
:src="logoPath ? logoPath : '/squareLogo.png'" :src="logoPath ? logoPath : '/squareLogo.png'"
style="width: 100%; height: 100%; border-radius: 50%" style="width: 100%; height: 100%; border-radius: 50%"
draggable="false" draggable="false"
@dragstart.prevent @dragstart.prevent
> >
<div <div
id="upload-dragger" id="upload-dragger"
@dblclick="openUploadWindow" @dblclick="openUploadWindow"
> >
<input <input
id="file-uploader" id="file-uploader"
type="file" type="file"
multiple multiple
@change="onChange" @change="onChange"
> >
</div> </div>
</div> </div>
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import type { IpcRendererEvent } from 'electron' import type { IConfig } from 'piclist'
import type { IConfig } from 'piclist' import { onBeforeMount, onBeforeUnmount, ref, watch } from 'vue'
import { onBeforeMount, onBeforeUnmount, ref, watch } from 'vue'
import { isUrl } from '@/utils/common'
import { isUrl } from '@/utils/common' import { getConfig } from '@/utils/dataSender'
import { getConfig } from '@/utils/dataSender' import { IRPCActionType } from '@/utils/enum'
import { IRPCActionType } from '@/utils/enum' import { osGlobal } from '@/utils/global'
import { osGlobal } from '@/utils/global' import type { IFileWithPath } from '#/types/types'
import type { IFileWithPath } from '#/types/types'
const logoPath = ref('')
const logoPath = ref('') const dragover = ref(false)
const dragover = ref(false) const progress = ref(0)
const progress = ref(0) const isShowingProgress = ref(false)
const isShowingProgress = ref(false) const draggingState = ref(false)
const draggingState = ref(false) const wX = ref(-1)
const wX = ref(-1) const wY = ref(-1)
const wY = ref(-1) const screenX = ref(-1)
const screenX = ref(-1) const screenY = ref(-1)
const screenY = ref(-1)
let removeListeners: () => void = () => {}
async function initLogoPath () {
const config = await getConfig<IConfig>() async function initLogoPath () {
if (config) { const config = await getConfig<IConfig>()
if (config.settings?.isCustomMiniIcon && config.settings?.customMiniIcon) { if (config) {
logoPath.value = if (config.settings?.isCustomMiniIcon && config.settings?.customMiniIcon) {
'data:image/jpg;base64,' + logoPath.value =
(await window.electron.triggerRPC(IRPCActionType.MANAGE_CONVERT_PATH_TO_BASE64, config.settings.customMiniIcon)) 'data:image/jpg;base64,' +
} (await window.electron.triggerRPC(IRPCActionType.MANAGE_CONVERT_PATH_TO_BASE64, config.settings.customMiniIcon))
} }
} }
}
const uploadProgressHandler = (_: IpcRendererEvent, _progress: number) => {
if (_progress !== -1) { const uploadProgressHandler = (p: number) => {
isShowingProgress.value = true if (p !== -1) {
progress.value = _progress isShowingProgress.value = true
} else { progress.value = p
progress.value = 100 } else {
} progress.value = 100
} }
}
const updateMiniIconHandler = async () => {
await initLogoPath() const updateMiniIconHandler = async () => {
} await initLogoPath()
}
onBeforeMount(async () => {
await initLogoPath() watch(progress, val => {
window.electron.ipcRendererOn('uploadProgress', uploadProgressHandler) if (val === 100) {
window.electron.ipcRendererOn('updateMiniIcon', updateMiniIconHandler) setTimeout(() => {
window.addEventListener('mousedown', handleMouseDown, false) isShowingProgress.value = false
window.addEventListener('mousemove', handleMouseMove, false) }, 1000)
window.addEventListener('mouseup', handleMouseUp, false) setTimeout(() => {
}) progress.value = 0
}, 1200)
watch(progress, val => { }
if (val === 100) { })
setTimeout(() => {
isShowingProgress.value = false function onDrop (e: DragEvent) {
}, 1000) dragover.value = false
setTimeout(() => {
progress.value = 0 // send files first
}, 1200) if (e.dataTransfer?.files?.length) {
} ipcSendFiles(e.dataTransfer.files)
}) } else if (e.dataTransfer?.items) {
const items = e.dataTransfer.items
function onDrop (e: DragEvent) { if (items.length === 2 && items[0].type === 'text/uri-list') {
dragover.value = false handleURLDrag(items, e.dataTransfer)
} else if (items[0].type === 'text/plain') {
// send files first const str = e.dataTransfer!.getData(items[0].type)
if (e.dataTransfer?.files?.length) { if (isUrl(str)) {
ipcSendFiles(e.dataTransfer.files) window.electron.sendRPC(IRPCActionType.UPLOAD_CHOOSED_FILES, [{ path: str }])
} 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) function handleURLDrag (items: DataTransferItemList, dataTransfer: DataTransfer) {
if (isUrl(str)) { // text/html
window.electron.sendRPC(IRPCActionType.UPLOAD_CHOOSED_FILES, [{ path: str }]) // 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, [
{
function handleURLDrag (items: DataTransferItemList, dataTransfer: DataTransfer) { path: urlMatch[1]
// 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, [ function openUploadWindow () {
{ // @ts-expect-error file-uploader
path: urlMatch[1] document.getElementById('file-uploader').click()
} }
])
} function onChange (e: any) {
} ipcSendFiles(e.target.files)
// @ts-expect-error file-uploader
function openUploadWindow () { document.getElementById('file-uploader').value = ''
// @ts-expect-error file-uploader }
document.getElementById('file-uploader').click()
} function ipcSendFiles (files: FileList) {
const sendFiles: IFileWithPath[] = []
function onChange (e: any) { Array.from(files).forEach(item => {
ipcSendFiles(e.target.files) const obj = {
// @ts-expect-error file-uploader name: item.name,
document.getElementById('file-uploader').value = '' path: window.electron.showFilePath(item)
} }
sendFiles.push(obj)
function ipcSendFiles (files: FileList) { })
const sendFiles: IFileWithPath[] = [] window.electron.sendRPC(IRPCActionType.UPLOAD_CHOOSED_FILES, sendFiles)
Array.from(files).forEach(item => { }
const obj = {
name: item.name, function handleMouseDown (e: MouseEvent) {
path: window.electron.showFilePath(item) draggingState.value = true
} wX.value = e.pageX
sendFiles.push(obj) wY.value = e.pageY
}) screenX.value = e.screenX
window.electron.sendRPC(IRPCActionType.UPLOAD_CHOOSED_FILES, sendFiles) screenY.value = e.screenY
} }
function handleMouseDown (e: MouseEvent) { function handleMouseMove (e: MouseEvent) {
draggingState.value = true e.preventDefault()
wX.value = e.pageX e.stopPropagation()
wY.value = e.pageY if (draggingState.value) {
screenX.value = e.screenX const xLoc = e.screenX - wX.value
screenY.value = e.screenY const yLoc = e.screenY - wY.value
} window.electron.sendRPC(IRPCActionType.SET_MINI_WINDOW_POS, {
x: xLoc,
function handleMouseMove (e: MouseEvent) { y: yLoc,
e.preventDefault() width: 64,
e.stopPropagation() height: 64
if (draggingState.value) { })
const xLoc = e.screenX - wX.value }
const yLoc = e.screenY - wY.value }
window.electron.sendRPC(IRPCActionType.SET_MINI_WINDOW_POS, {
x: xLoc, function handleMouseUp (e: MouseEvent) {
y: yLoc, draggingState.value = false
width: 64, if (screenX.value === e.screenX && screenY.value === e.screenY) {
height: 64 if (e.button === 0) {
}) // left mouse
} openUploadWindow()
} } else {
openContextMenu()
function handleMouseUp (e: MouseEvent) { }
draggingState.value = false }
if (screenX.value === e.screenX && screenY.value === e.screenY) { }
if (e.button === 0) {
// left mouse function openContextMenu () {
openUploadWindow() window.electron.sendRPC(IRPCActionType.SHOW_MINI_PAGE_MENU)
} else { }
openContextMenu()
} onBeforeMount(async () => {
} await initLogoPath()
} removeListeners = window.electron.ipcRendererOn('uploadProgress', uploadProgressHandler)
window.electron.ipcRendererOn('updateMiniIcon', updateMiniIconHandler)
function openContextMenu () { window.addEventListener('mousedown', handleMouseDown, false)
window.electron.sendRPC(IRPCActionType.SHOW_MINI_PAGE_MENU) window.addEventListener('mousemove', handleMouseMove, false)
} window.addEventListener('mouseup', handleMouseUp, false)
})
onBeforeUnmount(() => {
window.electron.ipcRendererRemoveListener('uploadProgress', uploadProgressHandler) onBeforeUnmount(() => {
window.electron.ipcRendererRemoveListener('updateMiniIcon', updateMiniIconHandler) removeListeners()
window.removeEventListener('mousedown', handleMouseDown, false) window.electron.ipcRendererRemoveAllListeners('updateMiniIcon')
window.removeEventListener('mousemove', handleMouseMove, false) window.removeEventListener('mousedown', handleMouseDown, false)
window.removeEventListener('mouseup', handleMouseUp, false) window.removeEventListener('mousemove', handleMouseMove, false)
}) window.removeEventListener('mouseup', handleMouseUp, false)
</script> })
<script lang="ts"> </script>
export default { <script lang="ts">
name: 'MiniPage' export default {
} name: 'MiniPage'
</script> }
<style lang="stylus"> </script>
#mini-page <style lang="stylus">
color #FFF #mini-page
height 100vh color #FFF
width 100vw height 100vh
border-radius 50% width 100vw
text-align center border-radius 50%
line-height 100vh text-align center
font-size 40px line-height 100vh
background-size 90vh 90vw font-size 40px
background-position center center background-size 90vh 90vw
background-repeat no-repeat background-position center center
position relative background-repeat no-repeat
box-sizing border-box position relative
cursor pointer box-sizing border-box
&.linux cursor pointer
border-radius 0 &.linux
background-size 100vh 100vw border-radius 0
#upload-area background-size 100vh 100vw
height 100% #upload-area
width 100% height 100%
border-radius 50% width 100%
&.linux border-radius 50%
border-radius 0 &.linux
&.uploading border-radius 0
background: linear-gradient(to top, #409EFF 50%, #fff 51%) &.uploading
background-size 200% background: linear-gradient(to top, #409EFF 50%, #fff 51%)
#upload-dragger background-size 200%
height 100% #upload-dragger
&.is-dragover height 100%
background rgba(0,0,0,0.3) &.is-dragover
#file-uploader background rgba(0,0,0,0.3)
display none #file-uploader
</style> display none
</style>

File diff suppressed because it is too large Load Diff

View File

@@ -1,369 +1,369 @@
<template> <template>
<div class="rename-container"> <div class="rename-container">
<div class="rename-card"> <div class="rename-card">
<form @submit.prevent="confirmName"> <form @submit.prevent="confirmName">
<div class="form-content"> <div class="form-content">
<div class="form-group"> <div class="form-group">
<div class="input-wrapper"> <div class="input-wrapper">
<input <input
ref="fileNameInput" ref="fileNameInput"
v-model="form.fileName" v-model="form.fileName"
type="text" type="text"
class="form-input" class="form-input"
:class="{ 'input-error': validationError }" :class="{ 'input-error': validationError }"
:placeholder="t('pages.rename.placeholder')" :placeholder="t('pages.rename.placeholder')"
autofocus autofocus
@keyup.enter="confirmName" @keyup.enter="confirmName"
@input="clearValidationError" @input="clearValidationError"
> >
<button <button
v-if="form.fileName" v-if="form.fileName"
type="button" type="button"
class="input-clear" class="input-clear"
@click="clearFileName" @click="clearFileName"
> >
<XIcon :size="16" /> <XIcon :size="16" />
</button> </button>
</div> </div>
<div <div
v-if="validationError" v-if="validationError"
class="validation-error" class="validation-error"
> >
{{ validationError }} {{ validationError }}
</div> </div>
</div> </div>
</div> </div>
<!-- Actions --> <!-- Actions -->
<div class="form-actions"> <div class="form-actions">
<button <button
type="button" type="button"
class="btn btn-secondary" class="btn btn-secondary"
@click="cancel" @click="cancel"
> >
{{ $t('common.cancel') }} {{ $t('common.cancel') }}
</button> </button>
<button <button
type="submit" type="submit"
class="btn btn-primary" class="btn btn-primary"
:disabled="!form.fileName.trim()" :disabled="!form.fileName.trim()"
> >
{{ $t('common.confirm') }} {{ $t('common.confirm') }}
</button> </button>
</div> </div>
</form> </form>
</div> </div>
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import type { IpcRendererEvent } from 'electron' import { XIcon } from 'lucide-vue-next'
import { XIcon } from 'lucide-vue-next' import { nextTick, onBeforeMount, onBeforeUnmount, reactive, ref } from 'vue'
import { nextTick, onBeforeMount, onBeforeUnmount, reactive, ref } from 'vue' import { useI18n } from 'vue-i18n'
import { useI18n } from 'vue-i18n'
import { GET_RENAME_FILE_NAME, RENAME_FILE_NAME } from '@/utils/constant'
import { GET_RENAME_FILE_NAME, RENAME_FILE_NAME } from '@/utils/constant'
const { t } = useI18n()
const { t } = useI18n() const id = ref<string | null>(null)
const id = ref<string | null>(null) const fileNameInput = ref<HTMLInputElement>()
const fileNameInput = ref<HTMLInputElement>() const validationError = ref<string>('')
const validationError = ref<string>('')
const form = reactive({
const form = reactive({ fileName: '',
fileName: '', originName: ''
originName: '' })
})
const handleFileName = (newName: string, _originName: string, _id: string) => {
const handleFileName = (_: IpcRendererEvent, newName: string, _originName: string, _id: string) => { form.fileName = newName
form.fileName = newName form.originName = _originName
form.originName = _originName id.value = _id
id.value = _id nextTick(() => {
nextTick(() => { fileNameInput.value?.focus()
fileNameInput.value?.focus() fileNameInput.value?.select()
fileNameInput.value?.select() })
}) }
}
window.electron.ipcRendererOn(RENAME_FILE_NAME, handleFileName)
window.electron.ipcRendererOn(RENAME_FILE_NAME, handleFileName)
function validateFileName (fileName: string): string {
onBeforeMount(() => { if (!fileName.trim()) {
window.electron.sendToMain(GET_RENAME_FILE_NAME, '') return 'File name is required'
}) }
const invalidChars = /[<>:"/\\|?*]/g
function validateFileName (fileName: string): string { if (invalidChars.test(fileName)) {
if (!fileName.trim()) { return 'File name contains invalid characters'
return 'File name is required' }
} const reservedNames = /^(CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])$/i
const invalidChars = /[<>:"/\\|?*]/g if (reservedNames.test(fileName.trim())) {
if (invalidChars.test(fileName)) { return 'This is a reserved file name'
return 'File name contains invalid characters' }
} return ''
const reservedNames = /^(CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])$/i }
if (reservedNames.test(fileName.trim())) {
return 'This is a reserved file name' function confirmName () {
} const error = validateFileName(form.fileName)
return '' if (error) {
} validationError.value = error
return
function confirmName () { }
const error = validateFileName(form.fileName)
if (error) { window.electron.sendToMain(`${RENAME_FILE_NAME}${id.value}`, form.fileName)
validationError.value = error }
return
} function cancel () {
window.electron.sendToMain(`${RENAME_FILE_NAME}${id.value}`, form.originName)
window.electron.sendToMain(`${RENAME_FILE_NAME}${id.value}`, form.fileName) }
}
function clearFileName () {
function cancel () { form.fileName = ''
window.electron.sendToMain(`${RENAME_FILE_NAME}${id.value}`, form.originName) validationError.value = ''
} nextTick(() => {
fileNameInput.value?.focus()
function clearFileName () { })
form.fileName = '' }
validationError.value = ''
nextTick(() => { function clearValidationError () {
fileNameInput.value?.focus() if (validationError.value) {
}) validationError.value = ''
} }
}
function clearValidationError () {
if (validationError.value) { onBeforeMount(() => {
validationError.value = '' window.electron.sendToMain(GET_RENAME_FILE_NAME, '')
} })
}
onBeforeUnmount(() => {
onBeforeUnmount(() => { window.electron.ipcRendererRemoveAllListeners(RENAME_FILE_NAME)
window.electron.ipcRendererRemoveListener(RENAME_FILE_NAME, handleFileName) })
})
</script> </script>
<script lang="ts"> <script lang="ts">
export default { export default {
name: 'RenamePage' name: 'RenamePage'
} }
</script> </script>
<style scoped> <style scoped>
.rename-container { .rename-container {
padding: 2rem; padding: 2rem;
min-height: 100vh; min-height: 100vh;
background: var(--color-background-secondary); background: var(--color-background-secondary);
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
} }
.rename-card { .rename-card {
background: var(--color-background-primary); background: var(--color-background-primary);
border-radius: 12px; border-radius: 12px;
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04); box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
border: 1px solid var(--color-border); border: 1px solid var(--color-border);
width: 100%; width: 100%;
max-width: 500px; max-width: 500px;
overflow: hidden; overflow: hidden;
} }
/* Form */ /* Form */
.form-content { .form-content {
padding: 2rem; padding: 2rem;
} }
.form-group { .form-group {
margin-bottom: 1.5rem; margin-bottom: 1.5rem;
} }
.form-group:last-child { .form-group:last-child {
margin-bottom: 0; margin-bottom: 0;
} }
.form-label { .form-label {
display: block; display: block;
margin-bottom: 0.75rem; margin-bottom: 0.75rem;
font-size: 0.875rem; font-size: 0.875rem;
font-weight: 500; font-weight: 500;
color: var(--color-text-primary); color: var(--color-text-primary);
} }
.input-wrapper { .input-wrapper {
position: relative; position: relative;
display: flex; display: flex;
align-items: center; align-items: center;
} }
.form-input { .form-input {
width: 100%; width: 100%;
padding: 0.875rem 1rem; padding: 0.875rem 1rem;
padding-right: 2.5rem; padding-right: 2.5rem;
border: 1px solid var(--color-border); border: 1px solid var(--color-border);
border-radius: 8px; border-radius: 8px;
background: var(--color-background-primary); background: var(--color-background-primary);
color: var(--color-text-primary); color: var(--color-text-primary);
font-size: 0.875rem; font-size: 0.875rem;
transition: all 0.2s ease; transition: all 0.2s ease;
box-sizing: border-box; box-sizing: border-box;
} }
.form-input:focus { .form-input:focus {
outline: none; outline: none;
border-color: var(--color-blue-common); border-color: var(--color-blue-common);
box-shadow: 0 0 0 2px rgba(64, 158, 255, 0.2); box-shadow: 0 0 0 2px rgba(64, 158, 255, 0.2);
} }
.form-input.input-error { .form-input.input-error {
border-color: #f56c6c; border-color: #f56c6c;
box-shadow: 0 0 0 2px rgba(245, 108, 108, 0.2); box-shadow: 0 0 0 2px rgba(245, 108, 108, 0.2);
} }
.input-clear { .input-clear {
position: absolute; position: absolute;
right: 0.75rem; right: 0.75rem;
top: 50%; top: 50%;
transform: translateY(-50%); transform: translateY(-50%);
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
width: 24px; width: 24px;
height: 24px; height: 24px;
border-radius: 4px; border-radius: 4px;
border: none; border: none;
background: var(--color-background-tertiary); background: var(--color-background-tertiary);
color: var(--color-text-secondary); color: var(--color-text-secondary);
cursor: pointer; cursor: pointer;
transition: all 0.2s ease; transition: all 0.2s ease;
} }
.input-clear:hover { .input-clear:hover {
background: var(--color-background-secondary); background: var(--color-background-secondary);
color: var(--color-text-primary); color: var(--color-text-primary);
} }
.validation-error { .validation-error {
margin-top: 0.5rem; margin-top: 0.5rem;
font-size: 0.75rem; font-size: 0.75rem;
color: #f56c6c; color: #f56c6c;
display: flex; display: flex;
align-items: center; align-items: center;
gap: 0.25rem; gap: 0.25rem;
} }
/* Actions */ /* Actions */
.form-actions { .form-actions {
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;
gap: 0.75rem; gap: 0.75rem;
padding: 1rem 2rem 2rem; padding: 1rem 2rem 2rem;
background: var(--color-background-tertiary); background: var(--color-background-tertiary);
} }
/* Buttons */ /* Buttons */
.btn { .btn {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
gap: 0.5rem; gap: 0.5rem;
padding: 0.75rem 1.5rem; padding: 0.75rem 1.5rem;
border-radius: 8px; border-radius: 8px;
border: none; border: none;
font-size: 0.875rem; font-size: 0.875rem;
font-weight: 500; font-weight: 500;
cursor: pointer; cursor: pointer;
transition: all 0.2s ease; transition: all 0.2s ease;
min-width: fit-content; min-width: fit-content;
} }
.btn:disabled { .btn:disabled {
opacity: 0.5; opacity: 0.5;
cursor: not-allowed; cursor: not-allowed;
} }
.btn:hover:not(:disabled) { .btn:hover:not(:disabled) {
transform: translateY(-1px); transform: translateY(-1px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15); box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
} }
.btn-primary { .btn-primary {
background: #409eff; background: #409eff;
color: white; color: white;
} }
.btn-primary:hover:not(:disabled) { .btn-primary:hover:not(:disabled) {
background: #66b1ff; background: #66b1ff;
} }
.btn-secondary { .btn-secondary {
background: var(--color-background-primary); background: var(--color-background-primary);
color: var(--color-text-primary); color: var(--color-text-primary);
border: 1px solid var(--color-border); border: 1px solid var(--color-border);
} }
.btn-secondary:hover:not(:disabled) { .btn-secondary:hover:not(:disabled) {
background: var(--color-background-secondary); background: var(--color-background-secondary);
border-color: var(--color-accent); border-color: var(--color-accent);
} }
/* Responsive Design */ /* Responsive Design */
@media (max-width: 768px) { @media (max-width: 768px) {
.rename-container { .rename-container {
padding: 1rem; padding: 1rem;
} }
.rename-card { .rename-card {
max-width: none; max-width: none;
} }
.form-actions { .form-actions {
padding: 1rem 1.5rem 1.5rem; padding: 1rem 1.5rem 1.5rem;
flex-direction: column-reverse; flex-direction: column-reverse;
} }
.btn { .btn {
width: 100%; width: 100%;
justify-content: center; justify-content: center;
} }
} }
@media (max-width: 480px) { @media (max-width: 480px) {
.rename-container { .rename-container {
padding: 0.75rem; padding: 0.75rem;
} }
.form-actions { .form-actions {
padding: 1rem; padding: 1rem;
} }
} }
/* Focus styles for accessibility */ /* Focus styles for accessibility */
.btn:focus-visible, .btn:focus-visible,
.input-clear:focus-visible { .input-clear:focus-visible {
outline: 2px solid var(--color-accent); outline: 2px solid var(--color-accent);
outline-offset: 2px; outline-offset: 2px;
} }
.form-input:focus-visible { .form-input:focus-visible {
outline: 2px solid var(--color-accent); outline: 2px solid var(--color-accent);
outline-offset: 2px; outline-offset: 2px;
} }
/* Animation for error state */ /* Animation for error state */
@keyframes shake { @keyframes shake {
0%, 100% { 0%, 100% {
transform: translateX(0); transform: translateX(0);
} }
25% { 25% {
transform: translateX(-4px); transform: translateX(-4px);
} }
75% { 75% {
transform: translateX(4px); transform: translateX(4px);
} }
} }
.input-error { .input-error {
animation: shake 0.3s ease-in-out; animation: shake 0.3s ease-in-out;
} }
/* Dark mode adjustments */ /* Dark mode adjustments */
:root.dark .rename-card, :root.dark .rename-card,
:root.auto.dark .rename-card { :root.auto.dark .rename-card {
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.3), 0 10px 10px -5px rgba(0, 0, 0, 0.2); box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.3), 0 10px 10px -5px rgba(0, 0, 0, 0.2);
} }
</style> </style>

View File

@@ -1,284 +1,284 @@
<template> <template>
<div class="toolbox-container"> <div class="toolbox-container">
<!-- Header Card --> <!-- Header Card -->
<div class="toolbox-card header-card"> <div class="toolbox-card header-card">
<div class="card-header"> <div class="card-header">
<div class="header-content"> <div class="header-content">
<img <img
class="header-logo" class="header-logo"
:src="defaultLogo" :src="defaultLogo"
alt="Toolbox Logo" alt="Toolbox Logo"
> >
<div class="header-text"> <div class="header-text">
<h1 class="header-title"> <h1 class="header-title">
{{ t('pages.toolbox.title') }} {{ t('pages.toolbox.title') }}
</h1> </h1>
<p class="header-subtitle"> <p class="header-subtitle">
{{ t('pages.toolbox.description') }} {{ t('pages.toolbox.description') }}
</p> </p>
</div> </div>
</div> </div>
<div class="header-actions"> <div class="header-actions">
<template v-if="progress !== 100"> <template v-if="progress !== 100">
<button <button
class="action-button" class="action-button"
:class="{ disabled: isLoading }" :class="{ disabled: isLoading }"
:disabled="isLoading" :disabled="isLoading"
@click="handleCheck" @click="handleCheck"
> >
<span>{{ t('pages.toolbox.startScan') }}</span> <span>{{ t('pages.toolbox.startScan') }}</span>
</button> </button>
</template> </template>
<template v-else-if="isAllSuccess"> <template v-else-if="isAllSuccess">
<div class="success-tips"> <div class="success-tips">
{{ t('pages.toolbox.success') }} {{ t('pages.toolbox.success') }}
</div> </div>
</template> </template>
<template v-else-if="!isAllSuccess"> <template v-else-if="!isAllSuccess">
<template v-if="canFixLength !== 0"> <template v-if="canFixLength !== 0">
<button <button
class="action-button" class="action-button"
@click="handleFix" @click="handleFix"
> >
<span>{{ t('pages.toolbox.startFix') }}</span> <span>{{ t('pages.toolbox.startFix') }}</span>
</button> </button>
</template> </template>
<template v-else> <template v-else>
<div class="cant-fix-container"> <div class="cant-fix-container">
<span class="cant-fix-text">{{ $t('pages.toolbox.autoFixFail') }}</span> <span class="cant-fix-text">{{ $t('pages.toolbox.autoFixFail') }}</span>
<button <button
class="action-button secondary small" class="action-button secondary small"
@click="handleCheck" @click="handleCheck"
> >
<span>{{ t('pages.toolbox.reScan') }}</span> <span>{{ t('pages.toolbox.reScan') }}</span>
</button> </button>
</div> </div>
</template> </template>
</template> </template>
</div> </div>
</div> </div>
</div> </div>
<!-- Progress Card --> <!-- Progress Card -->
<div class="toolbox-card progress-card"> <div class="toolbox-card progress-card">
<div class="progress-container"> <div class="progress-container">
<div class="progress-bar"> <div class="progress-bar">
<div <div
class="progress-fill" class="progress-fill"
:style="{ width: `${progress}%` }" :style="{ width: `${progress}%` }"
/> />
</div> </div>
<span class="progress-text">{{ Math.round(progress) }}%</span> <span class="progress-text">{{ Math.round(progress) }}%</span>
</div> </div>
</div> </div>
<!-- Items Card --> <!-- Items Card -->
<div class="toolbox-card items-card"> <div class="toolbox-card items-card">
<div class="items-list"> <div class="items-list">
<div <div
v-for="(item, key) in fixList" v-for="(item, key) in fixList"
:key="key" :key="key"
class="item" class="item"
:class="{ :class="{
'item-active': activeTypes.includes(key), 'item-active': activeTypes.includes(key),
'item-error': item.status === IToolboxItemCheckStatus.ERROR, 'item-error': item.status === IToolboxItemCheckStatus.ERROR,
'item-success': item.status === IToolboxItemCheckStatus.SUCCESS, 'item-success': item.status === IToolboxItemCheckStatus.SUCCESS,
'item-loading': item.status === IToolboxItemCheckStatus.LOADING 'item-loading': item.status === IToolboxItemCheckStatus.LOADING
}" }"
> >
<div <div
class="item-header" class="item-header"
@click="toggleItem(key)" @click="toggleItem(key)"
> >
<div class="item-title"> <div class="item-title">
<span>{{ item.title }}</span> <span>{{ item.title }}</span>
<toolbox-status-icon :status="item.status" /> <toolbox-status-icon :status="item.status" />
</div> </div>
<div class="item-chevron"> <div class="item-chevron">
<svg <svg
width="16" width="16"
height="16" height="16"
viewBox="0 0 24 24" viewBox="0 0 24 24"
fill="none" fill="none"
stroke="currentColor" stroke="currentColor"
stroke-width="2" stroke-width="2"
> >
<polyline points="6,9 12,15 18,9" /> <polyline points="6,9 12,15 18,9" />
</svg> </svg>
</div> </div>
</div> </div>
<transition name="item-content"> <transition name="item-content">
<div <div
v-if="activeTypes.includes(key)" v-if="activeTypes.includes(key)"
class="item-content" class="item-content"
> >
<div class="item-message"> <div class="item-message">
{{ item.msg || '' }} {{ item.msg || '' }}
</div> </div>
<template v-if="item.handler && item.handlerText && item.value"> <template v-if="item.handler && item.handlerText && item.value">
<div class="item-actions"> <div class="item-actions">
<toolbox-handler <toolbox-handler
:value="item.value" :value="item.value"
:status="item.status" :status="item.status"
:handler="item.handler" :handler="item.handler"
:handler-text="item.handlerText" :handler-text="item.handlerText"
/> />
</div> </div>
</template> </template>
</div> </div>
</transition> </transition>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { computed, onUnmounted, reactive, ref } from 'vue' import { computed, onUnmounted, reactive, ref } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import ToolboxHandler from '@/components/ToolboxHandler.vue' import ToolboxHandler from '@/components/ToolboxHandler.vue'
import ToolboxStatusIcon from '@/components/ToolboxStatusIcon.vue' import ToolboxStatusIcon from '@/components/ToolboxStatusIcon.vue'
import useConfirm from '@/hooks/useConfirm' import useConfirm from '@/hooks/useConfirm'
import { IRPCActionType, IToolboxItemCheckStatus, IToolboxItemType } from '@/utils/enum' import { IRPCActionType, IToolboxItemCheckStatus, IToolboxItemType } from '@/utils/enum'
import type { IToolboxCheckRes } from '#/types/rpc' import type { IToolboxCheckRes } from '#/types/rpc'
import type { IToolboxMap } from '#/types/view' import type { IToolboxMap } from '#/types/view'
const { t } = useI18n() const { t } = useI18n()
const { confirm } = useConfirm() const { confirm } = useConfirm()
const defaultLogo = ref('/roundLogo.png') const defaultLogo = ref('/roundLogo.png')
const activeTypes = ref<string[]>([]) const activeTypes = ref<string[]>([])
const fixList = reactive<IToolboxMap>({ const fixList = reactive<IToolboxMap>({
[IToolboxItemType.IS_CONFIG_FILE_BROKEN]: { [IToolboxItemType.IS_CONFIG_FILE_BROKEN]: {
title: t('pages.toolbox.checkConfigFileBroken'), title: t('pages.toolbox.checkConfigFileBroken'),
status: IToolboxItemCheckStatus.INIT, status: IToolboxItemCheckStatus.INIT,
handlerText: t('pages.toolbox.openConfigFile'), handlerText: t('pages.toolbox.openConfigFile'),
handler (value: string) { handler (value: string) {
window.electron.sendRPC(IRPCActionType.OPEN_FILE, value) window.electron.sendRPC(IRPCActionType.OPEN_FILE, value)
} }
}, },
[IToolboxItemType.IS_GALLERY_FILE_BROKEN]: { [IToolboxItemType.IS_GALLERY_FILE_BROKEN]: {
title: t('pages.toolbox.checkGalleryFileBroken'), title: t('pages.toolbox.checkGalleryFileBroken'),
status: IToolboxItemCheckStatus.INIT status: IToolboxItemCheckStatus.INIT
}, },
[IToolboxItemType.HAS_PROBLEM_WITH_CLIPBOARD_PIC_UPLOAD]: { [IToolboxItemType.HAS_PROBLEM_WITH_CLIPBOARD_PIC_UPLOAD]: {
title: t('pages.toolbox.checkProblemWithClipboardPicUpload'), // picgo-image-clipboard folder title: t('pages.toolbox.checkProblemWithClipboardPicUpload'), // picgo-image-clipboard folder
status: IToolboxItemCheckStatus.INIT, status: IToolboxItemCheckStatus.INIT,
handlerText: t('pages.toolbox.openFilePath'), handlerText: t('pages.toolbox.openFilePath'),
handler (value: string) { handler (value: string) {
window.electron.sendRPC(IRPCActionType.OPEN_FILE, value) window.electron.sendRPC(IRPCActionType.OPEN_FILE, value)
} }
}, },
[IToolboxItemType.HAS_PROBLEM_WITH_PROXY]: { [IToolboxItemType.HAS_PROBLEM_WITH_PROXY]: {
title: t('pages.toolbox.checkProblemWithProxy'), title: t('pages.toolbox.checkProblemWithProxy'),
status: IToolboxItemCheckStatus.INIT, status: IToolboxItemCheckStatus.INIT,
hasNoFixMethod: true hasNoFixMethod: true
} }
}) })
const progress = computed(() => { const progress = computed(() => {
const total = Object.keys(fixList).length const total = Object.keys(fixList).length
const done = Object.keys(fixList).filter(key => { const done = Object.keys(fixList).filter(key => {
const status = fixList[key].status const status = fixList[key].status
return status !== IToolboxItemCheckStatus.INIT && status !== IToolboxItemCheckStatus.LOADING return status !== IToolboxItemCheckStatus.INIT && status !== IToolboxItemCheckStatus.LOADING
}).length }).length
return (done / total) * 100 return (done / total) * 100
}) })
const isAllSuccess = computed(() => { const isAllSuccess = computed(() => {
return Object.keys(fixList).every(key => { return Object.keys(fixList).every(key => {
const status = fixList[key].status const status = fixList[key].status
return status === IToolboxItemCheckStatus.SUCCESS return status === IToolboxItemCheckStatus.SUCCESS
}) })
}) })
const isLoading = computed(() => { const isLoading = computed(() => {
return Object.keys(fixList).some(key => { return Object.keys(fixList).some(key => {
const status = fixList[key].status const status = fixList[key].status
return status === IToolboxItemCheckStatus.LOADING return status === IToolboxItemCheckStatus.LOADING
}) })
}) })
const canFixLength = computed(() => { const canFixLength = computed(() => {
return Object.keys(fixList).filter(key => { return Object.keys(fixList).filter(key => {
const status = fixList[key].status const status = fixList[key].status
return status === IToolboxItemCheckStatus.ERROR && !fixList[key].hasNoFixMethod return status === IToolboxItemCheckStatus.ERROR && !fixList[key].hasNoFixMethod
}).length }).length
}) })
const toggleItem = (key: string) => { const toggleItem = (key: string) => {
const index = activeTypes.value.indexOf(key) const index = activeTypes.value.indexOf(key)
if (index > -1) { if (index > -1) {
activeTypes.value.splice(index, 1) activeTypes.value.splice(index, 1)
} else { } else {
activeTypes.value.push(key) activeTypes.value.push(key)
} }
} }
const toolboxCheckResHandler = (_event: any, { type, msg = '', status, value = '' }: IToolboxCheckRes) => { const toolboxCheckResHandler = ({ type, msg = '', status, value = '' }: IToolboxCheckRes) => {
fixList[type].status = status fixList[type].status = status
fixList[type].msg = msg fixList[type].msg = msg
fixList[type].value = value fixList[type].value = value
if (status === IToolboxItemCheckStatus.ERROR) { if (status === IToolboxItemCheckStatus.ERROR) {
activeTypes.value.push(type) activeTypes.value.push(type)
} }
} }
window.electron.ipcRendererOn(IRPCActionType.TOOLBOX_CHECK_RES, toolboxCheckResHandler) window.electron.ipcRendererOn(IRPCActionType.TOOLBOX_CHECK_RES, toolboxCheckResHandler)
const handleCheck = () => { const handleCheck = () => {
activeTypes.value = [] activeTypes.value = []
Object.keys(fixList).forEach(key => { Object.keys(fixList).forEach(key => {
fixList[key].status = IToolboxItemCheckStatus.LOADING fixList[key].status = IToolboxItemCheckStatus.LOADING
fixList[key].msg = '' fixList[key].msg = ''
fixList[key].value = '' fixList[key].value = ''
}) })
window.electron.sendRPC(IRPCActionType.TOOLBOX_CHECK) window.electron.sendRPC(IRPCActionType.TOOLBOX_CHECK)
} }
const handleFix = async () => { const handleFix = async () => {
const fixRes = await Promise.all( const fixRes = await Promise.all(
Object.keys(fixList) Object.keys(fixList)
.filter(key => { .filter(key => {
const status = fixList[key].status const status = fixList[key].status
return status === IToolboxItemCheckStatus.ERROR && !fixList[key].hasNoFixMethod return status === IToolboxItemCheckStatus.ERROR && !fixList[key].hasNoFixMethod
}) })
.map(async key => { .map(async key => {
return window.electron.triggerRPC<IToolboxCheckRes>(IRPCActionType.TOOLBOX_CHECK_FIX, key) return window.electron.triggerRPC<IToolboxCheckRes>(IRPCActionType.TOOLBOX_CHECK_FIX, key)
}) })
) )
fixRes fixRes
.filter(item => item !== null) .filter(item => item !== null)
.forEach(item => { .forEach(item => {
if (item) { if (item) {
fixList[item.type].status = item.status fixList[item.type].status = item.status
fixList[item.type].msg = item.msg fixList[item.type].msg = item.msg
fixList[item.type].value = item.value fixList[item.type].value = item.value
} }
}) })
confirm({ confirm({
title: t('pages.toolbox.notice'), title: t('pages.toolbox.notice'),
message: t('pages.toolbox.fixDoneNeedReload'), message: t('pages.toolbox.fixDoneNeedReload'),
type: 'warning', type: 'warning',
confirmButtonText: t('common.confirm'), confirmButtonText: t('common.confirm'),
cancelButtonText: t('common.cancel'), cancelButtonText: t('common.cancel'),
center: true center: true
}).then(result => { }).then(result => {
if (!result) return if (!result) return
window.electron.sendRPC(IRPCActionType.RELOAD_APP) window.electron.sendRPC(IRPCActionType.RELOAD_APP)
}) })
} }
onUnmounted(() => { onUnmounted(() => {
window.electron.ipcRendererRemoveListener(IRPCActionType.TOOLBOX_CHECK_RES, toolboxCheckResHandler) window.electron.ipcRendererRemoveAllListeners(IRPCActionType.TOOLBOX_CHECK_RES)
}) })
</script> </script>
<script lang="ts"> <script lang="ts">
export default { export default {
name: 'ToolBoxPage' name: 'ToolBoxPage'
} }
</script> </script>
<style scoped src="./css/ToolboxPage.css"></style> <style scoped src="./css/ToolboxPage.css"></style>

File diff suppressed because it is too large Load Diff

View File

@@ -13,7 +13,7 @@
<span class="provider-name">{{ picBedName }}</span> <span class="provider-name">{{ picBedName }}</span>
<span class="provider-config">{{ picBedConfigName || 'Default' }}</span> <span class="provider-config">{{ picBedConfigName || 'Default' }}</span>
</div> </div>
<ChevronDownIcon <EditIcon
:size="16" :size="16"
class="provider-arrow" class="provider-arrow"
/> />
@@ -31,7 +31,7 @@
class="action-button" class="action-button"
@click="handleChangePicBed" @click="handleChangePicBed"
> >
<DatabaseIcon :size="16" /> <ArrowLeftRightIcon :size="16" />
<span>{{ t('pages.upload.changePicBed') }}</span> <span>{{ t('pages.upload.changePicBed') }}</span>
</button> </button>
</div> </div>
@@ -173,7 +173,7 @@
<div <div
v-if="imageProcessDialogVisible" v-if="imageProcessDialogVisible"
class="modal-overlay" class="modal-overlay"
@click="imageProcessDialogVisible = false" @click.stop
> >
<div <div
class="modal-container" class="modal-container"
@@ -200,8 +200,7 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import type { IpcRendererEvent } from 'electron' import { ArrowLeftRightIcon, ClipboardIcon, EditIcon, LinkIcon, Settings, UploadCloudIcon, XIcon } from 'lucide-vue-next'
import { ChevronDownIcon, ClipboardIcon, DatabaseIcon, LinkIcon, Settings, UploadCloudIcon, XIcon } from 'lucide-vue-next'
import { onBeforeMount, onBeforeUnmount, ref, watch } from 'vue' import { onBeforeMount, onBeforeUnmount, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
@@ -247,30 +246,23 @@ watch(picBedGlobal, () => {
getDefaultPicBed() getDefaultPicBed()
}) })
const uploadProgressHandler = (_event: IpcRendererEvent, _progress: number) => { let removeUploadProgressListenerCallback: (() => void) = () => {}
if (_progress !== -1) { let removeSyncPicBedListenerCallback: (() => void) = () => {}
function uploadProgressHandler (p: number): void {
if (p !== -1) {
showProgress.value = true showProgress.value = true
progress.value = _progress progress.value = p
} else { } else {
progress.value = 100 progress.value = 100
showError.value = true showError.value = true
} }
} }
const syncPicBedHandler = () => { function syncPicBedHandler (): void {
getDefaultPicBed() 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 = () => { const handleImageProcess = () => {
imageProcessDialogVisible.value = true imageProcessDialogVisible.value = true
} }
@@ -308,12 +300,6 @@ async function handlePicBedNameClick (_picBedName: string, picBedConfigName: str
}) })
} }
onBeforeUnmount(() => {
$bus.off(SHOW_INPUT_BOX_RESPONSE)
window.electron.ipcRendererRemoveListener('uploadProgress', uploadProgressHandler)
window.electron.ipcRendererRemoveListener('syncPicBed', syncPicBedHandler)
})
function onDrop (e: DragEvent) { function onDrop (e: DragEvent) {
dragover.value = false dragover.value = false
@@ -434,6 +420,23 @@ async function getDefaultPicBed () {
async function handleChangePicBed () { async function handleChangePicBed () {
window.electron.sendRPC(IRPCActionType.SHOW_UPLOAD_PAGE_MENU) window.electron.sendRPC(IRPCActionType.SHOW_UPLOAD_PAGE_MENU)
} }
onBeforeUnmount(() => {
$bus.off(SHOW_INPUT_BOX_RESPONSE)
removeUploadProgressListenerCallback()
removeSyncPicBedListenerCallback()
})
onBeforeMount(() => {
updatePicBedGlobal()
getUseShortUrl()
getPasteStyle()
getDefaultPicBed()
removeUploadProgressListenerCallback = window.electron.ipcRendererOn('uploadProgress', uploadProgressHandler)
removeSyncPicBedListenerCallback = window.electron.ipcRendererOn('syncPicBed', syncPicBedHandler)
$bus.on(SHOW_INPUT_BOX_RESPONSE, handleInputBoxValue)
})
</script> </script>
<script lang="ts"> <script lang="ts">

View File

@@ -1,204 +1,203 @@
<template> <template>
<div class="config-container"> <div class="config-container">
<!-- Header Card --> <!-- Header Card -->
<div class="config-card header-card"> <div class="config-card header-card">
<div class="card-header"> <div class="card-header">
<h1 class="page-title"> <h1 class="page-title">
{{ t('pages.uploaderConfig.title') }} {{ t('pages.uploaderConfig.title') }}
</h1> </h1>
</div> </div>
</div> </div>
<!-- Config Items Card --> <!-- Config Items Card -->
<div class="config-card main-card"> <div class="config-card main-card">
<div class="config-grid"> <div class="config-grid">
<div <div
v-for="item in curConfigList" v-for="item in curConfigList"
:key="item._id" :key="item._id"
:class="`config-item ${defaultConfigId === item._id ? 'selected' : ''}`" :class="`config-item ${defaultConfigId === item._id ? 'selected' : ''}`"
@click="() => selectItem(item._id)" @click="() => selectItem(item._id)"
> >
<div class="config-content"> <div class="config-content">
<div class="config-name"> <div class="config-name">
{{ item._configName }} {{ item._configName }}
</div> </div>
<div class="config-update-time"> <div class="config-update-time">
{{ formatTime(item._updatedAt) }} {{ formatTime(item._updatedAt) }}
</div> </div>
<div <div
v-if="defaultConfigId === item._id" v-if="defaultConfigId === item._id"
class="default-badge" class="default-badge"
> >
{{ t('pages.uploaderConfig.selected') }} {{ t('pages.uploaderConfig.selected') }}
</div> </div>
</div> </div>
<div class="config-actions"> <div class="config-actions">
<button <button
class="action-btn edit-btn" class="action-btn edit-btn"
:title="t('pages.uploaderConfig.edit')" :title="t('pages.uploaderConfig.edit')"
@click.stop="openEditPage(item._id)" @click.stop="openEditPage(item._id)"
> >
<Edit :size="16" /> <Edit :size="16" />
</button> </button>
<button <button
class="action-btn delete-btn" class="action-btn delete-btn"
:class="curConfigList.length <= 1 ? 'disabled' : ''" :class="curConfigList.length <= 1 ? 'disabled' : ''"
:title="t('pages.uploaderConfig.delete')" :title="t('pages.uploaderConfig.delete')"
:disabled="curConfigList.length <= 1" :disabled="curConfigList.length <= 1"
@click.stop="() => deleteConfig(item._id)" @click.stop="() => deleteConfig(item._id)"
> >
<Trash2 :size="16" /> <Trash2 :size="16" />
</button> </button>
</div> </div>
</div> </div>
<!-- Add New Config Button --> <!-- Add New Config Button -->
<div <div
class="config-item config-item-add" class="config-item config-item-add"
@click="addNewConfig" @click="addNewConfig"
> >
<div class="add-content"> <div class="add-content">
<Plus :size="32" /> <Plus :size="32" />
<span class="add-text">{{ t('pages.uploaderConfig.addNew') }}</span> <span class="add-text">{{ t('pages.uploaderConfig.addNew') }}</span>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<!-- Actions Card --> <!-- Actions Card -->
<div class="config-card actions-card"> <div class="config-card actions-card">
<div class="card-actions"> <div class="card-actions">
<button <button
class="primary-button" class="primary-button"
:disabled="store?.state.defaultPicBed === type" :disabled="store?.state.defaultPicBed === type"
@click="setDefaultPicBed(type)" @click="setDefaultPicBed(type)"
> >
<DatabaseIcon :size="16" /> <DatabaseIcon :size="16" />
<span>{{ t('pages.uploaderConfig.setAsDefault') }}</span> <span>{{ t('pages.uploaderConfig.setAsDefault') }}</span>
</button> </button>
</div> </div>
</div> </div>
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { DatabaseIcon, Edit, Plus, Trash2 } from 'lucide-vue-next' import { DatabaseIcon, Edit, Plus, Trash2 } from 'lucide-vue-next'
import { onBeforeMount, ref } from 'vue' import { onBeforeMount, ref } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { onBeforeRouteUpdate, useRoute, useRouter } from 'vue-router' import { onBeforeRouteUpdate, useRoute, useRouter } from 'vue-router'
import useConfirm from '@/hooks/useConfirm' import useConfirm from '@/hooks/useConfirm'
import useMessage from '@/hooks/useMessage' import useMessage from '@/hooks/useMessage'
import { useStore } from '@/hooks/useStore' import { useStore } from '@/hooks/useStore'
import { PICBEDS_PAGE, UPLOADER_CONFIG_PAGE } from '@/router/config' import { PICBEDS_PAGE, UPLOADER_CONFIG_PAGE } from '@/router/config'
import { configPaths } from '@/utils/configPaths' import { configPaths } from '@/utils/configPaths'
import { saveConfig } from '@/utils/dataSender' import { saveConfig } from '@/utils/dataSender'
import { IRPCActionType } from '@/utils/enum' import { IRPCActionType } from '@/utils/enum'
import type { IStringKeyMap, IUploaderConfigItem } from '#/types/types' import type { IStringKeyMap, IUploaderConfigItem } from '#/types/types'
const { t } = useI18n() const { t } = useI18n()
const message = useMessage() const message = useMessage()
const { confirm } = useConfirm() const { confirm } = useConfirm()
const router = useRouter() const router = useRouter()
const route = useRoute() const route = useRoute()
const type = ref('') const type = ref('')
const curConfigList = ref<IStringKeyMap[]>([]) const curConfigList = ref<IStringKeyMap[]>([])
const defaultConfigId = ref('') const defaultConfigId = ref('')
const store = useStore() const store = useStore()
async function selectItem (id: string) { async function selectItem (id: string) {
await window.electron.triggerRPC<void>(IRPCActionType.UPLOADER_SELECT, type.value, id) await window.electron.triggerRPC<void>(IRPCActionType.UPLOADER_SELECT, type.value, id)
if (store?.state.defaultPicBed === type.value) { if (store?.state.defaultPicBed === type.value) {
window.electron.sendRPC( window.electron.sendRPC(
IRPCActionType.TRAY_SET_TOOL_TIP, IRPCActionType.TRAY_SET_TOOL_TIP,
`${type.value} ${curConfigList.value.find(item => item._id === id)?._configName || ''}` `${type.value} ${curConfigList.value.find(item => item._id === id)?._configName || ''}`
) )
} }
defaultConfigId.value = id defaultConfigId.value = id
} }
onBeforeRouteUpdate((to, _, next) => { onBeforeRouteUpdate((to, _, next) => {
if (to.params.type && to.name === UPLOADER_CONFIG_PAGE) { if (to.params.type && to.name === UPLOADER_CONFIG_PAGE) {
type.value = to.params.type as string type.value = to.params.type as string
getCurrentConfigList() getCurrentConfigList()
} }
next() next()
}) })
onBeforeMount(() => { onBeforeMount(() => {
type.value = route.params.type as string type.value = route.params.type as string
getCurrentConfigList() getCurrentConfigList()
}) })
async function getCurrentConfigList () { async function getCurrentConfigList () {
const configList = await window.electron.triggerRPC<IUploaderConfigItem>(IRPCActionType.PICBED_GET_CONFIG_LIST, type.value) const configList = await window.electron.triggerRPC<IUploaderConfigItem>(IRPCActionType.PICBED_GET_CONFIG_LIST, type.value)
curConfigList.value = configList?.configList ?? [] curConfigList.value = configList?.configList ?? []
defaultConfigId.value = configList?.defaultId ?? '' defaultConfigId.value = configList?.defaultId ?? ''
} }
function openEditPage (configId: string) { function openEditPage (configId: string) {
router.push({ router.push({
name: PICBEDS_PAGE, name: PICBEDS_PAGE,
params: { params: {
type: type.value, type: type.value,
configId configId
}, },
query: { query: {
defaultConfigId: defaultConfigId.value defaultConfigId: defaultConfigId.value
} }
}) })
} }
function formatTime (time: number): string { function formatTime (time: number): string {
return dayjs(time).format('YYYY-MM-DD HH:mm') return dayjs(time).format('YYYY-MM-DD HH:mm')
} }
async function deleteConfig (id: string) { async function deleteConfig (id: string) {
confirm({ const result = await confirm({
title: t('pages.uploaderConfig.deleteTitle'), title: t('pages.uploaderConfig.deleteTitle'),
message: t('pages.uploaderConfig.deleteConfirm'), message: t('pages.uploaderConfig.deleteConfirm'),
type: 'warning', type: 'warning',
confirmButtonText: t('common.confirm'), confirmButtonText: t('common.confirm'),
cancelButtonText: t('common.cancel'), cancelButtonText: t('common.cancel'),
center: true center: true
}).then(async result => { })
if (!result) return if (!result) return
const res = await window.electron.triggerRPC<IUploaderConfigItem>(IRPCActionType.PICBED_DELETE_CONFIG, type.value, id) const res = await window.electron.triggerRPC<IUploaderConfigItem>(IRPCActionType.PICBED_DELETE_CONFIG, type.value, id)
if (!res) return if (!res) return
curConfigList.value = res.configList curConfigList.value = res.configList
defaultConfigId.value = res.defaultId defaultConfigId.value = res.defaultId
message.success(t('pages.uploaderConfig.deleteSuccess')) message.success(t('pages.uploaderConfig.deleteSuccess'))
}) }
}
function addNewConfig () {
function addNewConfig () { router.push({
router.push({ name: PICBEDS_PAGE,
name: PICBEDS_PAGE, params: {
params: { type: type.value,
type: type.value, configId: ''
configId: '' }
} })
}) }
}
function setDefaultPicBed (type: string) {
function setDefaultPicBed (type: string) { saveConfig({
saveConfig({ [configPaths.picBed.current]: type,
[configPaths.picBed.current]: type, [configPaths.picBed.uploader]: type
[configPaths.picBed.uploader]: type })
})
store?.setDefaultPicBed(type)
store?.setDefaultPicBed(type) const currentConfigName = curConfigList.value.find(item => item._id === defaultConfigId.value)?._configName
const currentConfigName = curConfigList.value.find(item => item._id === defaultConfigId.value)?._configName window.electron.sendRPC(IRPCActionType.TRAY_SET_TOOL_TIP, `${type} ${currentConfigName || ''}`)
window.electron.sendRPC(IRPCActionType.TRAY_SET_TOOL_TIP, `${type} ${currentConfigName || ''}`) message.success(t('pages.uploaderConfig.setSuccess'))
message.success(t('pages.uploaderConfig.setSuccess')) }
} </script>
</script> <script lang="ts">
<script lang="ts"> export default {
export default { name: 'UploaderConfigPage'
name: 'UploaderConfigPage' }
} </script>
</script>
<style scoped src="./css/UploaderConfigPage.css"></style>
<style scoped src="./css/UploaderConfigPage.css"></style>

View File

@@ -107,7 +107,6 @@ html, body {
.provider-button:hover .provider-arrow { .provider-button:hover .provider-arrow {
color: var(--color-accent); color: var(--color-accent);
transform: rotate(180deg);
} }
.header-actions { .header-actions {

View File

View File

@@ -1,74 +1,74 @@
// src/global.d.ts // src/global.d.ts
import crypto from 'node:crypto' import crypto from 'node:crypto'
import path from 'node:path' import path from 'node:path'
import { clipboard } from 'electron' import { clipboard } from 'electron'
import fs from 'fs-extra' import fs from 'fs-extra'
import yaml from 'js-yaml' import yaml from 'js-yaml'
import mime from 'mime-types' import mime from 'mime-types'
import { VNode } from 'vue' import { VNode } from 'vue'
import { IpcRendererListener } from '#/types/electron' import { ILocales, ILocalesKey } from '#/types/i18n'
import { ILocales, ILocalesKey } from '#/types/i18n' import { IStringKeyMap } from '#/types/types'
import { IStringKeyMap } from '#/types/types'
declare global {
declare global {
export namespace JSX {
export namespace JSX { export interface Element extends VNode {}
export interface Element extends VNode {} export interface IntrinsicElements {
export interface IntrinsicElements { [elem: string]: any
[elem: string]: any }
} }
} export interface Window {
export interface Window { electron: {
electron: { platform: string
platform: string setVisualZoomLevelLimits: (min: number, max: number) => void
setVisualZoomLevelLimits: (min: number, max: number) => void sendRpcSync: (action: string, ...args: any[]) => any
sendRpcSync: (action: string, ...args: any[]) => any triggerRPC: <T>(action: string, ...args: any[]) => Promise<T | undefined>
triggerRPC: <T>(action: string, ...args: any[]) => Promise<T | undefined> sendToMain: (channel: string, ...args: any[]) => void
sendToMain: (channel: string, ...args: any[]) => void sendRPC: (action: string, ...args: any[]) => void
sendRPC: (action: string, ...args: any[]) => void ipcRendererOn: (channel: string, listener: (...args: any[]) => void) => () => void
ipcRendererOn: (channel: string, listener: IpcRendererListener) => void ipcRendererCountListeners: (channel: string) => number
ipcRendererRemoveListener: (channel: string, listener: IpcRendererListener) => void ipcRendererRemoveAllListeners: (channel: string) => void
clipboard: { clipboard: {
writeText: typeof clipboard.writeText writeText: typeof clipboard.writeText
}, },
showFilePath: (file: File) => string showFilePath: (file: File) => string
} }
node: { node: {
path: { path: {
join: typeof path.join join: typeof path.join
dirname: typeof path.dirname dirname: typeof path.dirname
basename: typeof path.basename basename: typeof path.basename
normalize: typeof path.normalize normalize: typeof path.normalize
extname: typeof path.extname extname: typeof path.extname
sep: typeof path.sep, sep: typeof path.sep,
posix: { posix: {
sep: typeof path.posix.sep sep: typeof path.posix.sep
} }
} }
crypto: { crypto: {
randomBytes: typeof crypto.randomBytes randomBytes: typeof crypto.randomBytes
createHash: typeof crypto.createHash createHash: typeof crypto.createHash
} }
fs: { fs: {
remove: typeof fs.remove remove: typeof fs.remove
readFile: typeof fs.readFile readFile: typeof fs.readFile
statSync: typeof fs.statSync statSync: typeof fs.statSync
} }
yaml: { yaml: {
load: typeof yaml.load load: typeof yaml.load
} }
mime: { mime: {
lookup: typeof mime.lookup lookup: typeof mime.lookup
} }
} }
i18n: { i18n: {
setLocales: (lang: string, locales: ILocales) => void setLocales: (lang: string, locales: ILocales) => void
translate: (key: ILocalesKey, args?: IStringKeyMap) => string translate: (key: ILocalesKey, args?: IStringKeyMap) => string
} }
} }
} }