mirror of
https://github.com/Kuingsmile/PicList.git
synced 2026-07-08 08:21:26 +08:00
🎨 Style(custom): lint code
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -1,244 +1,236 @@
|
||||
<template>
|
||||
<div id="mini-page">
|
||||
<div
|
||||
id="upload-area"
|
||||
:class="{
|
||||
'is-dragover': dragover,
|
||||
uploading: isShowingProgress,
|
||||
linux: osGlobal === 'linux'
|
||||
}"
|
||||
:style="{ backgroundPosition: '0 ' + progress + '%' }"
|
||||
@drop.prevent="onDrop"
|
||||
@dragover.prevent="dragover = true"
|
||||
@dragleave.prevent="dragover = false"
|
||||
>
|
||||
<img
|
||||
v-if="!dragover && !isShowingProgress"
|
||||
:src="logoPath ? logoPath : './squareLogo.png'"
|
||||
style="width: 100%; height: 100%; border-radius: 50%"
|
||||
draggable="false"
|
||||
@dragstart.prevent
|
||||
>
|
||||
<div
|
||||
id="upload-dragger"
|
||||
@dblclick="openUploadWindow"
|
||||
>
|
||||
<input
|
||||
id="file-uploader"
|
||||
type="file"
|
||||
multiple
|
||||
@change="onChange"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { IConfig } from 'piclist'
|
||||
import { onBeforeMount, onBeforeUnmount, ref, watch } from 'vue'
|
||||
|
||||
import { isUrl } from '@/utils/common'
|
||||
import { getConfig } from '@/utils/dataSender'
|
||||
import { IRPCActionType } from '@/utils/enum'
|
||||
import { osGlobal } from '@/utils/global'
|
||||
import type { IFileWithPath } from '#/types/types'
|
||||
|
||||
const logoPath = ref('')
|
||||
const dragover = ref(false)
|
||||
const progress = ref(0)
|
||||
const isShowingProgress = ref(false)
|
||||
const draggingState = ref(false)
|
||||
const wX = ref(-1)
|
||||
const wY = ref(-1)
|
||||
const screenX = ref(-1)
|
||||
const screenY = ref(-1)
|
||||
|
||||
let removeListeners: () => void = () => {}
|
||||
|
||||
async function initLogoPath () {
|
||||
const config = await getConfig<IConfig>()
|
||||
if (config) {
|
||||
if (config.settings?.isCustomMiniIcon && config.settings?.customMiniIcon) {
|
||||
logoPath.value =
|
||||
'data:image/jpg;base64,' +
|
||||
(await window.electron.triggerRPC(IRPCActionType.MANAGE_CONVERT_PATH_TO_BASE64, config.settings.customMiniIcon))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const uploadProgressHandler = (p: number) => {
|
||||
if (p !== -1) {
|
||||
isShowingProgress.value = true
|
||||
progress.value = p
|
||||
} else {
|
||||
progress.value = 100
|
||||
}
|
||||
}
|
||||
|
||||
const updateMiniIconHandler = async () => {
|
||||
await initLogoPath()
|
||||
}
|
||||
|
||||
watch(progress, val => {
|
||||
if (val === 100) {
|
||||
setTimeout(() => {
|
||||
isShowingProgress.value = false
|
||||
}, 1000)
|
||||
setTimeout(() => {
|
||||
progress.value = 0
|
||||
}, 1200)
|
||||
}
|
||||
})
|
||||
|
||||
function onDrop (e: DragEvent) {
|
||||
dragover.value = false
|
||||
|
||||
// send files first
|
||||
if (e.dataTransfer?.files?.length) {
|
||||
ipcSendFiles(e.dataTransfer.files)
|
||||
} else if (e.dataTransfer?.items) {
|
||||
const items = e.dataTransfer.items
|
||||
if (items.length === 2 && items[0].type === 'text/uri-list') {
|
||||
handleURLDrag(items, e.dataTransfer)
|
||||
} else if (items[0].type === 'text/plain') {
|
||||
const str = e.dataTransfer!.getData(items[0].type)
|
||||
if (isUrl(str)) {
|
||||
window.electron.sendRPC(IRPCActionType.UPLOAD_CHOOSED_FILES, [{ path: str }])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleURLDrag (items: DataTransferItemList, dataTransfer: DataTransfer) {
|
||||
// text/html
|
||||
// Use this data to get a more precise URL
|
||||
const urlString = dataTransfer.getData(items[1].type)
|
||||
const urlMatch = urlString.match(/<img.*src="(.*?)"/)
|
||||
if (urlMatch) {
|
||||
window.electron.sendRPC(IRPCActionType.UPLOAD_CHOOSED_FILES, [
|
||||
{
|
||||
path: urlMatch[1]
|
||||
}
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
function openUploadWindow () {
|
||||
// @ts-expect-error file-uploader
|
||||
document.getElementById('file-uploader').click()
|
||||
}
|
||||
|
||||
function onChange (e: any) {
|
||||
ipcSendFiles(e.target.files)
|
||||
// @ts-expect-error file-uploader
|
||||
document.getElementById('file-uploader').value = ''
|
||||
}
|
||||
|
||||
function ipcSendFiles (files: FileList) {
|
||||
const sendFiles: IFileWithPath[] = []
|
||||
Array.from(files).forEach(item => {
|
||||
const obj = {
|
||||
name: item.name,
|
||||
path: window.electron.showFilePath(item)
|
||||
}
|
||||
sendFiles.push(obj)
|
||||
})
|
||||
window.electron.sendRPC(IRPCActionType.UPLOAD_CHOOSED_FILES, sendFiles)
|
||||
}
|
||||
|
||||
function handleMouseDown (e: MouseEvent) {
|
||||
draggingState.value = true
|
||||
wX.value = e.pageX
|
||||
wY.value = e.pageY
|
||||
screenX.value = e.screenX
|
||||
screenY.value = e.screenY
|
||||
}
|
||||
|
||||
function handleMouseMove (e: MouseEvent) {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
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,
|
||||
y: yLoc,
|
||||
width: 64,
|
||||
height: 64
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function handleMouseUp (e: MouseEvent) {
|
||||
draggingState.value = false
|
||||
if (screenX.value === e.screenX && screenY.value === e.screenY) {
|
||||
if (e.button === 0) {
|
||||
// left mouse
|
||||
openUploadWindow()
|
||||
} else {
|
||||
openContextMenu()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function openContextMenu () {
|
||||
window.electron.sendRPC(IRPCActionType.SHOW_MINI_PAGE_MENU)
|
||||
}
|
||||
|
||||
onBeforeMount(async () => {
|
||||
await initLogoPath()
|
||||
removeListeners = window.electron.ipcRendererOn('uploadProgress', uploadProgressHandler)
|
||||
window.electron.ipcRendererOn('updateMiniIcon', updateMiniIconHandler)
|
||||
window.addEventListener('mousedown', handleMouseDown, false)
|
||||
window.addEventListener('mousemove', handleMouseMove, false)
|
||||
window.addEventListener('mouseup', handleMouseUp, false)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
removeListeners()
|
||||
window.electron.ipcRendererRemoveAllListeners('updateMiniIcon')
|
||||
window.removeEventListener('mousedown', handleMouseDown, false)
|
||||
window.removeEventListener('mousemove', handleMouseMove, false)
|
||||
window.removeEventListener('mouseup', handleMouseUp, false)
|
||||
})
|
||||
</script>
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'MiniPage'
|
||||
}
|
||||
</script>
|
||||
<style lang="stylus">
|
||||
#mini-page
|
||||
color #FFF
|
||||
height 100vh
|
||||
width 100vw
|
||||
border-radius 50%
|
||||
text-align center
|
||||
line-height 100vh
|
||||
font-size 40px
|
||||
background-size 90vh 90vw
|
||||
background-position center center
|
||||
background-repeat no-repeat
|
||||
position relative
|
||||
box-sizing border-box
|
||||
cursor pointer
|
||||
&.linux
|
||||
border-radius 0
|
||||
background-size 100vh 100vw
|
||||
#upload-area
|
||||
height 100%
|
||||
width 100%
|
||||
border-radius 50%
|
||||
&.linux
|
||||
border-radius 0
|
||||
&.uploading
|
||||
background: linear-gradient(to top, #409EFF 50%, #fff 51%)
|
||||
background-size 200%
|
||||
#upload-dragger
|
||||
height 100%
|
||||
&.is-dragover
|
||||
background rgba(0,0,0,0.3)
|
||||
#file-uploader
|
||||
display none
|
||||
</style>
|
||||
<template>
|
||||
<div id="mini-page">
|
||||
<div
|
||||
id="upload-area"
|
||||
:class="{
|
||||
'is-dragover': dragover,
|
||||
uploading: isShowingProgress,
|
||||
linux: osGlobal === 'linux'
|
||||
}"
|
||||
:style="{ backgroundPosition: '0 ' + progress + '%' }"
|
||||
@drop.prevent="onDrop"
|
||||
@dragover.prevent="dragover = true"
|
||||
@dragleave.prevent="dragover = false"
|
||||
>
|
||||
<img
|
||||
v-if="!dragover && !isShowingProgress"
|
||||
:src="logoPath ? logoPath : './squareLogo.png'"
|
||||
style="width: 100%; height: 100%; border-radius: 50%"
|
||||
draggable="false"
|
||||
@dragstart.prevent
|
||||
/>
|
||||
<div id="upload-dragger" @dblclick="openUploadWindow">
|
||||
<input id="file-uploader" type="file" multiple @change="onChange" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { IConfig } from 'piclist'
|
||||
import { onBeforeMount, onBeforeUnmount, ref, watch } from 'vue'
|
||||
|
||||
import { isUrl } from '@/utils/common'
|
||||
import { getConfig } from '@/utils/dataSender'
|
||||
import { IRPCActionType } from '@/utils/enum'
|
||||
import { osGlobal } from '@/utils/global'
|
||||
import type { IFileWithPath } from '#/types/types'
|
||||
|
||||
const logoPath = ref('')
|
||||
const dragover = ref(false)
|
||||
const progress = ref(0)
|
||||
const isShowingProgress = ref(false)
|
||||
const draggingState = ref(false)
|
||||
const wX = ref(-1)
|
||||
const wY = ref(-1)
|
||||
const screenX = ref(-1)
|
||||
const screenY = ref(-1)
|
||||
|
||||
let removeListeners: () => void = () => {}
|
||||
|
||||
async function initLogoPath() {
|
||||
const config = await getConfig<IConfig>()
|
||||
if (config) {
|
||||
if (config.settings?.isCustomMiniIcon && config.settings?.customMiniIcon) {
|
||||
logoPath.value =
|
||||
'data:image/jpg;base64,' +
|
||||
(await window.electron.triggerRPC(IRPCActionType.MANAGE_CONVERT_PATH_TO_BASE64, config.settings.customMiniIcon))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const uploadProgressHandler = (p: number) => {
|
||||
if (p !== -1) {
|
||||
isShowingProgress.value = true
|
||||
progress.value = p
|
||||
} else {
|
||||
progress.value = 100
|
||||
}
|
||||
}
|
||||
|
||||
const updateMiniIconHandler = async () => {
|
||||
await initLogoPath()
|
||||
}
|
||||
|
||||
watch(progress, val => {
|
||||
if (val === 100) {
|
||||
setTimeout(() => {
|
||||
isShowingProgress.value = false
|
||||
}, 1000)
|
||||
setTimeout(() => {
|
||||
progress.value = 0
|
||||
}, 1200)
|
||||
}
|
||||
})
|
||||
|
||||
function onDrop(e: DragEvent) {
|
||||
dragover.value = false
|
||||
|
||||
// send files first
|
||||
if (e.dataTransfer?.files?.length) {
|
||||
ipcSendFiles(e.dataTransfer.files)
|
||||
} else if (e.dataTransfer?.items) {
|
||||
const items = e.dataTransfer.items
|
||||
if (items.length === 2 && items[0].type === 'text/uri-list') {
|
||||
handleURLDrag(items, e.dataTransfer)
|
||||
} else if (items[0].type === 'text/plain') {
|
||||
const str = e.dataTransfer!.getData(items[0].type)
|
||||
if (isUrl(str)) {
|
||||
window.electron.sendRPC(IRPCActionType.UPLOAD_CHOOSED_FILES, [{ path: str }])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleURLDrag(items: DataTransferItemList, dataTransfer: DataTransfer) {
|
||||
// text/html
|
||||
// Use this data to get a more precise URL
|
||||
const urlString = dataTransfer.getData(items[1].type)
|
||||
const urlMatch = urlString.match(/<img.*src="(.*?)"/)
|
||||
if (urlMatch) {
|
||||
window.electron.sendRPC(IRPCActionType.UPLOAD_CHOOSED_FILES, [
|
||||
{
|
||||
path: urlMatch[1]
|
||||
}
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
function openUploadWindow() {
|
||||
// @ts-expect-error file-uploader
|
||||
document.getElementById('file-uploader').click()
|
||||
}
|
||||
|
||||
function onChange(e: any) {
|
||||
ipcSendFiles(e.target.files)
|
||||
// @ts-expect-error file-uploader
|
||||
document.getElementById('file-uploader').value = ''
|
||||
}
|
||||
|
||||
function ipcSendFiles(files: FileList) {
|
||||
const sendFiles: IFileWithPath[] = []
|
||||
Array.from(files).forEach(item => {
|
||||
const obj = {
|
||||
name: item.name,
|
||||
path: window.electron.showFilePath(item)
|
||||
}
|
||||
sendFiles.push(obj)
|
||||
})
|
||||
window.electron.sendRPC(IRPCActionType.UPLOAD_CHOOSED_FILES, sendFiles)
|
||||
}
|
||||
|
||||
function handleMouseDown(e: MouseEvent) {
|
||||
draggingState.value = true
|
||||
wX.value = e.pageX
|
||||
wY.value = e.pageY
|
||||
screenX.value = e.screenX
|
||||
screenY.value = e.screenY
|
||||
}
|
||||
|
||||
function handleMouseMove(e: MouseEvent) {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
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,
|
||||
y: yLoc,
|
||||
width: 64,
|
||||
height: 64
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function handleMouseUp(e: MouseEvent) {
|
||||
draggingState.value = false
|
||||
if (screenX.value === e.screenX && screenY.value === e.screenY) {
|
||||
if (e.button === 0) {
|
||||
// left mouse
|
||||
openUploadWindow()
|
||||
} else {
|
||||
openContextMenu()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function openContextMenu() {
|
||||
window.electron.sendRPC(IRPCActionType.SHOW_MINI_PAGE_MENU)
|
||||
}
|
||||
|
||||
onBeforeMount(async () => {
|
||||
await initLogoPath()
|
||||
removeListeners = window.electron.ipcRendererOn('uploadProgress', uploadProgressHandler)
|
||||
window.electron.ipcRendererOn('updateMiniIcon', updateMiniIconHandler)
|
||||
window.addEventListener('mousedown', handleMouseDown, false)
|
||||
window.addEventListener('mousemove', handleMouseMove, false)
|
||||
window.addEventListener('mouseup', handleMouseUp, false)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
removeListeners()
|
||||
window.electron.ipcRendererRemoveAllListeners('updateMiniIcon')
|
||||
window.removeEventListener('mousedown', handleMouseDown, false)
|
||||
window.removeEventListener('mousemove', handleMouseMove, false)
|
||||
window.removeEventListener('mouseup', handleMouseUp, false)
|
||||
})
|
||||
</script>
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'MiniPage'
|
||||
}
|
||||
</script>
|
||||
<style lang="stylus">
|
||||
#mini-page
|
||||
color #FFF
|
||||
height 100vh
|
||||
width 100vw
|
||||
border-radius 50%
|
||||
text-align center
|
||||
line-height 100vh
|
||||
font-size 40px
|
||||
background-size 90vh 90vw
|
||||
background-position center center
|
||||
background-repeat no-repeat
|
||||
position relative
|
||||
box-sizing border-box
|
||||
cursor pointer
|
||||
&.linux
|
||||
border-radius 0
|
||||
background-size 100vh 100vw
|
||||
#upload-area
|
||||
height 100%
|
||||
width 100%
|
||||
border-radius 50%
|
||||
&.linux
|
||||
border-radius 0
|
||||
&.uploading
|
||||
background: linear-gradient(to top, #409EFF 50%, #fff 51%)
|
||||
background-size 200%
|
||||
#upload-dragger
|
||||
height 100%
|
||||
&.is-dragover
|
||||
background rgba(0,0,0,0.3)
|
||||
#file-uploader
|
||||
display none
|
||||
</style>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,369 +1,356 @@
|
||||
<template>
|
||||
<div class="rename-container">
|
||||
<div class="rename-card">
|
||||
<form @submit.prevent="confirmName">
|
||||
<div class="form-content">
|
||||
<div class="form-group">
|
||||
<div class="input-wrapper">
|
||||
<input
|
||||
ref="fileNameInput"
|
||||
v-model="form.fileName"
|
||||
type="text"
|
||||
class="form-input"
|
||||
:class="{ 'input-error': validationError }"
|
||||
:placeholder="t('pages.rename.placeholder')"
|
||||
autofocus
|
||||
@keyup.enter="confirmName"
|
||||
@input="clearValidationError"
|
||||
>
|
||||
<button
|
||||
v-if="form.fileName"
|
||||
type="button"
|
||||
class="input-clear"
|
||||
@click="clearFileName"
|
||||
>
|
||||
<XIcon :size="16" />
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
v-if="validationError"
|
||||
class="validation-error"
|
||||
>
|
||||
{{ validationError }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="form-actions">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-secondary"
|
||||
@click="cancel"
|
||||
>
|
||||
{{ $t('common.cancel') }}
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
class="btn btn-primary"
|
||||
:disabled="!form.fileName.trim()"
|
||||
>
|
||||
{{ $t('common.confirm') }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { XIcon } from 'lucide-vue-next'
|
||||
import { nextTick, onBeforeMount, onBeforeUnmount, reactive, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { GET_RENAME_FILE_NAME, RENAME_FILE_NAME } from '@/utils/constant'
|
||||
|
||||
const { t } = useI18n()
|
||||
const id = ref<string | null>(null)
|
||||
const fileNameInput = ref<HTMLInputElement>()
|
||||
const validationError = ref<string>('')
|
||||
|
||||
const form = reactive({
|
||||
fileName: '',
|
||||
originName: ''
|
||||
})
|
||||
|
||||
const handleFileName = (newName: string, _originName: string, _id: string) => {
|
||||
form.fileName = newName
|
||||
form.originName = _originName
|
||||
id.value = _id
|
||||
nextTick(() => {
|
||||
fileNameInput.value?.focus()
|
||||
fileNameInput.value?.select()
|
||||
})
|
||||
}
|
||||
|
||||
window.electron.ipcRendererOn(RENAME_FILE_NAME, handleFileName)
|
||||
|
||||
function validateFileName (fileName: string): string {
|
||||
if (!fileName.trim()) {
|
||||
return 'File name is required'
|
||||
}
|
||||
const invalidChars = /[<>:"/\\|?*]/g
|
||||
if (invalidChars.test(fileName)) {
|
||||
return 'File name contains invalid characters'
|
||||
}
|
||||
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'
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
function confirmName () {
|
||||
const error = validateFileName(form.fileName)
|
||||
if (error) {
|
||||
validationError.value = error
|
||||
return
|
||||
}
|
||||
|
||||
window.electron.sendToMain(`${RENAME_FILE_NAME}${id.value}`, form.fileName)
|
||||
}
|
||||
|
||||
function cancel () {
|
||||
window.electron.sendToMain(`${RENAME_FILE_NAME}${id.value}`, form.originName)
|
||||
}
|
||||
|
||||
function clearFileName () {
|
||||
form.fileName = ''
|
||||
validationError.value = ''
|
||||
nextTick(() => {
|
||||
fileNameInput.value?.focus()
|
||||
})
|
||||
}
|
||||
|
||||
function clearValidationError () {
|
||||
if (validationError.value) {
|
||||
validationError.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
onBeforeMount(() => {
|
||||
window.electron.sendToMain(GET_RENAME_FILE_NAME, '')
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.electron.ipcRendererRemoveAllListeners(RENAME_FILE_NAME)
|
||||
})
|
||||
|
||||
</script>
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'RenamePage'
|
||||
}
|
||||
</script>
|
||||
<style scoped>
|
||||
.rename-container {
|
||||
padding: 2rem;
|
||||
min-height: 100vh;
|
||||
background: var(--color-background-secondary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.rename-card {
|
||||
background: var(--color-background-primary);
|
||||
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);
|
||||
border: 1px solid var(--color-border);
|
||||
width: 100%;
|
||||
max-width: 500px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Form */
|
||||
.form-content {
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.form-group:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
display: block;
|
||||
margin-bottom: 0.75rem;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.input-wrapper {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.form-input {
|
||||
width: 100%;
|
||||
padding: 0.875rem 1rem;
|
||||
padding-right: 2.5rem;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 8px;
|
||||
background: var(--color-background-primary);
|
||||
color: var(--color-text-primary);
|
||||
font-size: 0.875rem;
|
||||
transition: all 0.2s ease;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.form-input:focus {
|
||||
outline: none;
|
||||
border-color: var(--color-blue-common);
|
||||
box-shadow: 0 0 0 2px rgba(64, 158, 255, 0.2);
|
||||
}
|
||||
|
||||
.form-input.input-error {
|
||||
border-color: #f56c6c;
|
||||
box-shadow: 0 0 0 2px rgba(245, 108, 108, 0.2);
|
||||
}
|
||||
|
||||
.input-clear {
|
||||
position: absolute;
|
||||
right: 0.75rem;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 4px;
|
||||
border: none;
|
||||
background: var(--color-background-tertiary);
|
||||
color: var(--color-text-secondary);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.input-clear:hover {
|
||||
background: var(--color-background-secondary);
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.validation-error {
|
||||
margin-top: 0.5rem;
|
||||
font-size: 0.75rem;
|
||||
color: #f56c6c;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
/* Actions */
|
||||
.form-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 0.75rem;
|
||||
padding: 1rem 2rem 2rem;
|
||||
background: var(--color-background-tertiary);
|
||||
}
|
||||
|
||||
/* Buttons */
|
||||
.btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.75rem 1.5rem;
|
||||
border-radius: 8px;
|
||||
border: none;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
min-width: fit-content;
|
||||
}
|
||||
|
||||
.btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.btn:hover:not(:disabled) {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: #409eff;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover:not(:disabled) {
|
||||
background: #66b1ff;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: var(--color-background-primary);
|
||||
color: var(--color-text-primary);
|
||||
border: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.btn-secondary:hover:not(:disabled) {
|
||||
background: var(--color-background-secondary);
|
||||
border-color: var(--color-accent);
|
||||
}
|
||||
|
||||
/* Responsive Design */
|
||||
@media (max-width: 768px) {
|
||||
.rename-container {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.rename-card {
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
padding: 1rem 1.5rem 1.5rem;
|
||||
flex-direction: column-reverse;
|
||||
}
|
||||
|
||||
.btn {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.rename-container {
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/* Focus styles for accessibility */
|
||||
.btn:focus-visible,
|
||||
.input-clear:focus-visible {
|
||||
outline: 2px solid var(--color-accent);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.form-input:focus-visible {
|
||||
outline: 2px solid var(--color-accent);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* Animation for error state */
|
||||
@keyframes shake {
|
||||
0%, 100% {
|
||||
transform: translateX(0);
|
||||
}
|
||||
25% {
|
||||
transform: translateX(-4px);
|
||||
}
|
||||
75% {
|
||||
transform: translateX(4px);
|
||||
}
|
||||
}
|
||||
|
||||
.input-error {
|
||||
animation: shake 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
/* Dark mode adjustments */
|
||||
:root.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);
|
||||
}
|
||||
</style>
|
||||
<template>
|
||||
<div class="rename-container">
|
||||
<div class="rename-card">
|
||||
<form @submit.prevent="confirmName">
|
||||
<div class="form-content">
|
||||
<div class="form-group">
|
||||
<div class="input-wrapper">
|
||||
<input
|
||||
ref="fileNameInput"
|
||||
v-model="form.fileName"
|
||||
type="text"
|
||||
class="form-input"
|
||||
:class="{ 'input-error': validationError }"
|
||||
:placeholder="t('pages.rename.placeholder')"
|
||||
autofocus
|
||||
@keyup.enter="confirmName"
|
||||
@input="clearValidationError"
|
||||
/>
|
||||
<button v-if="form.fileName" type="button" class="input-clear" @click="clearFileName">
|
||||
<XIcon :size="16" />
|
||||
</button>
|
||||
</div>
|
||||
<div v-if="validationError" class="validation-error">
|
||||
{{ validationError }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="form-actions">
|
||||
<button type="button" class="btn btn-secondary" @click="cancel">
|
||||
{{ $t('common.cancel') }}
|
||||
</button>
|
||||
<button type="submit" class="btn btn-primary" :disabled="!form.fileName.trim()">
|
||||
{{ $t('common.confirm') }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { XIcon } from 'lucide-vue-next'
|
||||
import { nextTick, onBeforeMount, onBeforeUnmount, reactive, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { GET_RENAME_FILE_NAME, RENAME_FILE_NAME } from '@/utils/constant'
|
||||
|
||||
const { t } = useI18n()
|
||||
const id = ref<string | null>(null)
|
||||
const fileNameInput = ref<HTMLInputElement>()
|
||||
const validationError = ref<string>('')
|
||||
|
||||
const form = reactive({
|
||||
fileName: '',
|
||||
originName: ''
|
||||
})
|
||||
|
||||
const handleFileName = (newName: string, _originName: string, _id: string) => {
|
||||
form.fileName = newName
|
||||
form.originName = _originName
|
||||
id.value = _id
|
||||
nextTick(() => {
|
||||
fileNameInput.value?.focus()
|
||||
fileNameInput.value?.select()
|
||||
})
|
||||
}
|
||||
|
||||
window.electron.ipcRendererOn(RENAME_FILE_NAME, handleFileName)
|
||||
|
||||
function validateFileName(fileName: string): string {
|
||||
if (!fileName.trim()) {
|
||||
return 'File name is required'
|
||||
}
|
||||
const invalidChars = /[<>:"/\\|?*]/g
|
||||
if (invalidChars.test(fileName)) {
|
||||
return 'File name contains invalid characters'
|
||||
}
|
||||
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'
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
function confirmName() {
|
||||
const error = validateFileName(form.fileName)
|
||||
if (error) {
|
||||
validationError.value = error
|
||||
return
|
||||
}
|
||||
|
||||
window.electron.sendToMain(`${RENAME_FILE_NAME}${id.value}`, form.fileName)
|
||||
}
|
||||
|
||||
function cancel() {
|
||||
window.electron.sendToMain(`${RENAME_FILE_NAME}${id.value}`, form.originName)
|
||||
}
|
||||
|
||||
function clearFileName() {
|
||||
form.fileName = ''
|
||||
validationError.value = ''
|
||||
nextTick(() => {
|
||||
fileNameInput.value?.focus()
|
||||
})
|
||||
}
|
||||
|
||||
function clearValidationError() {
|
||||
if (validationError.value) {
|
||||
validationError.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
onBeforeMount(() => {
|
||||
window.electron.sendToMain(GET_RENAME_FILE_NAME, '')
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.electron.ipcRendererRemoveAllListeners(RENAME_FILE_NAME)
|
||||
})
|
||||
</script>
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'RenamePage'
|
||||
}
|
||||
</script>
|
||||
<style scoped>
|
||||
.rename-container {
|
||||
padding: 2rem;
|
||||
min-height: 100vh;
|
||||
background: var(--color-background-secondary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.rename-card {
|
||||
background: var(--color-background-primary);
|
||||
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);
|
||||
border: 1px solid var(--color-border);
|
||||
width: 100%;
|
||||
max-width: 500px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Form */
|
||||
.form-content {
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.form-group:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
display: block;
|
||||
margin-bottom: 0.75rem;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.input-wrapper {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.form-input {
|
||||
width: 100%;
|
||||
padding: 0.875rem 1rem;
|
||||
padding-right: 2.5rem;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 8px;
|
||||
background: var(--color-background-primary);
|
||||
color: var(--color-text-primary);
|
||||
font-size: 0.875rem;
|
||||
transition: all 0.2s ease;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.form-input:focus {
|
||||
outline: none;
|
||||
border-color: var(--color-blue-common);
|
||||
box-shadow: 0 0 0 2px rgba(64, 158, 255, 0.2);
|
||||
}
|
||||
|
||||
.form-input.input-error {
|
||||
border-color: #f56c6c;
|
||||
box-shadow: 0 0 0 2px rgba(245, 108, 108, 0.2);
|
||||
}
|
||||
|
||||
.input-clear {
|
||||
position: absolute;
|
||||
right: 0.75rem;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 4px;
|
||||
border: none;
|
||||
background: var(--color-background-tertiary);
|
||||
color: var(--color-text-secondary);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.input-clear:hover {
|
||||
background: var(--color-background-secondary);
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.validation-error {
|
||||
margin-top: 0.5rem;
|
||||
font-size: 0.75rem;
|
||||
color: #f56c6c;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
/* Actions */
|
||||
.form-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 0.75rem;
|
||||
padding: 1rem 2rem 2rem;
|
||||
background: var(--color-background-tertiary);
|
||||
}
|
||||
|
||||
/* Buttons */
|
||||
.btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.75rem 1.5rem;
|
||||
border-radius: 8px;
|
||||
border: none;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
min-width: fit-content;
|
||||
}
|
||||
|
||||
.btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.btn:hover:not(:disabled) {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: #409eff;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover:not(:disabled) {
|
||||
background: #66b1ff;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: var(--color-background-primary);
|
||||
color: var(--color-text-primary);
|
||||
border: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.btn-secondary:hover:not(:disabled) {
|
||||
background: var(--color-background-secondary);
|
||||
border-color: var(--color-accent);
|
||||
}
|
||||
|
||||
/* Responsive Design */
|
||||
@media (max-width: 768px) {
|
||||
.rename-container {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.rename-card {
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
padding: 1rem 1.5rem 1.5rem;
|
||||
flex-direction: column-reverse;
|
||||
}
|
||||
|
||||
.btn {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.rename-container {
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
padding: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Focus styles for accessibility */
|
||||
.btn:focus-visible,
|
||||
.input-clear:focus-visible {
|
||||
outline: 2px solid var(--color-accent);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.form-input:focus-visible {
|
||||
outline: 2px solid var(--color-accent);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* Animation for error state */
|
||||
@keyframes shake {
|
||||
0%,
|
||||
100% {
|
||||
transform: translateX(0);
|
||||
}
|
||||
25% {
|
||||
transform: translateX(-4px);
|
||||
}
|
||||
75% {
|
||||
transform: translateX(4px);
|
||||
}
|
||||
}
|
||||
|
||||
.input-error {
|
||||
animation: shake 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
/* Dark mode adjustments */
|
||||
:root.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);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -3,10 +3,7 @@
|
||||
<!-- Header -->
|
||||
<div class="shortkey-header">
|
||||
<div class="header-content">
|
||||
<KeyboardIcon
|
||||
:size="24"
|
||||
class="header-icon"
|
||||
/>
|
||||
<KeyboardIcon :size="24" class="header-icon" />
|
||||
<div>
|
||||
<h1>{{ t('pages.shortKey.title') }}</h1>
|
||||
<p>{{ ' ' }}</p>
|
||||
@@ -28,11 +25,7 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="(item, index) in list"
|
||||
:key="item.name"
|
||||
class="table-row"
|
||||
>
|
||||
<tr v-for="(item, index) in list" :key="item.name" class="table-row">
|
||||
<td class="name-cell">
|
||||
<div class="shortcut-name">
|
||||
{{ item.label ? item.label : item.name }}
|
||||
@@ -40,21 +33,12 @@
|
||||
</td>
|
||||
<td class="key-cell">
|
||||
<div class="key-binding">
|
||||
<kbd
|
||||
v-if="item.key"
|
||||
class="key-display"
|
||||
>{{ item.key }}</kbd>
|
||||
<span
|
||||
v-else
|
||||
class="no-binding"
|
||||
>{{ t('pages.shortKey.noBinding') }}</span>
|
||||
<kbd v-if="item.key" class="key-display">{{ item.key }}</kbd>
|
||||
<span v-else class="no-binding">{{ t('pages.shortKey.noBinding') }}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="status-cell">
|
||||
<span
|
||||
class="status-badge"
|
||||
:class="{ 'status-enabled': item.enable, 'status-disabled': !item.enable }"
|
||||
>
|
||||
<span class="status-badge" :class="{ 'status-enabled': item.enable, 'status-disabled': !item.enable }">
|
||||
{{ item.enable ? t('pages.shortKey.enabled') : t('pages.shortKey.disabled') }}
|
||||
</span>
|
||||
</td>
|
||||
@@ -70,10 +54,7 @@
|
||||
>
|
||||
{{ item.enable ? t('pages.shortKey.disable') : t('pages.shortKey.enable') }}
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-sm btn-secondary"
|
||||
@click="openKeyBindingDialog(item, index)"
|
||||
>
|
||||
<button class="btn btn-sm btn-secondary" @click="openKeyBindingDialog(item, index)">
|
||||
<Edit :size="14" />
|
||||
{{ t('pages.shortKey.edit') }}
|
||||
</button>
|
||||
@@ -87,20 +68,13 @@
|
||||
|
||||
<!-- Key Binding Modal -->
|
||||
<transition name="modal">
|
||||
<div
|
||||
v-if="keyBindingVisible"
|
||||
class="modal-overlay"
|
||||
@click.self="cancelKeyBinding"
|
||||
>
|
||||
<div v-if="keyBindingVisible" class="modal-overlay" @click.self="cancelKeyBinding">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h3 class="modal-title">
|
||||
{{ t('pages.shortKey.changeUpload') }}
|
||||
</h3>
|
||||
<button
|
||||
class="modal-close"
|
||||
@click="cancelKeyBinding"
|
||||
>
|
||||
<button class="modal-close" @click="cancelKeyBinding">
|
||||
<XIcon :size="20" />
|
||||
</button>
|
||||
</div>
|
||||
@@ -113,23 +87,17 @@
|
||||
:placeholder="t('pages.shortKey.pressKeys')"
|
||||
readonly
|
||||
@keydown.prevent="keyDetect($event as KeyboardEvent)"
|
||||
>
|
||||
/>
|
||||
<div class="input-hint">
|
||||
{{ t('pages.shortKey.pressHint') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button
|
||||
class="btn btn-secondary"
|
||||
@click="cancelKeyBinding"
|
||||
>
|
||||
<button class="btn btn-secondary" @click="cancelKeyBinding">
|
||||
{{ $t('CANCEL') }}
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-primary"
|
||||
@click="confirmKeyBinding"
|
||||
>
|
||||
<button class="btn btn-primary" @click="confirmKeyBinding">
|
||||
{{ $t('common.confirm') }}
|
||||
</button>
|
||||
</div>
|
||||
@@ -171,38 +139,38 @@ watch(keyBindingVisible, (val: boolean) => {
|
||||
window.electron.sendRPC(IRPCActionType.SHORTKEY_TOGGLE_SHORTKEY_MODIFIED_MODE, val)
|
||||
})
|
||||
|
||||
function calcOrigin (item: string) {
|
||||
function calcOrigin(item: string) {
|
||||
const [origin] = item.split(':')
|
||||
return origin
|
||||
}
|
||||
|
||||
function calcOriginShowName (item: string) {
|
||||
function calcOriginShowName(item: string) {
|
||||
return item.replace('picgo-plugin-', '')
|
||||
}
|
||||
|
||||
function toggleEnable (item: IShortKeyConfig) {
|
||||
function toggleEnable(item: IShortKeyConfig) {
|
||||
const status = !item.enable
|
||||
item.enable = status
|
||||
window.electron.sendRPC(IRPCActionType.SHORTKEY_BIND_OR_UNBIND, item, item.from)
|
||||
}
|
||||
|
||||
function keyDetect (event: KeyboardEvent) {
|
||||
function keyDetect(event: KeyboardEvent) {
|
||||
shortKey.value = keyBinding(event).join('+')
|
||||
}
|
||||
|
||||
async function openKeyBindingDialog (config: IShortKeyConfig, index: number) {
|
||||
async function openKeyBindingDialog(config: IShortKeyConfig, index: number) {
|
||||
command.value = `${config.from}:${config.name}`
|
||||
shortKey.value = (await getConfig(`settings.shortKey.${command.value}.key`)) || ''
|
||||
currentIndex.value = index
|
||||
keyBindingVisible.value = true
|
||||
}
|
||||
|
||||
async function cancelKeyBinding () {
|
||||
async function cancelKeyBinding() {
|
||||
keyBindingVisible.value = false
|
||||
shortKey.value = (await getConfig<string>(`settings.shortKey.${command.value}.key`)) || ''
|
||||
}
|
||||
|
||||
async function confirmKeyBinding () {
|
||||
async function confirmKeyBinding() {
|
||||
const oldKey = await getConfig<string>(`settings.shortKey.${command.value}.key`)
|
||||
const config = { ...list.value[currentIndex.value] }
|
||||
config.key = shortKey.value
|
||||
|
||||
@@ -1,284 +1,253 @@
|
||||
<template>
|
||||
<div class="toolbox-container">
|
||||
<!-- Header Card -->
|
||||
<div class="toolbox-card header-card">
|
||||
<div class="card-header">
|
||||
<div class="header-content">
|
||||
<img
|
||||
class="header-logo"
|
||||
:src="defaultLogo"
|
||||
alt="Toolbox Logo"
|
||||
>
|
||||
<div class="header-text">
|
||||
<h1 class="header-title">
|
||||
{{ t('pages.toolbox.title') }}
|
||||
</h1>
|
||||
<p class="header-subtitle">
|
||||
{{ t('pages.toolbox.description') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<template v-if="progress !== 100">
|
||||
<button
|
||||
class="action-button"
|
||||
:class="{ disabled: isLoading }"
|
||||
:disabled="isLoading"
|
||||
@click="handleCheck"
|
||||
>
|
||||
<span>{{ t('pages.toolbox.startScan') }}</span>
|
||||
</button>
|
||||
</template>
|
||||
<template v-else-if="isAllSuccess">
|
||||
<div class="success-tips">
|
||||
{{ t('pages.toolbox.success') }}
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="!isAllSuccess">
|
||||
<template v-if="canFixLength !== 0">
|
||||
<button
|
||||
class="action-button"
|
||||
@click="handleFix"
|
||||
>
|
||||
<span>{{ t('pages.toolbox.startFix') }}</span>
|
||||
</button>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="cant-fix-container">
|
||||
<span class="cant-fix-text">{{ $t('pages.toolbox.autoFixFail') }}</span>
|
||||
<button
|
||||
class="action-button secondary small"
|
||||
@click="handleCheck"
|
||||
>
|
||||
<span>{{ t('pages.toolbox.reScan') }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Progress Card -->
|
||||
<div class="toolbox-card progress-card">
|
||||
<div class="progress-container">
|
||||
<div class="progress-bar">
|
||||
<div
|
||||
class="progress-fill"
|
||||
:style="{ width: `${progress}%` }"
|
||||
/>
|
||||
</div>
|
||||
<span class="progress-text">{{ Math.round(progress) }}%</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Items Card -->
|
||||
<div class="toolbox-card items-card">
|
||||
<div class="items-list">
|
||||
<div
|
||||
v-for="(item, key) in fixList"
|
||||
:key="key"
|
||||
class="item"
|
||||
:class="{
|
||||
'item-active': activeTypes.includes(key),
|
||||
'item-error': item.status === IToolboxItemCheckStatus.ERROR,
|
||||
'item-success': item.status === IToolboxItemCheckStatus.SUCCESS,
|
||||
'item-loading': item.status === IToolboxItemCheckStatus.LOADING
|
||||
}"
|
||||
>
|
||||
<div
|
||||
class="item-header"
|
||||
@click="toggleItem(key)"
|
||||
>
|
||||
<div class="item-title">
|
||||
<span>{{ item.title }}</span>
|
||||
<toolbox-status-icon :status="item.status" />
|
||||
</div>
|
||||
<div class="item-chevron">
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<polyline points="6,9 12,15 18,9" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<transition name="item-content">
|
||||
<div
|
||||
v-if="activeTypes.includes(key)"
|
||||
class="item-content"
|
||||
>
|
||||
<div class="item-message">
|
||||
{{ item.msg || '' }}
|
||||
</div>
|
||||
<template v-if="item.handler && item.handlerText && item.value">
|
||||
<div class="item-actions">
|
||||
<toolbox-handler
|
||||
:value="item.value"
|
||||
:status="item.status"
|
||||
:handler="item.handler"
|
||||
:handler-text="item.handlerText"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, onUnmounted, reactive, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import ToolboxHandler from '@/components/ToolboxHandler.vue'
|
||||
import ToolboxStatusIcon from '@/components/ToolboxStatusIcon.vue'
|
||||
import useConfirm from '@/hooks/useConfirm'
|
||||
import { IRPCActionType, IToolboxItemCheckStatus, IToolboxItemType } from '@/utils/enum'
|
||||
import type { IToolboxCheckRes } from '#/types/rpc'
|
||||
import type { IToolboxMap } from '#/types/view'
|
||||
|
||||
const { t } = useI18n()
|
||||
const { confirm } = useConfirm()
|
||||
const activeTypes = ref<string[]>([])
|
||||
const defaultLogo = computed(() => `${import.meta.env.BASE_URL}roundLogo.png`)
|
||||
const fixList = reactive<IToolboxMap>({
|
||||
[IToolboxItemType.IS_CONFIG_FILE_BROKEN]: {
|
||||
title: t('pages.toolbox.checkConfigFileBroken'),
|
||||
status: IToolboxItemCheckStatus.INIT,
|
||||
handlerText: t('pages.toolbox.openConfigFile'),
|
||||
handler (value: string) {
|
||||
window.electron.sendRPC(IRPCActionType.OPEN_FILE, value)
|
||||
}
|
||||
},
|
||||
[IToolboxItemType.IS_GALLERY_FILE_BROKEN]: {
|
||||
title: t('pages.toolbox.checkGalleryFileBroken'),
|
||||
status: IToolboxItemCheckStatus.INIT
|
||||
},
|
||||
[IToolboxItemType.HAS_PROBLEM_WITH_CLIPBOARD_PIC_UPLOAD]: {
|
||||
title: t('pages.toolbox.checkProblemWithClipboardPicUpload'), // picgo-image-clipboard folder
|
||||
status: IToolboxItemCheckStatus.INIT,
|
||||
handlerText: t('pages.toolbox.openFilePath'),
|
||||
handler (value: string) {
|
||||
window.electron.sendRPC(IRPCActionType.OPEN_FILE, value)
|
||||
}
|
||||
},
|
||||
[IToolboxItemType.HAS_PROBLEM_WITH_PROXY]: {
|
||||
title: t('pages.toolbox.checkProblemWithProxy'),
|
||||
status: IToolboxItemCheckStatus.INIT,
|
||||
hasNoFixMethod: true
|
||||
}
|
||||
})
|
||||
|
||||
const progress = computed(() => {
|
||||
const total = Object.keys(fixList).length
|
||||
const done = Object.keys(fixList).filter(key => {
|
||||
const status = fixList[key].status
|
||||
return status !== IToolboxItemCheckStatus.INIT && status !== IToolboxItemCheckStatus.LOADING
|
||||
}).length
|
||||
return (done / total) * 100
|
||||
})
|
||||
|
||||
const isAllSuccess = computed(() => {
|
||||
return Object.keys(fixList).every(key => {
|
||||
const status = fixList[key].status
|
||||
return status === IToolboxItemCheckStatus.SUCCESS
|
||||
})
|
||||
})
|
||||
|
||||
const isLoading = computed(() => {
|
||||
return Object.keys(fixList).some(key => {
|
||||
const status = fixList[key].status
|
||||
return status === IToolboxItemCheckStatus.LOADING
|
||||
})
|
||||
})
|
||||
|
||||
const canFixLength = computed(() => {
|
||||
return Object.keys(fixList).filter(key => {
|
||||
const status = fixList[key].status
|
||||
return status === IToolboxItemCheckStatus.ERROR && !fixList[key].hasNoFixMethod
|
||||
}).length
|
||||
})
|
||||
|
||||
const toggleItem = (key: string) => {
|
||||
const index = activeTypes.value.indexOf(key)
|
||||
if (index > -1) {
|
||||
activeTypes.value.splice(index, 1)
|
||||
} else {
|
||||
activeTypes.value.push(key)
|
||||
}
|
||||
}
|
||||
|
||||
const toolboxCheckResHandler = ({ type, msg = '', status, value = '' }: IToolboxCheckRes) => {
|
||||
fixList[type].status = status
|
||||
fixList[type].msg = msg
|
||||
fixList[type].value = value
|
||||
if (status === IToolboxItemCheckStatus.ERROR) {
|
||||
activeTypes.value.push(type)
|
||||
}
|
||||
}
|
||||
|
||||
window.electron.ipcRendererOn(IRPCActionType.TOOLBOX_CHECK_RES, toolboxCheckResHandler)
|
||||
|
||||
const handleCheck = () => {
|
||||
activeTypes.value = []
|
||||
Object.keys(fixList).forEach(key => {
|
||||
fixList[key].status = IToolboxItemCheckStatus.LOADING
|
||||
fixList[key].msg = ''
|
||||
fixList[key].value = ''
|
||||
})
|
||||
window.electron.sendRPC(IRPCActionType.TOOLBOX_CHECK)
|
||||
}
|
||||
|
||||
const handleFix = async () => {
|
||||
const fixRes = await Promise.all(
|
||||
Object.keys(fixList)
|
||||
.filter(key => {
|
||||
const status = fixList[key].status
|
||||
return status === IToolboxItemCheckStatus.ERROR && !fixList[key].hasNoFixMethod
|
||||
})
|
||||
.map(async key => {
|
||||
return window.electron.triggerRPC<IToolboxCheckRes>(IRPCActionType.TOOLBOX_CHECK_FIX, key)
|
||||
})
|
||||
)
|
||||
|
||||
fixRes
|
||||
.filter(item => item !== null)
|
||||
.forEach(item => {
|
||||
if (item) {
|
||||
fixList[item.type].status = item.status
|
||||
fixList[item.type].msg = item.msg
|
||||
fixList[item.type].value = item.value
|
||||
}
|
||||
})
|
||||
|
||||
confirm({
|
||||
title: t('pages.toolbox.notice'),
|
||||
message: t('pages.toolbox.fixDoneNeedReload'),
|
||||
type: 'warning',
|
||||
confirmButtonText: t('common.confirm'),
|
||||
cancelButtonText: t('common.cancel'),
|
||||
center: true
|
||||
}).then(result => {
|
||||
if (!result) return
|
||||
window.electron.sendRPC(IRPCActionType.RELOAD_APP)
|
||||
})
|
||||
}
|
||||
|
||||
onUnmounted(() => {
|
||||
window.electron.ipcRendererRemoveAllListeners(IRPCActionType.TOOLBOX_CHECK_RES)
|
||||
})
|
||||
</script>
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'ToolBoxPage'
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped src="./css/ToolboxPage.css"></style>
|
||||
<template>
|
||||
<div class="toolbox-container">
|
||||
<!-- Header Card -->
|
||||
<div class="toolbox-card header-card">
|
||||
<div class="card-header">
|
||||
<div class="header-content">
|
||||
<img class="header-logo" :src="defaultLogo" alt="Toolbox Logo" />
|
||||
<div class="header-text">
|
||||
<h1 class="header-title">
|
||||
{{ t('pages.toolbox.title') }}
|
||||
</h1>
|
||||
<p class="header-subtitle">
|
||||
{{ t('pages.toolbox.description') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<template v-if="progress !== 100">
|
||||
<button class="action-button" :class="{ disabled: isLoading }" :disabled="isLoading" @click="handleCheck">
|
||||
<span>{{ t('pages.toolbox.startScan') }}</span>
|
||||
</button>
|
||||
</template>
|
||||
<template v-else-if="isAllSuccess">
|
||||
<div class="success-tips">
|
||||
{{ t('pages.toolbox.success') }}
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="!isAllSuccess">
|
||||
<template v-if="canFixLength !== 0">
|
||||
<button class="action-button" @click="handleFix">
|
||||
<span>{{ t('pages.toolbox.startFix') }}</span>
|
||||
</button>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="cant-fix-container">
|
||||
<span class="cant-fix-text">{{ $t('pages.toolbox.autoFixFail') }}</span>
|
||||
<button class="action-button secondary small" @click="handleCheck">
|
||||
<span>{{ t('pages.toolbox.reScan') }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Progress Card -->
|
||||
<div class="toolbox-card progress-card">
|
||||
<div class="progress-container">
|
||||
<div class="progress-bar">
|
||||
<div class="progress-fill" :style="{ width: `${progress}%` }" />
|
||||
</div>
|
||||
<span class="progress-text">{{ Math.round(progress) }}%</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Items Card -->
|
||||
<div class="toolbox-card items-card">
|
||||
<div class="items-list">
|
||||
<div
|
||||
v-for="(item, key) in fixList"
|
||||
:key="key"
|
||||
class="item"
|
||||
:class="{
|
||||
'item-active': activeTypes.includes(key),
|
||||
'item-error': item.status === IToolboxItemCheckStatus.ERROR,
|
||||
'item-success': item.status === IToolboxItemCheckStatus.SUCCESS,
|
||||
'item-loading': item.status === IToolboxItemCheckStatus.LOADING
|
||||
}"
|
||||
>
|
||||
<div class="item-header" @click="toggleItem(key)">
|
||||
<div class="item-title">
|
||||
<span>{{ item.title }}</span>
|
||||
<toolbox-status-icon :status="item.status" />
|
||||
</div>
|
||||
<div class="item-chevron">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<polyline points="6,9 12,15 18,9" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<transition name="item-content">
|
||||
<div v-if="activeTypes.includes(key)" class="item-content">
|
||||
<div class="item-message">
|
||||
{{ item.msg || '' }}
|
||||
</div>
|
||||
<template v-if="item.handler && item.handlerText && item.value">
|
||||
<div class="item-actions">
|
||||
<toolbox-handler
|
||||
:value="item.value"
|
||||
:status="item.status"
|
||||
:handler="item.handler"
|
||||
:handler-text="item.handlerText"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, onUnmounted, reactive, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import ToolboxHandler from '@/components/ToolboxHandler.vue'
|
||||
import ToolboxStatusIcon from '@/components/ToolboxStatusIcon.vue'
|
||||
import useConfirm from '@/hooks/useConfirm'
|
||||
import { IRPCActionType, IToolboxItemCheckStatus, IToolboxItemType } from '@/utils/enum'
|
||||
import type { IToolboxCheckRes } from '#/types/rpc'
|
||||
import type { IToolboxMap } from '#/types/view'
|
||||
|
||||
const { t } = useI18n()
|
||||
const { confirm } = useConfirm()
|
||||
const activeTypes = ref<string[]>([])
|
||||
const defaultLogo = computed(() => `${import.meta.env.BASE_URL}roundLogo.png`)
|
||||
const fixList = reactive<IToolboxMap>({
|
||||
[IToolboxItemType.IS_CONFIG_FILE_BROKEN]: {
|
||||
title: t('pages.toolbox.checkConfigFileBroken'),
|
||||
status: IToolboxItemCheckStatus.INIT,
|
||||
handlerText: t('pages.toolbox.openConfigFile'),
|
||||
handler(value: string) {
|
||||
window.electron.sendRPC(IRPCActionType.OPEN_FILE, value)
|
||||
}
|
||||
},
|
||||
[IToolboxItemType.IS_GALLERY_FILE_BROKEN]: {
|
||||
title: t('pages.toolbox.checkGalleryFileBroken'),
|
||||
status: IToolboxItemCheckStatus.INIT
|
||||
},
|
||||
[IToolboxItemType.HAS_PROBLEM_WITH_CLIPBOARD_PIC_UPLOAD]: {
|
||||
title: t('pages.toolbox.checkProblemWithClipboardPicUpload'), // picgo-image-clipboard folder
|
||||
status: IToolboxItemCheckStatus.INIT,
|
||||
handlerText: t('pages.toolbox.openFilePath'),
|
||||
handler(value: string) {
|
||||
window.electron.sendRPC(IRPCActionType.OPEN_FILE, value)
|
||||
}
|
||||
},
|
||||
[IToolboxItemType.HAS_PROBLEM_WITH_PROXY]: {
|
||||
title: t('pages.toolbox.checkProblemWithProxy'),
|
||||
status: IToolboxItemCheckStatus.INIT,
|
||||
hasNoFixMethod: true
|
||||
}
|
||||
})
|
||||
|
||||
const progress = computed(() => {
|
||||
const total = Object.keys(fixList).length
|
||||
const done = Object.keys(fixList).filter(key => {
|
||||
const status = fixList[key].status
|
||||
return status !== IToolboxItemCheckStatus.INIT && status !== IToolboxItemCheckStatus.LOADING
|
||||
}).length
|
||||
return (done / total) * 100
|
||||
})
|
||||
|
||||
const isAllSuccess = computed(() => {
|
||||
return Object.keys(fixList).every(key => {
|
||||
const status = fixList[key].status
|
||||
return status === IToolboxItemCheckStatus.SUCCESS
|
||||
})
|
||||
})
|
||||
|
||||
const isLoading = computed(() => {
|
||||
return Object.keys(fixList).some(key => {
|
||||
const status = fixList[key].status
|
||||
return status === IToolboxItemCheckStatus.LOADING
|
||||
})
|
||||
})
|
||||
|
||||
const canFixLength = computed(() => {
|
||||
return Object.keys(fixList).filter(key => {
|
||||
const status = fixList[key].status
|
||||
return status === IToolboxItemCheckStatus.ERROR && !fixList[key].hasNoFixMethod
|
||||
}).length
|
||||
})
|
||||
|
||||
const toggleItem = (key: string) => {
|
||||
const index = activeTypes.value.indexOf(key)
|
||||
if (index > -1) {
|
||||
activeTypes.value.splice(index, 1)
|
||||
} else {
|
||||
activeTypes.value.push(key)
|
||||
}
|
||||
}
|
||||
|
||||
const toolboxCheckResHandler = ({ type, msg = '', status, value = '' }: IToolboxCheckRes) => {
|
||||
fixList[type].status = status
|
||||
fixList[type].msg = msg
|
||||
fixList[type].value = value
|
||||
if (status === IToolboxItemCheckStatus.ERROR) {
|
||||
activeTypes.value.push(type)
|
||||
}
|
||||
}
|
||||
|
||||
window.electron.ipcRendererOn(IRPCActionType.TOOLBOX_CHECK_RES, toolboxCheckResHandler)
|
||||
|
||||
const handleCheck = () => {
|
||||
activeTypes.value = []
|
||||
Object.keys(fixList).forEach(key => {
|
||||
fixList[key].status = IToolboxItemCheckStatus.LOADING
|
||||
fixList[key].msg = ''
|
||||
fixList[key].value = ''
|
||||
})
|
||||
window.electron.sendRPC(IRPCActionType.TOOLBOX_CHECK)
|
||||
}
|
||||
|
||||
const handleFix = async () => {
|
||||
const fixRes = await Promise.all(
|
||||
Object.keys(fixList)
|
||||
.filter(key => {
|
||||
const status = fixList[key].status
|
||||
return status === IToolboxItemCheckStatus.ERROR && !fixList[key].hasNoFixMethod
|
||||
})
|
||||
.map(async key => {
|
||||
return window.electron.triggerRPC<IToolboxCheckRes>(IRPCActionType.TOOLBOX_CHECK_FIX, key)
|
||||
})
|
||||
)
|
||||
|
||||
fixRes
|
||||
.filter(item => item !== null)
|
||||
.forEach(item => {
|
||||
if (item) {
|
||||
fixList[item.type].status = item.status
|
||||
fixList[item.type].msg = item.msg
|
||||
fixList[item.type].value = item.value
|
||||
}
|
||||
})
|
||||
|
||||
confirm({
|
||||
title: t('pages.toolbox.notice'),
|
||||
message: t('pages.toolbox.fixDoneNeedReload'),
|
||||
type: 'warning',
|
||||
confirmButtonText: t('common.confirm'),
|
||||
cancelButtonText: t('common.cancel'),
|
||||
center: true
|
||||
}).then(result => {
|
||||
if (!result) return
|
||||
window.electron.sendRPC(IRPCActionType.RELOAD_APP)
|
||||
})
|
||||
}
|
||||
|
||||
onUnmounted(() => {
|
||||
window.electron.ipcRendererRemoveAllListeners(IRPCActionType.TOOLBOX_CHECK_RES)
|
||||
})
|
||||
</script>
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'ToolBoxPage'
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped src="./css/ToolboxPage.css"></style>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,448 +1,411 @@
|
||||
<template>
|
||||
<div class="upload-container">
|
||||
<!-- Header Card -->
|
||||
<div class="upload-card header-card">
|
||||
<div class="card-header">
|
||||
<div class="provider-section">
|
||||
<button
|
||||
class="provider-button"
|
||||
:title="t('pages.upload.uploadViewHint')"
|
||||
@click="handlePicBedNameClick(picBedName, picBedConfigName)"
|
||||
>
|
||||
<div class="provider-info">
|
||||
<span class="provider-name">{{ picBedName }}</span>
|
||||
<span class="provider-config">{{ picBedConfigName || 'Default' }}</span>
|
||||
</div>
|
||||
<EditIcon
|
||||
:size="16"
|
||||
class="provider-arrow"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<button
|
||||
class="action-button secondary"
|
||||
@click="handleImageProcess"
|
||||
>
|
||||
<Settings :size="16" />
|
||||
<span>{{ t('pages.upload.imageProcessName') }}</span>
|
||||
</button>
|
||||
<button
|
||||
class="action-button"
|
||||
@click="handleChangePicBed"
|
||||
>
|
||||
<ArrowLeftRightIcon :size="16" />
|
||||
<span>{{ t('pages.upload.changePicBed') }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main Upload Card -->
|
||||
<div class="upload-card main-card">
|
||||
<div
|
||||
id="upload-area"
|
||||
class="upload-zone"
|
||||
:class="{ 'drag-active': dragover }"
|
||||
@drop.prevent="onDrop"
|
||||
@dragover.prevent="dragover = true"
|
||||
@dragleave.prevent="dragover = false"
|
||||
@click="openUplodWindow"
|
||||
>
|
||||
<div class="upload-content">
|
||||
<div class="upload-icon">
|
||||
<UploadCloudIcon :size="48" />
|
||||
</div>
|
||||
<div class="upload-text">
|
||||
<h3 class="upload-title">
|
||||
{{ t('pages.upload.dragFileToHere') }}
|
||||
</h3>
|
||||
<p class="upload-subtitle">
|
||||
{{ t('pages.upload.clickToUpload') }}
|
||||
</p>
|
||||
<div class="upload-formats">
|
||||
<span class="format-label">{{ t('pages.upload.uploadHint') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<input
|
||||
id="file-uploader"
|
||||
ref="fileInput"
|
||||
type="file"
|
||||
multiple
|
||||
style="display: none"
|
||||
@change="onChange"
|
||||
>
|
||||
</div>
|
||||
|
||||
<!-- Progress Bar -->
|
||||
<transition name="progress">
|
||||
<div
|
||||
v-if="showProgress"
|
||||
class="progress-container"
|
||||
>
|
||||
<div class="progress-bar">
|
||||
<div
|
||||
class="progress-fill"
|
||||
:class="{ 'progress-error': showError }"
|
||||
:style="{ width: `${progress}%` }"
|
||||
/>
|
||||
</div>
|
||||
<span class="progress-text">
|
||||
{{ showError ? t('pages.upload.uploadFailed') : `${progress}%` }}
|
||||
</span>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
|
||||
<!-- Quick Actions Card -->
|
||||
<div class="upload-card actions-card">
|
||||
<div class="card-header">
|
||||
<h4 class="card-title">
|
||||
{{ t('pages.upload.quickUpload') }}
|
||||
</h4>
|
||||
</div>
|
||||
<div class="quick-actions">
|
||||
<button
|
||||
class="quick-action-button"
|
||||
@click="uploadClipboardFiles"
|
||||
>
|
||||
<ClipboardIcon :size="20" />
|
||||
<span>{{ t('pages.upload.clipboardPicture') }}</span>
|
||||
</button>
|
||||
<button
|
||||
class="quick-action-button"
|
||||
@click="uploadURLFiles"
|
||||
>
|
||||
<LinkIcon :size="20" />
|
||||
<span>{{ t('pages.upload.urlUpload') }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Settings Card -->
|
||||
<div class="upload-card settings-card">
|
||||
<div class="card-header">
|
||||
<h4 class="card-title">
|
||||
{{ t('pages.upload.linkFormat') }}
|
||||
</h4>
|
||||
</div>
|
||||
<div class="settings-content">
|
||||
<!-- Format Options -->
|
||||
<div class="setting-group">
|
||||
<label class="setting-label">{{ t('pages.upload.outputFormat') }}</label>
|
||||
<div class="format-buttons">
|
||||
<button
|
||||
v-for="(format, key) in pasteFormatList"
|
||||
:key="key"
|
||||
class="format-button"
|
||||
:class="{ active: pasteStyle === key }"
|
||||
:title="format"
|
||||
@click="updatePasteStyle(key)"
|
||||
>
|
||||
{{ key }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- URL Length Options -->
|
||||
<div class="setting-group">
|
||||
<label class="setting-label">{{ t('pages.upload.urlType.title') }}</label>
|
||||
<div class="url-toggle">
|
||||
<button
|
||||
class="toggle-button"
|
||||
:class="{ active: !useShortUrl }"
|
||||
@click="updateUrlType(false)"
|
||||
>
|
||||
<span>{{ t('pages.upload.urlType.normal') }}</span>
|
||||
</button>
|
||||
<button
|
||||
class="toggle-button"
|
||||
:class="{ active: useShortUrl }"
|
||||
@click="updateUrlType(true)"
|
||||
>
|
||||
<span>{{ t('pages.upload.urlType.short') }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Image Process Dialog -->
|
||||
<transition name="modal">
|
||||
<div
|
||||
v-if="imageProcessDialogVisible"
|
||||
class="modal-overlay"
|
||||
@click.stop
|
||||
>
|
||||
<div
|
||||
class="modal-container"
|
||||
@click.stop
|
||||
>
|
||||
<div class="modal-header">
|
||||
<h3 class="modal-title">
|
||||
{{ t('pages.imageProcess.title') }}
|
||||
</h3>
|
||||
<button
|
||||
class="modal-close"
|
||||
@click="imageProcessDialogVisible = false"
|
||||
>
|
||||
<XIcon :size="20" />
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-content">
|
||||
<ImageProcessSetting v-model="imageProcessDialogVisible" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ArrowLeftRightIcon, ClipboardIcon, EditIcon, LinkIcon, Settings, UploadCloudIcon, XIcon } from 'lucide-vue-next'
|
||||
import { onBeforeMount, onBeforeUnmount, ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
import ImageProcessSetting from '@/components/ImageProcessSetting.vue'
|
||||
import useMessage from '@/hooks/useMessage'
|
||||
import { PICBEDS_PAGE } from '@/router/config'
|
||||
import $bus from '@/utils/bus'
|
||||
import { isUrl } from '@/utils/common'
|
||||
import { configPaths } from '@/utils/configPaths'
|
||||
import { SHOW_INPUT_BOX, SHOW_INPUT_BOX_RESPONSE } from '@/utils/constant'
|
||||
import { getConfig, saveConfig } from '@/utils/dataSender'
|
||||
import { useDragEventListeners } from '@/utils/drag'
|
||||
import { IPasteStyle, IRPCActionType } from '@/utils/enum'
|
||||
import { picBedGlobal, updatePicBedGlobal } from '@/utils/global'
|
||||
import type { IFileWithPath, IUploaderConfigItem } from '#/types/types'
|
||||
|
||||
useDragEventListeners()
|
||||
const $router = useRouter()
|
||||
const { t } = useI18n()
|
||||
const message = useMessage()
|
||||
|
||||
const imageProcessDialogVisible = ref(false)
|
||||
const useShortUrl = ref(false)
|
||||
const dragover = ref(false)
|
||||
const progress = ref(0)
|
||||
const showProgress = ref(false)
|
||||
const showError = ref(false)
|
||||
const pasteStyle = ref('')
|
||||
const picBedName = ref('')
|
||||
const picBedConfigName = ref('')
|
||||
const fileInput = ref<HTMLInputElement>()
|
||||
|
||||
const pasteFormatList = ref<Record<string, string>>({
|
||||
[IPasteStyle.MARKDOWN]: '',
|
||||
[IPasteStyle.HTML]: '<img src="url"/>',
|
||||
[IPasteStyle.URL]: 'http://test.com/test.png',
|
||||
[IPasteStyle.UBB]: '[img]url[/img]',
|
||||
[IPasteStyle.CUSTOM]: ''
|
||||
})
|
||||
|
||||
watch(picBedGlobal, () => {
|
||||
getDefaultPicBed()
|
||||
})
|
||||
|
||||
let removeUploadProgressListenerCallback: (() => void) = () => {}
|
||||
let removeSyncPicBedListenerCallback: (() => void) = () => {}
|
||||
|
||||
function uploadProgressHandler (p: number): void {
|
||||
if (p !== -1) {
|
||||
showProgress.value = true
|
||||
progress.value = p
|
||||
} else {
|
||||
progress.value = 100
|
||||
showError.value = true
|
||||
}
|
||||
}
|
||||
|
||||
function syncPicBedHandler (): void {
|
||||
getDefaultPicBed()
|
||||
}
|
||||
|
||||
const handleImageProcess = () => {
|
||||
imageProcessDialogVisible.value = true
|
||||
}
|
||||
|
||||
watch(progress, onProgressChange)
|
||||
|
||||
function onProgressChange (val: number) {
|
||||
if (val === 100) {
|
||||
setTimeout(() => {
|
||||
showProgress.value = false
|
||||
showError.value = false
|
||||
}, 1000)
|
||||
setTimeout(() => {
|
||||
progress.value = 0
|
||||
}, 1200)
|
||||
}
|
||||
}
|
||||
|
||||
async function handlePicBedNameClick (_picBedName: string, picBedConfigName: string | undefined) {
|
||||
const formatedpicBedConfigName = picBedConfigName || 'Default'
|
||||
const currentPicBed = await getConfig<string>(configPaths.picBed.current)
|
||||
const currentPicBedConfig = ((await getConfig<any[]>(`uploader.${currentPicBed}`)) as any) || {}
|
||||
const configList = await window.electron.triggerRPC<IUploaderConfigItem>(IRPCActionType.PICBED_GET_CONFIG_LIST, currentPicBed)
|
||||
const currentConfigList = configList?.configList ?? []
|
||||
const config = currentConfigList.find((item: any) => item._configName === formatedpicBedConfigName)
|
||||
$router.push({
|
||||
name: PICBEDS_PAGE,
|
||||
params: {
|
||||
type: currentPicBed,
|
||||
configId: config?._id || ''
|
||||
},
|
||||
query: {
|
||||
defaultConfigId: currentPicBedConfig.defaultId || ''
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function onDrop (e: DragEvent) {
|
||||
dragover.value = false
|
||||
|
||||
// send files first
|
||||
if (e.dataTransfer?.files?.length) {
|
||||
ipcSendFiles(e.dataTransfer.files)
|
||||
} else if (e.dataTransfer?.items) {
|
||||
const items = e.dataTransfer.items
|
||||
if (items.length === 2 && items[0].type === 'text/uri-list') {
|
||||
handleURLDrag(items, e.dataTransfer)
|
||||
} else if (items[0].type === 'text/plain') {
|
||||
const str = e.dataTransfer.getData(items[0].type)
|
||||
if (isUrl(str)) {
|
||||
window.electron.sendRPC(IRPCActionType.UPLOAD_CHOOSED_FILES, [{ path: str }])
|
||||
} else {
|
||||
message.error(t('pages.upload.dragValidPictureOrUrl'))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleURLDrag (items: DataTransferItemList, dataTransfer: DataTransfer) {
|
||||
// text/html
|
||||
// Use this data to get a more precise URL
|
||||
const urlString = dataTransfer.getData(items[1].type)
|
||||
const urlMatch = urlString.match(/<img.*src="(.*?)"/)
|
||||
if (urlMatch) {
|
||||
window.electron.sendRPC(IRPCActionType.UPLOAD_CHOOSED_FILES, [
|
||||
{
|
||||
path: urlMatch[1]
|
||||
}
|
||||
])
|
||||
} else {
|
||||
message.error(t('pages.upload.dragValidPictureOrUrl'))
|
||||
}
|
||||
}
|
||||
|
||||
function openUplodWindow () {
|
||||
fileInput.value?.click()
|
||||
}
|
||||
|
||||
function onChange (e: any) {
|
||||
ipcSendFiles(e.target.files)
|
||||
;(fileInput.value as HTMLInputElement).value = ''
|
||||
}
|
||||
|
||||
function ipcSendFiles (files: FileList) {
|
||||
const sendFiles: IFileWithPath[] = []
|
||||
Array.from(files).forEach(item => {
|
||||
const obj = {
|
||||
name: item.name,
|
||||
path: window.electron.showFilePath(item)
|
||||
}
|
||||
sendFiles.push(obj)
|
||||
})
|
||||
window.electron.sendRPC(IRPCActionType.UPLOAD_CHOOSED_FILES, sendFiles)
|
||||
}
|
||||
|
||||
async function getPasteStyle () {
|
||||
pasteStyle.value = (await getConfig(configPaths.settings.pasteStyle)) || IPasteStyle.MARKDOWN
|
||||
pasteFormatList.value.Custom = (await getConfig(configPaths.settings.customLink)) || ''
|
||||
}
|
||||
|
||||
async function getUseShortUrl () {
|
||||
useShortUrl.value = (await getConfig(configPaths.settings.useShortUrl)) || false
|
||||
}
|
||||
|
||||
function updatePasteStyle (style: string) {
|
||||
pasteStyle.value = style
|
||||
saveConfig({
|
||||
[configPaths.settings.pasteStyle]: style || IPasteStyle.MARKDOWN
|
||||
})
|
||||
}
|
||||
|
||||
function updateUrlType (shortUrl: boolean) {
|
||||
useShortUrl.value = shortUrl
|
||||
saveConfig({
|
||||
[configPaths.settings.useShortUrl]: shortUrl
|
||||
})
|
||||
}
|
||||
|
||||
function uploadClipboardFiles () {
|
||||
window.electron.sendRPC(IRPCActionType.UPLOAD_CLIPBOARD_FILES_FROM_UPLOAD_PAGE)
|
||||
}
|
||||
|
||||
async function uploadURLFiles () {
|
||||
const str = await navigator.clipboard.readText()
|
||||
$bus.emit(SHOW_INPUT_BOX, {
|
||||
value: isUrl(str) ? str : '',
|
||||
title: t('pages.upload.inputUrlTip'),
|
||||
placeholder: t('pages.upload.httpPrefixTip')
|
||||
})
|
||||
}
|
||||
|
||||
function handleInputBoxValue (val: string) {
|
||||
if (val === '') return
|
||||
if (isUrl(val)) {
|
||||
window.electron.sendRPC(IRPCActionType.UPLOAD_CHOOSED_FILES, [
|
||||
{
|
||||
path: val
|
||||
}
|
||||
])
|
||||
} else {
|
||||
message.error(t('pages.upload.inputValidUrl'))
|
||||
}
|
||||
}
|
||||
|
||||
async function getDefaultPicBed () {
|
||||
const currentPicBed = await getConfig<string>(configPaths.picBed.current)
|
||||
picBedGlobal.value.forEach(item => {
|
||||
if (item.type === currentPicBed) {
|
||||
picBedName.value = item.name
|
||||
}
|
||||
})
|
||||
picBedConfigName.value = (await getConfig<string>(`picBed.${currentPicBed}._configName`)) || ''
|
||||
}
|
||||
|
||||
async function handleChangePicBed () {
|
||||
window.electron.sendRPC(IRPCActionType.SHOW_UPLOAD_PAGE_MENU)
|
||||
}
|
||||
|
||||
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 lang="ts">
|
||||
export default {
|
||||
name: 'UploadPage'
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped src="./css/UploadPage.css"></style>
|
||||
<template>
|
||||
<div class="upload-container">
|
||||
<!-- Header Card -->
|
||||
<div class="upload-card header-card">
|
||||
<div class="card-header">
|
||||
<div class="provider-section">
|
||||
<button
|
||||
class="provider-button"
|
||||
:title="t('pages.upload.uploadViewHint')"
|
||||
@click="handlePicBedNameClick(picBedName, picBedConfigName)"
|
||||
>
|
||||
<div class="provider-info">
|
||||
<span class="provider-name">{{ picBedName }}</span>
|
||||
<span class="provider-config">{{ picBedConfigName || 'Default' }}</span>
|
||||
</div>
|
||||
<EditIcon :size="16" class="provider-arrow" />
|
||||
</button>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<button class="action-button secondary" @click="handleImageProcess">
|
||||
<Settings :size="16" />
|
||||
<span>{{ t('pages.upload.imageProcessName') }}</span>
|
||||
</button>
|
||||
<button class="action-button" @click="handleChangePicBed">
|
||||
<ArrowLeftRightIcon :size="16" />
|
||||
<span>{{ t('pages.upload.changePicBed') }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main Upload Card -->
|
||||
<div class="upload-card main-card">
|
||||
<div
|
||||
id="upload-area"
|
||||
class="upload-zone"
|
||||
:class="{ 'drag-active': dragover }"
|
||||
@drop.prevent="onDrop"
|
||||
@dragover.prevent="dragover = true"
|
||||
@dragleave.prevent="dragover = false"
|
||||
@click="openUplodWindow"
|
||||
>
|
||||
<div class="upload-content">
|
||||
<div class="upload-icon">
|
||||
<UploadCloudIcon :size="48" />
|
||||
</div>
|
||||
<div class="upload-text">
|
||||
<h3 class="upload-title">
|
||||
{{ t('pages.upload.dragFileToHere') }}
|
||||
</h3>
|
||||
<p class="upload-subtitle">
|
||||
{{ t('pages.upload.clickToUpload') }}
|
||||
</p>
|
||||
<div class="upload-formats">
|
||||
<span class="format-label">{{ t('pages.upload.uploadHint') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<input id="file-uploader" ref="fileInput" type="file" multiple style="display: none" @change="onChange" />
|
||||
</div>
|
||||
|
||||
<!-- Progress Bar -->
|
||||
<transition name="progress">
|
||||
<div v-if="showProgress" class="progress-container">
|
||||
<div class="progress-bar">
|
||||
<div class="progress-fill" :class="{ 'progress-error': showError }" :style="{ width: `${progress}%` }" />
|
||||
</div>
|
||||
<span class="progress-text">
|
||||
{{ showError ? t('pages.upload.uploadFailed') : `${progress}%` }}
|
||||
</span>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
|
||||
<!-- Quick Actions Card -->
|
||||
<div class="upload-card actions-card">
|
||||
<div class="card-header">
|
||||
<h4 class="card-title">
|
||||
{{ t('pages.upload.quickUpload') }}
|
||||
</h4>
|
||||
</div>
|
||||
<div class="quick-actions">
|
||||
<button class="quick-action-button" @click="uploadClipboardFiles">
|
||||
<ClipboardIcon :size="20" />
|
||||
<span>{{ t('pages.upload.clipboardPicture') }}</span>
|
||||
</button>
|
||||
<button class="quick-action-button" @click="uploadURLFiles">
|
||||
<LinkIcon :size="20" />
|
||||
<span>{{ t('pages.upload.urlUpload') }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Settings Card -->
|
||||
<div class="upload-card settings-card">
|
||||
<div class="card-header">
|
||||
<h4 class="card-title">
|
||||
{{ t('pages.upload.linkFormat') }}
|
||||
</h4>
|
||||
</div>
|
||||
<div class="settings-content">
|
||||
<!-- Format Options -->
|
||||
<div class="setting-group">
|
||||
<label class="setting-label">{{ t('pages.upload.outputFormat') }}</label>
|
||||
<div class="format-buttons">
|
||||
<button
|
||||
v-for="(format, key) in pasteFormatList"
|
||||
:key="key"
|
||||
class="format-button"
|
||||
:class="{ active: pasteStyle === key }"
|
||||
:title="format"
|
||||
@click="updatePasteStyle(key)"
|
||||
>
|
||||
{{ key }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- URL Length Options -->
|
||||
<div class="setting-group">
|
||||
<label class="setting-label">{{ t('pages.upload.urlType.title') }}</label>
|
||||
<div class="url-toggle">
|
||||
<button class="toggle-button" :class="{ active: !useShortUrl }" @click="updateUrlType(false)">
|
||||
<span>{{ t('pages.upload.urlType.normal') }}</span>
|
||||
</button>
|
||||
<button class="toggle-button" :class="{ active: useShortUrl }" @click="updateUrlType(true)">
|
||||
<span>{{ t('pages.upload.urlType.short') }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Image Process Dialog -->
|
||||
<transition name="modal">
|
||||
<div v-if="imageProcessDialogVisible" class="modal-overlay" @click.stop>
|
||||
<div class="modal-container" @click.stop>
|
||||
<div class="modal-header">
|
||||
<h3 class="modal-title">
|
||||
{{ t('pages.imageProcess.title') }}
|
||||
</h3>
|
||||
<button class="modal-close" @click="imageProcessDialogVisible = false">
|
||||
<XIcon :size="20" />
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-content">
|
||||
<ImageProcessSetting v-model="imageProcessDialogVisible" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import {
|
||||
ArrowLeftRightIcon,
|
||||
ClipboardIcon,
|
||||
EditIcon,
|
||||
LinkIcon,
|
||||
Settings,
|
||||
UploadCloudIcon,
|
||||
XIcon
|
||||
} from 'lucide-vue-next'
|
||||
import { onBeforeMount, onBeforeUnmount, ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
import ImageProcessSetting from '@/components/ImageProcessSetting.vue'
|
||||
import useMessage from '@/hooks/useMessage'
|
||||
import { PICBEDS_PAGE } from '@/router/config'
|
||||
import $bus from '@/utils/bus'
|
||||
import { isUrl } from '@/utils/common'
|
||||
import { configPaths } from '@/utils/configPaths'
|
||||
import { SHOW_INPUT_BOX, SHOW_INPUT_BOX_RESPONSE } from '@/utils/constant'
|
||||
import { getConfig, saveConfig } from '@/utils/dataSender'
|
||||
import { useDragEventListeners } from '@/utils/drag'
|
||||
import { IPasteStyle, IRPCActionType } from '@/utils/enum'
|
||||
import { picBedGlobal, updatePicBedGlobal } from '@/utils/global'
|
||||
import type { IFileWithPath, IUploaderConfigItem } from '#/types/types'
|
||||
|
||||
useDragEventListeners()
|
||||
const $router = useRouter()
|
||||
const { t } = useI18n()
|
||||
const message = useMessage()
|
||||
|
||||
const imageProcessDialogVisible = ref(false)
|
||||
const useShortUrl = ref(false)
|
||||
const dragover = ref(false)
|
||||
const progress = ref(0)
|
||||
const showProgress = ref(false)
|
||||
const showError = ref(false)
|
||||
const pasteStyle = ref('')
|
||||
const picBedName = ref('')
|
||||
const picBedConfigName = ref('')
|
||||
const fileInput = ref<HTMLInputElement>()
|
||||
|
||||
const pasteFormatList = ref<Record<string, string>>({
|
||||
[IPasteStyle.MARKDOWN]: '',
|
||||
[IPasteStyle.HTML]: '<img src="url"/>',
|
||||
[IPasteStyle.URL]: 'http://test.com/test.png',
|
||||
[IPasteStyle.UBB]: '[img]url[/img]',
|
||||
[IPasteStyle.CUSTOM]: ''
|
||||
})
|
||||
|
||||
watch(picBedGlobal, () => {
|
||||
getDefaultPicBed()
|
||||
})
|
||||
|
||||
let removeUploadProgressListenerCallback: () => void = () => {}
|
||||
let removeSyncPicBedListenerCallback: () => void = () => {}
|
||||
|
||||
function uploadProgressHandler(p: number): void {
|
||||
if (p !== -1) {
|
||||
showProgress.value = true
|
||||
progress.value = p
|
||||
} else {
|
||||
progress.value = 100
|
||||
showError.value = true
|
||||
}
|
||||
}
|
||||
|
||||
function syncPicBedHandler(): void {
|
||||
getDefaultPicBed()
|
||||
}
|
||||
|
||||
const handleImageProcess = () => {
|
||||
imageProcessDialogVisible.value = true
|
||||
}
|
||||
|
||||
watch(progress, onProgressChange)
|
||||
|
||||
function onProgressChange(val: number) {
|
||||
if (val === 100) {
|
||||
setTimeout(() => {
|
||||
showProgress.value = false
|
||||
showError.value = false
|
||||
}, 1000)
|
||||
setTimeout(() => {
|
||||
progress.value = 0
|
||||
}, 1200)
|
||||
}
|
||||
}
|
||||
|
||||
async function handlePicBedNameClick(_picBedName: string, picBedConfigName: string | undefined) {
|
||||
const formatedpicBedConfigName = picBedConfigName || 'Default'
|
||||
const currentPicBed = await getConfig<string>(configPaths.picBed.current)
|
||||
const currentPicBedConfig = ((await getConfig<any[]>(`uploader.${currentPicBed}`)) as any) || {}
|
||||
const configList = await window.electron.triggerRPC<IUploaderConfigItem>(
|
||||
IRPCActionType.PICBED_GET_CONFIG_LIST,
|
||||
currentPicBed
|
||||
)
|
||||
const currentConfigList = configList?.configList ?? []
|
||||
const config = currentConfigList.find((item: any) => item._configName === formatedpicBedConfigName)
|
||||
$router.push({
|
||||
name: PICBEDS_PAGE,
|
||||
params: {
|
||||
type: currentPicBed,
|
||||
configId: config?._id || ''
|
||||
},
|
||||
query: {
|
||||
defaultConfigId: currentPicBedConfig.defaultId || ''
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function onDrop(e: DragEvent) {
|
||||
dragover.value = false
|
||||
|
||||
// send files first
|
||||
if (e.dataTransfer?.files?.length) {
|
||||
ipcSendFiles(e.dataTransfer.files)
|
||||
} else if (e.dataTransfer?.items) {
|
||||
const items = e.dataTransfer.items
|
||||
if (items.length === 2 && items[0].type === 'text/uri-list') {
|
||||
handleURLDrag(items, e.dataTransfer)
|
||||
} else if (items[0].type === 'text/plain') {
|
||||
const str = e.dataTransfer.getData(items[0].type)
|
||||
if (isUrl(str)) {
|
||||
window.electron.sendRPC(IRPCActionType.UPLOAD_CHOOSED_FILES, [{ path: str }])
|
||||
} else {
|
||||
message.error(t('pages.upload.dragValidPictureOrUrl'))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleURLDrag(items: DataTransferItemList, dataTransfer: DataTransfer) {
|
||||
// text/html
|
||||
// Use this data to get a more precise URL
|
||||
const urlString = dataTransfer.getData(items[1].type)
|
||||
const urlMatch = urlString.match(/<img.*src="(.*?)"/)
|
||||
if (urlMatch) {
|
||||
window.electron.sendRPC(IRPCActionType.UPLOAD_CHOOSED_FILES, [
|
||||
{
|
||||
path: urlMatch[1]
|
||||
}
|
||||
])
|
||||
} else {
|
||||
message.error(t('pages.upload.dragValidPictureOrUrl'))
|
||||
}
|
||||
}
|
||||
|
||||
function openUplodWindow() {
|
||||
fileInput.value?.click()
|
||||
}
|
||||
|
||||
function onChange(e: any) {
|
||||
ipcSendFiles(e.target.files)
|
||||
;(fileInput.value as HTMLInputElement).value = ''
|
||||
}
|
||||
|
||||
function ipcSendFiles(files: FileList) {
|
||||
const sendFiles: IFileWithPath[] = []
|
||||
Array.from(files).forEach(item => {
|
||||
const obj = {
|
||||
name: item.name,
|
||||
path: window.electron.showFilePath(item)
|
||||
}
|
||||
sendFiles.push(obj)
|
||||
})
|
||||
window.electron.sendRPC(IRPCActionType.UPLOAD_CHOOSED_FILES, sendFiles)
|
||||
}
|
||||
|
||||
async function getPasteStyle() {
|
||||
pasteStyle.value = (await getConfig(configPaths.settings.pasteStyle)) || IPasteStyle.MARKDOWN
|
||||
pasteFormatList.value.Custom = (await getConfig(configPaths.settings.customLink)) || ''
|
||||
}
|
||||
|
||||
async function getUseShortUrl() {
|
||||
useShortUrl.value = (await getConfig(configPaths.settings.useShortUrl)) || false
|
||||
}
|
||||
|
||||
function updatePasteStyle(style: string) {
|
||||
pasteStyle.value = style
|
||||
saveConfig({
|
||||
[configPaths.settings.pasteStyle]: style || IPasteStyle.MARKDOWN
|
||||
})
|
||||
}
|
||||
|
||||
function updateUrlType(shortUrl: boolean) {
|
||||
useShortUrl.value = shortUrl
|
||||
saveConfig({
|
||||
[configPaths.settings.useShortUrl]: shortUrl
|
||||
})
|
||||
}
|
||||
|
||||
function uploadClipboardFiles() {
|
||||
window.electron.sendRPC(IRPCActionType.UPLOAD_CLIPBOARD_FILES_FROM_UPLOAD_PAGE)
|
||||
}
|
||||
|
||||
async function uploadURLFiles() {
|
||||
const str = await navigator.clipboard.readText()
|
||||
$bus.emit(SHOW_INPUT_BOX, {
|
||||
value: isUrl(str) ? str : '',
|
||||
title: t('pages.upload.inputUrlTip'),
|
||||
placeholder: t('pages.upload.httpPrefixTip')
|
||||
})
|
||||
}
|
||||
|
||||
function handleInputBoxValue(val: string) {
|
||||
if (val === '') return
|
||||
if (isUrl(val)) {
|
||||
window.electron.sendRPC(IRPCActionType.UPLOAD_CHOOSED_FILES, [
|
||||
{
|
||||
path: val
|
||||
}
|
||||
])
|
||||
} else {
|
||||
message.error(t('pages.upload.inputValidUrl'))
|
||||
}
|
||||
}
|
||||
|
||||
async function getDefaultPicBed() {
|
||||
const currentPicBed = await getConfig<string>(configPaths.picBed.current)
|
||||
picBedGlobal.value.forEach(item => {
|
||||
if (item.type === currentPicBed) {
|
||||
picBedName.value = item.name
|
||||
}
|
||||
})
|
||||
picBedConfigName.value = (await getConfig<string>(`picBed.${currentPicBed}._configName`)) || ''
|
||||
}
|
||||
|
||||
async function handleChangePicBed() {
|
||||
window.electron.sendRPC(IRPCActionType.SHOW_UPLOAD_PAGE_MENU)
|
||||
}
|
||||
|
||||
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 lang="ts">
|
||||
export default {
|
||||
name: 'UploadPage'
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped src="./css/UploadPage.css"></style>
|
||||
|
||||
@@ -1,203 +1,196 @@
|
||||
<template>
|
||||
<div class="config-container">
|
||||
<!-- Header Card -->
|
||||
<div class="config-card header-card">
|
||||
<div class="card-header">
|
||||
<h1 class="page-title">
|
||||
{{ t('pages.uploaderConfig.title') }}
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Config Items Card -->
|
||||
<div class="config-card main-card">
|
||||
<div class="config-grid">
|
||||
<div
|
||||
v-for="item in curConfigList"
|
||||
:key="item._id"
|
||||
:class="`config-item ${defaultConfigId === item._id ? 'selected' : ''}`"
|
||||
@click="() => selectItem(item._id)"
|
||||
>
|
||||
<div class="config-content">
|
||||
<div class="config-name">
|
||||
{{ item._configName }}
|
||||
</div>
|
||||
<div class="config-update-time">
|
||||
{{ formatTime(item._updatedAt) }}
|
||||
</div>
|
||||
<div
|
||||
v-if="defaultConfigId === item._id"
|
||||
class="default-badge"
|
||||
>
|
||||
{{ t('pages.uploaderConfig.selected') }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="config-actions">
|
||||
<button
|
||||
class="action-btn edit-btn"
|
||||
:title="t('pages.uploaderConfig.edit')"
|
||||
@click.stop="openEditPage(item._id)"
|
||||
>
|
||||
<Edit :size="16" />
|
||||
</button>
|
||||
<button
|
||||
class="action-btn delete-btn"
|
||||
:class="curConfigList.length <= 1 ? 'disabled' : ''"
|
||||
:title="t('pages.uploaderConfig.delete')"
|
||||
:disabled="curConfigList.length <= 1"
|
||||
@click.stop="() => deleteConfig(item._id)"
|
||||
>
|
||||
<Trash2 :size="16" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Add New Config Button -->
|
||||
<div
|
||||
class="config-item config-item-add"
|
||||
@click="addNewConfig"
|
||||
>
|
||||
<div class="add-content">
|
||||
<Plus :size="32" />
|
||||
<span class="add-text">{{ t('pages.uploaderConfig.addNew') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Actions Card -->
|
||||
<div class="config-card actions-card">
|
||||
<div class="card-actions">
|
||||
<button
|
||||
class="primary-button"
|
||||
:disabled="store?.state.defaultPicBed === type"
|
||||
@click="setDefaultPicBed(type)"
|
||||
>
|
||||
<DatabaseIcon :size="16" />
|
||||
<span>{{ t('pages.uploaderConfig.setAsDefault') }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import dayjs from 'dayjs'
|
||||
import { DatabaseIcon, Edit, Plus, Trash2 } from 'lucide-vue-next'
|
||||
import { onBeforeMount, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { onBeforeRouteUpdate, useRoute, useRouter } from 'vue-router'
|
||||
|
||||
import useConfirm from '@/hooks/useConfirm'
|
||||
import useMessage from '@/hooks/useMessage'
|
||||
import { useStore } from '@/hooks/useStore'
|
||||
import { PICBEDS_PAGE, UPLOADER_CONFIG_PAGE } from '@/router/config'
|
||||
import { configPaths } from '@/utils/configPaths'
|
||||
import { saveConfig } from '@/utils/dataSender'
|
||||
import { IRPCActionType } from '@/utils/enum'
|
||||
import type { IStringKeyMap, IUploaderConfigItem } from '#/types/types'
|
||||
|
||||
const { t } = useI18n()
|
||||
const message = useMessage()
|
||||
const { confirm } = useConfirm()
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
|
||||
const type = ref('')
|
||||
const curConfigList = ref<IStringKeyMap[]>([])
|
||||
const defaultConfigId = ref('')
|
||||
const store = useStore()
|
||||
|
||||
async function selectItem (id: string) {
|
||||
await window.electron.triggerRPC<void>(IRPCActionType.UPLOADER_SELECT, type.value, id)
|
||||
if (store?.state.defaultPicBed === type.value) {
|
||||
window.electron.sendRPC(
|
||||
IRPCActionType.TRAY_SET_TOOL_TIP,
|
||||
`${type.value} ${curConfigList.value.find(item => item._id === id)?._configName || ''}`
|
||||
)
|
||||
}
|
||||
defaultConfigId.value = id
|
||||
}
|
||||
|
||||
onBeforeRouteUpdate((to, _, next) => {
|
||||
if (to.params.type && to.name === UPLOADER_CONFIG_PAGE) {
|
||||
type.value = to.params.type as string
|
||||
getCurrentConfigList()
|
||||
}
|
||||
next()
|
||||
})
|
||||
|
||||
onBeforeMount(() => {
|
||||
type.value = route.params.type as string
|
||||
getCurrentConfigList()
|
||||
})
|
||||
|
||||
async function getCurrentConfigList () {
|
||||
const configList = await window.electron.triggerRPC<IUploaderConfigItem>(IRPCActionType.PICBED_GET_CONFIG_LIST, type.value)
|
||||
curConfigList.value = configList?.configList ?? []
|
||||
defaultConfigId.value = configList?.defaultId ?? ''
|
||||
}
|
||||
|
||||
function openEditPage (configId: string) {
|
||||
router.push({
|
||||
name: PICBEDS_PAGE,
|
||||
params: {
|
||||
type: type.value,
|
||||
configId
|
||||
},
|
||||
query: {
|
||||
defaultConfigId: defaultConfigId.value
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function formatTime (time: number): string {
|
||||
return dayjs(time).format('YYYY-MM-DD HH:mm')
|
||||
}
|
||||
|
||||
async function deleteConfig (id: string) {
|
||||
const result = await confirm({
|
||||
title: t('pages.uploaderConfig.deleteTitle'),
|
||||
message: t('pages.uploaderConfig.deleteConfirm'),
|
||||
type: 'warning',
|
||||
confirmButtonText: t('common.confirm'),
|
||||
cancelButtonText: t('common.cancel'),
|
||||
center: true
|
||||
})
|
||||
if (!result) return
|
||||
const res = await window.electron.triggerRPC<IUploaderConfigItem>(IRPCActionType.PICBED_DELETE_CONFIG, type.value, id)
|
||||
if (!res) return
|
||||
curConfigList.value = res.configList
|
||||
defaultConfigId.value = res.defaultId
|
||||
message.success(t('pages.uploaderConfig.deleteSuccess'))
|
||||
}
|
||||
|
||||
function addNewConfig () {
|
||||
router.push({
|
||||
name: PICBEDS_PAGE,
|
||||
params: {
|
||||
type: type.value,
|
||||
configId: ''
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function setDefaultPicBed (type: string) {
|
||||
saveConfig({
|
||||
[configPaths.picBed.current]: type,
|
||||
[configPaths.picBed.uploader]: type
|
||||
})
|
||||
|
||||
store?.setDefaultPicBed(type)
|
||||
const currentConfigName = curConfigList.value.find(item => item._id === defaultConfigId.value)?._configName
|
||||
window.electron.sendRPC(IRPCActionType.TRAY_SET_TOOL_TIP, `${type} ${currentConfigName || ''}`)
|
||||
message.success(t('pages.uploaderConfig.setSuccess'))
|
||||
}
|
||||
</script>
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'UploaderConfigPage'
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped src="./css/UploaderConfigPage.css"></style>
|
||||
<template>
|
||||
<div class="config-container">
|
||||
<!-- Header Card -->
|
||||
<div class="config-card header-card">
|
||||
<div class="card-header">
|
||||
<h1 class="page-title">
|
||||
{{ t('pages.uploaderConfig.title') }}
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Config Items Card -->
|
||||
<div class="config-card main-card">
|
||||
<div class="config-grid">
|
||||
<div
|
||||
v-for="item in curConfigList"
|
||||
:key="item._id"
|
||||
:class="`config-item ${defaultConfigId === item._id ? 'selected' : ''}`"
|
||||
@click="() => selectItem(item._id)"
|
||||
>
|
||||
<div class="config-content">
|
||||
<div class="config-name">
|
||||
{{ item._configName }}
|
||||
</div>
|
||||
<div class="config-update-time">
|
||||
{{ formatTime(item._updatedAt) }}
|
||||
</div>
|
||||
<div v-if="defaultConfigId === item._id" class="default-badge">
|
||||
{{ t('pages.uploaderConfig.selected') }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="config-actions">
|
||||
<button
|
||||
class="action-btn edit-btn"
|
||||
:title="t('pages.uploaderConfig.edit')"
|
||||
@click.stop="openEditPage(item._id)"
|
||||
>
|
||||
<Edit :size="16" />
|
||||
</button>
|
||||
<button
|
||||
class="action-btn delete-btn"
|
||||
:class="curConfigList.length <= 1 ? 'disabled' : ''"
|
||||
:title="t('pages.uploaderConfig.delete')"
|
||||
:disabled="curConfigList.length <= 1"
|
||||
@click.stop="() => deleteConfig(item._id)"
|
||||
>
|
||||
<Trash2 :size="16" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Add New Config Button -->
|
||||
<div class="config-item config-item-add" @click="addNewConfig">
|
||||
<div class="add-content">
|
||||
<Plus :size="32" />
|
||||
<span class="add-text">{{ t('pages.uploaderConfig.addNew') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Actions Card -->
|
||||
<div class="config-card actions-card">
|
||||
<div class="card-actions">
|
||||
<button class="primary-button" :disabled="store?.state.defaultPicBed === type" @click="setDefaultPicBed(type)">
|
||||
<DatabaseIcon :size="16" />
|
||||
<span>{{ t('pages.uploaderConfig.setAsDefault') }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import dayjs from 'dayjs'
|
||||
import { DatabaseIcon, Edit, Plus, Trash2 } from 'lucide-vue-next'
|
||||
import { onBeforeMount, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { onBeforeRouteUpdate, useRoute, useRouter } from 'vue-router'
|
||||
|
||||
import useConfirm from '@/hooks/useConfirm'
|
||||
import useMessage from '@/hooks/useMessage'
|
||||
import { useStore } from '@/hooks/useStore'
|
||||
import { PICBEDS_PAGE, UPLOADER_CONFIG_PAGE } from '@/router/config'
|
||||
import { configPaths } from '@/utils/configPaths'
|
||||
import { saveConfig } from '@/utils/dataSender'
|
||||
import { IRPCActionType } from '@/utils/enum'
|
||||
import type { IStringKeyMap, IUploaderConfigItem } from '#/types/types'
|
||||
|
||||
const { t } = useI18n()
|
||||
const message = useMessage()
|
||||
const { confirm } = useConfirm()
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
|
||||
const type = ref('')
|
||||
const curConfigList = ref<IStringKeyMap[]>([])
|
||||
const defaultConfigId = ref('')
|
||||
const store = useStore()
|
||||
|
||||
async function selectItem(id: string) {
|
||||
await window.electron.triggerRPC<void>(IRPCActionType.UPLOADER_SELECT, type.value, id)
|
||||
if (store?.state.defaultPicBed === type.value) {
|
||||
window.electron.sendRPC(
|
||||
IRPCActionType.TRAY_SET_TOOL_TIP,
|
||||
`${type.value} ${curConfigList.value.find(item => item._id === id)?._configName || ''}`
|
||||
)
|
||||
}
|
||||
defaultConfigId.value = id
|
||||
}
|
||||
|
||||
onBeforeRouteUpdate((to, _, next) => {
|
||||
if (to.params.type && to.name === UPLOADER_CONFIG_PAGE) {
|
||||
type.value = to.params.type as string
|
||||
getCurrentConfigList()
|
||||
}
|
||||
next()
|
||||
})
|
||||
|
||||
onBeforeMount(() => {
|
||||
type.value = route.params.type as string
|
||||
getCurrentConfigList()
|
||||
})
|
||||
|
||||
async function getCurrentConfigList() {
|
||||
const configList = await window.electron.triggerRPC<IUploaderConfigItem>(
|
||||
IRPCActionType.PICBED_GET_CONFIG_LIST,
|
||||
type.value
|
||||
)
|
||||
curConfigList.value = configList?.configList ?? []
|
||||
defaultConfigId.value = configList?.defaultId ?? ''
|
||||
}
|
||||
|
||||
function openEditPage(configId: string) {
|
||||
router.push({
|
||||
name: PICBEDS_PAGE,
|
||||
params: {
|
||||
type: type.value,
|
||||
configId
|
||||
},
|
||||
query: {
|
||||
defaultConfigId: defaultConfigId.value
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function formatTime(time: number): string {
|
||||
return dayjs(time).format('YYYY-MM-DD HH:mm')
|
||||
}
|
||||
|
||||
async function deleteConfig(id: string) {
|
||||
const result = await confirm({
|
||||
title: t('pages.uploaderConfig.deleteTitle'),
|
||||
message: t('pages.uploaderConfig.deleteConfirm'),
|
||||
type: 'warning',
|
||||
confirmButtonText: t('common.confirm'),
|
||||
cancelButtonText: t('common.cancel'),
|
||||
center: true
|
||||
})
|
||||
if (!result) return
|
||||
const res = await window.electron.triggerRPC<IUploaderConfigItem>(IRPCActionType.PICBED_DELETE_CONFIG, type.value, id)
|
||||
if (!res) return
|
||||
curConfigList.value = res.configList
|
||||
defaultConfigId.value = res.defaultId
|
||||
message.success(t('pages.uploaderConfig.deleteSuccess'))
|
||||
}
|
||||
|
||||
function addNewConfig() {
|
||||
router.push({
|
||||
name: PICBEDS_PAGE,
|
||||
params: {
|
||||
type: type.value,
|
||||
configId: ''
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function setDefaultPicBed(type: string) {
|
||||
saveConfig({
|
||||
[configPaths.picBed.current]: type,
|
||||
[configPaths.picBed.uploader]: type
|
||||
})
|
||||
|
||||
store?.setDefaultPicBed(type)
|
||||
const currentConfigName = curConfigList.value.find(item => item._id === defaultConfigId.value)?._configName
|
||||
window.electron.sendRPC(IRPCActionType.TRAY_SET_TOOL_TIP, `${type} ${currentConfigName || ''}`)
|
||||
message.success(t('pages.uploaderConfig.setSuccess'))
|
||||
}
|
||||
</script>
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'UploaderConfigPage'
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped src="./css/UploaderConfigPage.css"></style>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user