🎨 Style(custom): lint code

This commit is contained in:
Kuingsmile
2025-08-15 13:29:09 +08:00
parent 0ae27cfeef
commit f11a4264d0
160 changed files with 18208 additions and 20414 deletions

View File

@@ -1,60 +1,56 @@
<template>
<div
id="app"
:key="pageReloadCount"
>
<router-view />
<UIServiceProvider />
</div>
</template>
<script lang="ts" setup>
import type { IConfig } from 'piclist'
import { onBeforeMount, onMounted } from 'vue'
import UIServiceProvider from '@/components/ui/UIServiceProvider.vue'
import { useATagClick } from '@/hooks/useATagClick'
import { useStore } from '@/hooks/useStore'
import { getConfig } from '@/utils/dataSender'
import { pageReloadCount } from '@/utils/global'
import { useAppStore } from './hooks/useAppStore'
useATagClick()
const store = useStore()
const appStore = useAppStore()
onBeforeMount(async () => {
const config = await getConfig<IConfig>()
if (config) {
store?.setDefaultPicBed(config?.picBed?.uploader || config?.picBed?.current || 'smms')
}
})
onMounted(async () => {
try {
appStore.init()
} catch (error) {
console.error('Failed to load settings:', error)
}
})
</script>
<script lang="ts">
export default {
name: 'PicGoApp'
}
</script>
<style lang="stylus">
body,
html
padding 0
margin 0
height 100%
#app
height 100%
user-select none
</style>
<template>
<div id="app" :key="pageReloadCount">
<router-view />
<UIServiceProvider />
</div>
</template>
<script lang="ts" setup>
import type { IConfig } from 'piclist'
import { onBeforeMount, onMounted } from 'vue'
import UIServiceProvider from '@/components/ui/UIServiceProvider.vue'
import { useATagClick } from '@/hooks/useATagClick'
import { useStore } from '@/hooks/useStore'
import { getConfig } from '@/utils/dataSender'
import { pageReloadCount } from '@/utils/global'
import { useAppStore } from './hooks/useAppStore'
useATagClick()
const store = useStore()
const appStore = useAppStore()
onBeforeMount(async () => {
const config = await getConfig<IConfig>()
if (config) {
store?.setDefaultPicBed(config?.picBed?.uploader || config?.picBed?.current || 'smms')
}
})
onMounted(async () => {
try {
appStore.init()
} catch (error) {
console.error('Failed to load settings:', error)
}
})
</script>
<script lang="ts">
export default {
name: 'PicGoApp'
}
</script>
<style lang="stylus">
body,
html
padding 0
margin 0
height 100%
#app
height 100%
user-select none
</style>

View File

@@ -3,7 +3,7 @@ import { IRPCActionType } from '@/utils/enum'
import type { IStringKeyMap } from '#/types/types'
export default class ALLApi {
static async delete (configMap: IStringKeyMap): Promise<boolean> {
static async delete(configMap: IStringKeyMap): Promise<boolean> {
return (await window.electron.triggerRPC(IRPCActionType.DELETE_ALL_API, getRawData(configMap))) || false
}
}

View File

@@ -1,115 +1,107 @@
<template>
<div class="image-container">
<div
v-if="isLoading"
class="loading-placeholder"
>
<div class="loading-spinner" />
</div>
<img
v-else-if="!hasError"
:src="
isShowThumbnail && item.isImage
? base64Image
: `./assets/icons/${getFileIconPath(item.fileName ?? '')}`
"
alt=""
class="image"
@load="handleImageLoad"
@error="handleImageError"
>
<img
v-else
:src="`./assets/icons/${getFileIconPath(item.fileName ?? '')}`"
alt=""
class="image"
>
</div>
</template>
<script lang="ts" setup>
import { onBeforeMount, ref } from 'vue'
import { getFileIconPath } from '@/manage/utils/common'
const base64Image = ref('')
const isLoading = ref(true)
const hasError = ref(false)
const props = defineProps<{
isShowThumbnail: boolean
item: {
isImage: boolean
fileName: string
}
localPath: string
}>()
const createBase64Image = async () => {
try {
const filePath = window.node.path.normalize(props.localPath)
const base64 = await window.node.fs.readFile(filePath, 'base64')
base64Image.value = `data:${window.node.mime.lookup(filePath) || 'image/png'};base64,${base64}`
isLoading.value = false
} catch (e) {
console.log(e)
hasError.value = true
isLoading.value = false
}
}
const handleImageLoad = () => {
isLoading.value = false
hasError.value = false
}
const handleImageError = () => {
isLoading.value = false
hasError.value = true
}
onBeforeMount(async () => {
await createBase64Image()
})
</script>
<style scoped>
.image-container {
height: 100px;
width: 100%;
margin: 0 auto;
display: flex;
align-items: center;
justify-content: center;
position: relative;
}
.image {
max-height: 100%;
max-width: 100%;
object-fit: contain;
display: block;
}
.loading-placeholder {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
}
.loading-spinner {
width: 24px;
height: 24px;
border: 2px solid #e4e7ed;
border-top: 2px solid #409eff;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
</style>
<template>
<div class="image-container">
<div v-if="isLoading" class="loading-placeholder">
<div class="loading-spinner" />
</div>
<img
v-else-if="!hasError"
:src="isShowThumbnail && item.isImage ? base64Image : `./assets/icons/${getFileIconPath(item.fileName ?? '')}`"
alt=""
class="image"
@load="handleImageLoad"
@error="handleImageError"
/>
<img v-else :src="`./assets/icons/${getFileIconPath(item.fileName ?? '')}`" alt="" class="image" />
</div>
</template>
<script lang="ts" setup>
import { onBeforeMount, ref } from 'vue'
import { getFileIconPath } from '@/manage/utils/common'
const base64Image = ref('')
const isLoading = ref(true)
const hasError = ref(false)
const props = defineProps<{
isShowThumbnail: boolean
item: {
isImage: boolean
fileName: string
}
localPath: string
}>()
const createBase64Image = async () => {
try {
const filePath = window.node.path.normalize(props.localPath)
const base64 = await window.node.fs.readFile(filePath, 'base64')
base64Image.value = `data:${window.node.mime.lookup(filePath) || 'image/png'};base64,${base64}`
isLoading.value = false
} catch (e) {
console.log(e)
hasError.value = true
isLoading.value = false
}
}
const handleImageLoad = () => {
isLoading.value = false
hasError.value = false
}
const handleImageError = () => {
isLoading.value = false
hasError.value = true
}
onBeforeMount(async () => {
await createBase64Image()
})
</script>
<style scoped>
.image-container {
height: 100px;
width: 100%;
margin: 0 auto;
display: flex;
align-items: center;
justify-content: center;
position: relative;
}
.image {
max-height: 100%;
max-width: 100%;
object-fit: contain;
display: block;
}
.loading-placeholder {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
}
.loading-spinner {
width: 24px;
height: 24px;
border: 2px solid #e4e7ed;
border-top: 2px solid #409eff;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
</style>

View File

@@ -1,123 +1,123 @@
<template>
<div class="image-container">
<div
v-if="isLoading"
class="loading-placeholder"
>
<div class="loading-spinner" />
</div>
<img
v-else-if="!hasError"
:src="imageSource"
alt=""
class="image"
@load="handleImageLoad"
@error="handleImageError"
>
<img
v-else
:src="iconPath"
alt=""
class="image"
>
</div>
</template>
<script lang="ts" setup>
import { computed, onMounted, ref, watch } from 'vue'
import { getFileIconPath } from '@/manage/utils/common'
import { IRPCActionType } from '@/utils/enum'
const preSignedUrl = ref('')
const isLoading = ref(true)
const hasError = ref(false)
const props = defineProps<{
item: {
key: string
isImage: boolean
fileName: string | null | undefined
}
alias: string
url: string
config: any
isShowThumbnail: boolean
}>()
const imageSource = computed(() => {
return props.isShowThumbnail && props.item.isImage
? preSignedUrl.value
: `./assets/icons/${getFileIconPath(props.item.fileName ?? '')}`
})
const iconPath = computed(() => `./assets/icons/${getFileIconPath(props.item.fileName ?? '')}`)
async function getUrl () {
try {
isLoading.value = true
hasError.value = false
preSignedUrl.value = await window.electron.triggerRPC<any>(IRPCActionType.MANAGE_GET_PRE_SIGNED_URL, props.alias, props.config)
isLoading.value = false
} catch (error) {
console.error('Failed to get pre-signed URL:', error)
hasError.value = true
isLoading.value = false
}
}
const handleImageLoad = () => {
isLoading.value = false
hasError.value = false
}
const handleImageError = () => {
isLoading.value = false
hasError.value = true
}
watch(() => [props.url, props.item], getUrl, { deep: true })
onMounted(getUrl)
</script>
<style scoped>
.image-container {
height: 100px;
width: 100%;
margin: 0 auto;
display: flex;
align-items: center;
justify-content: center;
position: relative;
}
.image {
max-height: 100%;
max-width: 100%;
object-fit: contain;
display: block;
}
.loading-placeholder {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
}
.loading-spinner {
width: 24px;
height: 24px;
border: 2px solid #e4e7ed;
border-top: 2px solid #409eff;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
</style>
<template>
<div class="image-container">
<div v-if="isLoading" class="loading-placeholder">
<div class="loading-spinner" />
</div>
<img
v-else-if="!hasError"
:src="imageSource"
alt=""
class="image"
@load="handleImageLoad"
@error="handleImageError"
/>
<img v-else :src="iconPath" alt="" class="image" />
</div>
</template>
<script lang="ts" setup>
import { computed, onMounted, ref, watch } from 'vue'
import { getFileIconPath } from '@/manage/utils/common'
import { IRPCActionType } from '@/utils/enum'
const preSignedUrl = ref('')
const isLoading = ref(true)
const hasError = ref(false)
const props = defineProps<{
item: {
key: string
isImage: boolean
fileName: string | null | undefined
}
alias: string
url: string
config: any
isShowThumbnail: boolean
}>()
const imageSource = computed(() => {
return props.isShowThumbnail && props.item.isImage
? preSignedUrl.value
: `./assets/icons/${getFileIconPath(props.item.fileName ?? '')}`
})
const iconPath = computed(() => `./assets/icons/${getFileIconPath(props.item.fileName ?? '')}`)
async function getUrl() {
try {
isLoading.value = true
hasError.value = false
preSignedUrl.value = await window.electron.triggerRPC<any>(
IRPCActionType.MANAGE_GET_PRE_SIGNED_URL,
props.alias,
props.config
)
isLoading.value = false
} catch (error) {
console.error('Failed to get pre-signed URL:', error)
hasError.value = true
isLoading.value = false
}
}
const handleImageLoad = () => {
isLoading.value = false
hasError.value = false
}
const handleImageError = () => {
isLoading.value = false
hasError.value = true
}
watch(() => [props.url, props.item], getUrl, { deep: true })
onMounted(getUrl)
</script>
<style scoped>
.image-container {
height: 100px;
width: 100%;
margin: 0 auto;
display: flex;
align-items: center;
justify-content: center;
position: relative;
}
.image {
max-height: 100%;
max-width: 100%;
object-fit: contain;
display: block;
}
.loading-placeholder {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
}
.loading-spinner {
width: 24px;
height: 24px;
border: 2px solid #e4e7ed;
border-top: 2px solid #409eff;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
</style>

File diff suppressed because it is too large Load Diff

View File

@@ -1,154 +1,150 @@
<template>
<div class="image-container">
<div
v-if="isLoading"
class="loading-placeholder"
>
<div class="loading-spinner" />
</div>
<img
v-else-if="!hasError"
:src="imageSource"
alt=""
class="image"
@load="handleImageLoad"
@error="handleImageError"
>
<img
v-else
:src="iconPath"
alt=""
class="image"
>
</div>
</template>
<script lang="ts" setup>
import { computed, onMounted, ref, watch } from 'vue'
import { getFileIconPath } from '@/manage/utils/common'
import { getAuthHeader } from '@/manage/utils/digestAuth'
import { formatEndpoint } from '@/utils/common'
const base64Url = ref('')
const success = ref(false)
const isLoading = ref(true)
const hasError = ref(false)
const props = defineProps<{
item: {
key: string
isImage: boolean
fileName: string | null | undefined
}
url: string
config: any
isShowThumbnail: boolean
}>()
const imageSource = computed(() => {
return props.isShowThumbnail && props.item.isImage && success.value
? base64Url.value
: `./assets/icons/${getFileIconPath(props.item.fileName ?? '')}`
})
const iconPath = computed(() => `./assets/icons/${getFileIconPath(props.item.fileName ?? '')}`)
async function getWebdavHeader (key: string) {
let headers = {} as any
if (props.config.authType === 'digest') {
const authHeader = await getAuthHeader(
'GET',
formatEndpoint(props.config.endpoint, props.config.sslEnabled || false),
`/${key.replace(/^\//, '')}`,
props.config.username,
props.config.password
)
headers = {
Authorization: authHeader
}
} else {
headers = {
Authorization: 'Basic ' + Buffer.from(`${props.config.username}:${props.config.password}`).toString('base64')
}
}
return headers
}
const fetchImage = async () => {
try {
isLoading.value = true
hasError.value = false
const headers = await getWebdavHeader(props.item.key)
const res = await fetch(props.url, { method: 'GET', headers })
if (res.status >= 200 && res.status < 300) {
const blob = await res.blob()
success.value = true
base64Url.value = URL.createObjectURL(blob)
isLoading.value = false
} else {
throw new Error('Network response was not ok.')
}
} catch (err) {
success.value = false
hasError.value = true
isLoading.value = false
console.log(err)
}
}
const handleImageLoad = () => {
isLoading.value = false
hasError.value = false
}
const handleImageError = () => {
isLoading.value = false
hasError.value = true
}
watch(() => [props.url, props.item], fetchImage, { deep: true })
onMounted(fetchImage)
</script>
<style scoped>
.image-container {
height: 100px;
width: 100%;
margin: 0 auto;
display: flex;
align-items: center;
justify-content: center;
position: relative;
}
.image {
max-height: 100%;
max-width: 100%;
object-fit: contain;
display: block;
}
.loading-placeholder {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
}
.loading-spinner {
width: 24px;
height: 24px;
border: 2px solid #e4e7ed;
border-top: 2px solid #409eff;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
</style>
<template>
<div class="image-container">
<div v-if="isLoading" class="loading-placeholder">
<div class="loading-spinner" />
</div>
<img
v-else-if="!hasError"
:src="imageSource"
alt=""
class="image"
@load="handleImageLoad"
@error="handleImageError"
/>
<img v-else :src="iconPath" alt="" class="image" />
</div>
</template>
<script lang="ts" setup>
import { computed, onMounted, ref, watch } from 'vue'
import { getFileIconPath } from '@/manage/utils/common'
import { getAuthHeader } from '@/manage/utils/digestAuth'
import { formatEndpoint } from '@/utils/common'
const base64Url = ref('')
const success = ref(false)
const isLoading = ref(true)
const hasError = ref(false)
const props = defineProps<{
item: {
key: string
isImage: boolean
fileName: string | null | undefined
}
url: string
config: any
isShowThumbnail: boolean
}>()
const imageSource = computed(() => {
return props.isShowThumbnail && props.item.isImage && success.value
? base64Url.value
: `./assets/icons/${getFileIconPath(props.item.fileName ?? '')}`
})
const iconPath = computed(() => `./assets/icons/${getFileIconPath(props.item.fileName ?? '')}`)
async function getWebdavHeader(key: string) {
let headers = {} as any
if (props.config.authType === 'digest') {
const authHeader = await getAuthHeader(
'GET',
formatEndpoint(props.config.endpoint, props.config.sslEnabled || false),
`/${key.replace(/^\//, '')}`,
props.config.username,
props.config.password
)
headers = {
Authorization: authHeader
}
} else {
headers = {
Authorization: 'Basic ' + Buffer.from(`${props.config.username}:${props.config.password}`).toString('base64')
}
}
return headers
}
const fetchImage = async () => {
try {
isLoading.value = true
hasError.value = false
const headers = await getWebdavHeader(props.item.key)
const res = await fetch(props.url, { method: 'GET', headers })
if (res.status >= 200 && res.status < 300) {
const blob = await res.blob()
success.value = true
base64Url.value = URL.createObjectURL(blob)
isLoading.value = false
} else {
throw new Error('Network response was not ok.')
}
} catch (err) {
success.value = false
hasError.value = true
isLoading.value = false
console.log(err)
}
}
const handleImageLoad = () => {
isLoading.value = false
hasError.value = false
}
const handleImageError = () => {
isLoading.value = false
hasError.value = true
}
watch(() => [props.url, props.item], fetchImage, { deep: true })
onMounted(fetchImage)
</script>
<style scoped>
.image-container {
height: 100px;
width: 100%;
margin: 0 auto;
display: flex;
align-items: center;
justify-content: center;
position: relative;
}
.image {
max-height: 100%;
max-width: 100%;
object-fit: contain;
display: block;
}
.loading-placeholder {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
}
.loading-spinner {
width: 24px;
height: 24px;
border: 2px solid #e4e7ed;
border-top: 2px solid #409eff;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
</style>

View File

@@ -1,22 +1,12 @@
<template>
<Teleport to="body">
<div
v-if="showInputBoxVisible"
class="inputbox-overlay"
@click="handleInputBoxCancel"
>
<div
class="inputbox-container"
@click.stop
>
<div v-if="showInputBoxVisible" class="inputbox-overlay" @click="handleInputBoxCancel">
<div class="inputbox-container" @click.stop>
<div class="inputbox-header">
<h3 class="inputbox-title">
{{ inputBoxOptions.title || t('pages.inputBox.title') }}
</h3>
<button
class="inputbox-close"
@click="handleInputBoxCancel"
>
<button class="inputbox-close" @click="handleInputBoxCancel">
<X :size="20" />
</button>
</div>
@@ -28,19 +18,13 @@
type="text"
@keyup.enter="handleInputBoxConfirm"
@keyup.escape="handleInputBoxCancel"
>
/>
</div>
<div class="inputbox-actions">
<button
class="inputbox-btn cancel-btn"
@click="handleInputBoxCancel"
>
<button class="inputbox-btn cancel-btn" @click="handleInputBoxCancel">
{{ t('common.cancel') }}
</button>
<button
class="inputbox-btn confirm-btn primary"
@click="handleInputBoxConfirm"
>
<button class="inputbox-btn confirm-btn primary" @click="handleInputBoxConfirm">
{{ t('common.confirm') }}
</button>
</div>
@@ -66,27 +50,27 @@ const inputBoxOptions = reactive({
placeholder: ''
})
let removeInputBoxListenerCallback: (() => void) = () => {}
let removeInputBoxListenerCallback: () => void = () => {}
function handleIpcInputBoxEvent (options: IShowInputBoxOption) {
function handleIpcInputBoxEvent(options: IShowInputBoxOption) {
initInputBoxValue(options)
}
function initInputBoxValue (options: IShowInputBoxOption) {
function initInputBoxValue(options: IShowInputBoxOption) {
inputBoxValue.value = options.value || ''
inputBoxOptions.title = options.title || ''
inputBoxOptions.placeholder = options.placeholder || ''
showInputBoxVisible.value = true
}
function handleInputBoxCancel () {
function handleInputBoxCancel() {
// TODO: RPCServer
showInputBoxVisible.value = false
window.electron.sendToMain(SHOW_INPUT_BOX, '')
$bus.emit(SHOW_INPUT_BOX_RESPONSE, '')
}
function handleInputBoxConfirm () {
function handleInputBoxConfirm() {
showInputBoxVisible.value = false
window.electron.sendToMain(SHOW_INPUT_BOX, inputBoxValue.value)
$bus.emit(SHOW_INPUT_BOX_RESPONSE, inputBoxValue.value)
@@ -126,7 +110,9 @@ export default {
.inputbox-container {
background: white;
border-radius: 0.75rem;
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
box-shadow:
0 20px 25px -5px rgba(0, 0, 0, 0.1),
0 10px 10px -5px rgba(0, 0, 0, 0.04);
max-width: 32rem;
width: 90%;
max-height: 80vh;

File diff suppressed because it is too large Load Diff

View File

@@ -1,61 +1,57 @@
<template>
<div class="toolbox-handler">
<button
class="handler-button"
@click="() => props.handler(value)"
>
{{ props.handlerText }}
</button>
</div>
</template>
<script lang="ts" setup>
interface IProps {
status: string
value: any
handlerText: string
handler: (value: any) => void | Promise<void>
}
const props = defineProps<IProps>()
</script>
<script lang="ts">
export default {
name: 'ToolboxHandler'
}
</script>
<style lang="stylus">
.toolbox-handler {
margin-top: 0.75rem;
}
.handler-button {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 1rem;
background: var(--color-accent);
color: white;
border: none;
border-radius: var(--radius-md);
font-size: 0.75rem;
font-weight: 500;
cursor: pointer;
transition: var(--transition-fast);
font-family: inherit;
text-decoration: none;
}
.handler-button:hover {
background: var(--color-accent-hover);
transform: translateY(-1px);
box-shadow: var(--shadow-sm);
}
.handler-button:active {
transform: translateY(0);
}
</style>
<template>
<div class="toolbox-handler">
<button class="handler-button" @click="() => props.handler(value)">
{{ props.handlerText }}
</button>
</div>
</template>
<script lang="ts" setup>
interface IProps {
status: string
value: any
handlerText: string
handler: (value: any) => void | Promise<void>
}
const props = defineProps<IProps>()
</script>
<script lang="ts">
export default {
name: 'ToolboxHandler'
}
</script>
<style lang="stylus">
.toolbox-handler {
margin-top: 0.75rem;
}
.handler-button {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 1rem;
background: var(--color-accent);
color: white;
border: none;
border-radius: var(--radius-md);
font-size: 0.75rem;
font-weight: 500;
cursor: pointer;
transition: var(--transition-fast);
font-family: inherit;
text-decoration: none;
}
.handler-button:hover {
background: var(--color-accent-hover);
transform: translateY(-1px);
box-shadow: var(--shadow-sm);
}
.handler-button:active {
transform: translateY(0);
}
</style>

View File

@@ -1,9 +1,5 @@
<template>
<component
:is="icon"
class="toolbox-status-icon"
:style="{ color }"
/>
<component :is="icon" class="toolbox-status-icon" :style="{ color }" />
</template>
<script lang="ts" setup>

File diff suppressed because it is too large Load Diff

View File

@@ -1,14 +1,6 @@
<template>
<div
ref="containerRef"
class="virtual-scroller"
:style="{ height: `${containerHeight}px` }"
@scroll="handleScroll"
>
<div
class="virtual-scroller-content"
:style="contentStyles"
>
<div ref="containerRef" class="virtual-scroller" :style="{ height: `${containerHeight}px` }" @scroll="handleScroll">
<div class="virtual-scroller-content" :style="contentStyles">
<div
class="virtual-scroller-viewport"
:class="{ 'is-grid': isGridMode, 'is-list': !isGridMode }"
@@ -16,14 +8,15 @@
>
<div
v-for="realIndex in visibleIndexes"
:key="itemsRef[realIndex] && itemsRef[realIndex][props.keyField || 'id'] ? itemsRef[realIndex][props.keyField || 'id'] : realIndex"
:key="
itemsRef[realIndex] && itemsRef[realIndex][props.keyField || 'id']
? itemsRef[realIndex][props.keyField || 'id']
: realIndex
"
class="virtual-scroller-item"
:style="itemStyle"
>
<slot
:item="itemsRef[realIndex]"
:index="realIndex"
/>
<slot :item="itemsRef[realIndex]" :index="realIndex" />
</div>
</div>
</div>
@@ -36,29 +29,35 @@ import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
import { useVirtualGrid } from '@/hooks/useVirtualGrid'
type Item = any
interface Breakpoint { min: number; cols: number }
interface Breakpoint {
min: number
cols: number
}
const props = withDefaults(defineProps<{
items: Item[]
itemHeight: number
height?: number
gridItems?: number
gridBreakpoints?: Breakpoint[]
bufferFactor?: number
pageMode?: boolean
keyField?: string
itemPadding?: number
viewMode?: 'list' | 'grid'
}>(), {
height: 400,
gridItems: 1,
gridBreakpoints: () => [],
bufferFactor: 0.5,
pageMode: false,
keyField: 'id',
itemPadding: 0,
viewMode: 'grid'
})
const props = withDefaults(
defineProps<{
items: Item[]
itemHeight: number
height?: number
gridItems?: number
gridBreakpoints?: Breakpoint[]
bufferFactor?: number
pageMode?: boolean
keyField?: string
itemPadding?: number
viewMode?: 'list' | 'grid'
}>(),
{
height: 400,
gridItems: 1,
gridBreakpoints: () => [],
bufferFactor: 0.5,
pageMode: false,
keyField: 'id',
itemPadding: 0,
viewMode: 'grid'
}
)
const containerRef = ref<HTMLElement | null>(null)
const containerHeight = ref<number>(props.pageMode ? 0 : props.height)
@@ -66,15 +65,23 @@ const containerWidth = ref<number>(0)
const parentScrollListeners = ref<HTMLElement[]>([])
const itemsRef = ref<Item[]>(props.items)
watch(() => props.items, v => { itemsRef.value = v })
const localViewMode = ref< 'list' | 'grid'>(props.viewMode)
watch(() => props.viewMode, v => { localViewMode.value = v })
const sortedBreakpoints = computed<Breakpoint[]>(() =>
[...props.gridBreakpoints].sort((a, b) => a.min - b.min)
watch(
() => props.items,
v => {
itemsRef.value = v
}
)
const localViewMode = ref<'list' | 'grid'>(props.viewMode)
watch(
() => props.viewMode,
v => {
localViewMode.value = v
}
)
const sortedBreakpoints = computed<Breakpoint[]>(() => [...props.gridBreakpoints].sort((a, b) => a.min - b.min))
const isForcedList = computed(() => localViewMode.value === 'list')
const effectiveCols = computed<number>(() => {
@@ -92,19 +99,14 @@ const effectiveCols = computed<number>(() => {
const isGridMode = computed(() => effectiveCols.value > 1)
const {
gridCalculations,
visibleIndexes,
viewportOffset,
updateScrollTop,
scrollToItem, scrollToTop, scrollToBottom
} = useVirtualGrid({
items: itemsRef,
itemHeight: props.itemHeight,
containerHeight,
gridItems: effectiveCols,
bufferFactor: props.bufferFactor
})
const { gridCalculations, visibleIndexes, viewportOffset, updateScrollTop, scrollToItem, scrollToTop, scrollToBottom } =
useVirtualGrid({
items: itemsRef,
itemHeight: props.itemHeight,
containerHeight,
gridItems: effectiveCols,
bufferFactor: props.bufferFactor
})
const contentStyles = computed(() => ({
height: `${gridCalculations.value.totalHeight}px`
@@ -122,19 +124,15 @@ const viewportStyle = computed(() => {
return base
})
const itemStyle = computed(() =>
isGridMode.value
? {}
: { height: `${props.itemHeight}px` }
)
const itemStyle = computed(() => (isGridMode.value ? {} : { height: `${props.itemHeight}px` }))
function handleScroll () {
function handleScroll() {
const c = containerRef.value
if (!c) return
updateScrollTop(c.scrollTop)
}
function handlePageScroll () {
function handlePageScroll() {
if (!props.pageMode) return
const now = Date.now()
if (now - lastScrollTime.value < 16) return
@@ -159,7 +157,7 @@ function handlePageScroll () {
let ro: ResizeObserver | null = null
const lastScrollTime = ref(0)
function updateContainerMetrics () {
function updateContainerMetrics() {
const el = containerRef.value
if (!el) return
const rect = el.getBoundingClientRect()
@@ -206,16 +204,18 @@ onBeforeUnmount(() => {
}
})
function scrollTo (index: number) { scrollToItem(index) }
function scrollTo(index: number) {
scrollToItem(index)
}
function setViewMode (mode: 'list' | 'grid') {
function setViewMode(mode: 'list' | 'grid') {
localViewMode.value = mode
}
function toggleViewMode () {
function toggleViewMode() {
setViewMode(isGridMode.value ? 'list' : 'grid')
}
function refresh () {
function refresh() {
updateContainerMetrics()
if (containerRef.value) {
updateScrollTop(containerRef.value.scrollTop)
@@ -266,5 +266,4 @@ defineExpose({ scrollTo, scrollToTop, scrollToBottom, setViewMode, toggleViewMod
.virtual-scroller-viewport.is-grid .virtual-scroller-item {
width: 100%;
}
</style>

View File

@@ -1,342 +1,305 @@
<template>
<div
v-if="isOpen"
class="messagebox-overlay"
@click="onCancel"
>
<div
class="messagebox-container"
@click.stop
>
<div class="messagebox-header">
<h3 class="messagebox-title">
{{ title }}
</h3>
<button
v-if="showClose"
class="messagebox-close"
@click="onCancel"
>
×
</button>
</div>
<div class="messagebox-content">
<div
v-if="type"
class="messagebox-icon"
>
<component
:is="iconComponent"
:size="48"
/>
</div>
<div class="messagebox-message">
<p>{{ message }}</p>
</div>
</div>
<div
v-if="center"
class="messagebox-actions center"
>
<button
class="messagebox-btn cancel-btn"
@click="onCancel"
>
{{ cancelButtonText }}
</button>
<button
class="messagebox-btn confirm-btn"
:class="confirmButtonClass"
@click="onConfirm"
>
{{ confirmButtonText }}
</button>
</div>
<div
v-else
class="messagebox-actions"
>
<button
class="messagebox-btn confirm-btn"
:class="confirmButtonClass"
@click="onConfirm"
>
{{ confirmButtonText }}
</button>
<button
class="messagebox-btn cancel-btn"
@click="onCancel"
>
{{ cancelButtonText }}
</button>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { AlertTriangle, CheckCircle, Info, XCircle } from 'lucide-vue-next'
import { computed } from 'vue'
interface Props {
isOpen: boolean
title?: string
message: string
type?: 'info' | 'success' | 'warning' | 'error'
confirmButtonText?: string
cancelButtonText?: string
showClose?: boolean
center?: boolean
}
interface Emits {
(e: 'confirm'): void
(e: 'cancel'): void
}
const props = withDefaults(defineProps<Props>(), {
title: 'Confirm',
confirmButtonText: 'Confirm',
cancelButtonText: 'Cancel',
showClose: true,
center: false,
type: undefined
})
const emit = defineEmits<Emits>()
const iconComponent = computed(() => {
switch (props.type) {
case 'warning':
return AlertTriangle
case 'info':
return Info
case 'success':
return CheckCircle
case 'error':
return XCircle
default:
return Info
}
})
const confirmButtonClass = computed(() => {
switch (props.type) {
case 'warning':
case 'error':
return 'danger'
case 'success':
return 'success'
default:
return 'primary'
}
})
const onConfirm = () => {
emit('confirm')
}
const onCancel = () => {
emit('cancel')
}
</script>
<script lang="ts">
export default {
name: 'ConfirmMessageBox'
}
</script>
<style scoped>
.messagebox-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 2000;
}
.messagebox-container {
background: white;
border-radius: 0.75rem;
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
max-width: 32rem;
width: 90%;
max-height: 80vh;
overflow: hidden;
}
:root.dark .messagebox-container,
:root.auto.dark .messagebox-container {
background: rgb(31 41 55);
border: 1px solid rgb(55 65 81);
}
.messagebox-header {
padding: 1.5rem 1.5rem 0 1.5rem;
display: flex;
justify-content: space-between;
align-items: center;
}
.messagebox-title {
font-size: 1.25rem;
font-weight: 600;
color: rgb(17 24 39);
margin: 0;
}
:root.dark .messagebox-title,
:root.auto.dark .messagebox-title {
color: rgb(243 244 246);
}
.messagebox-close {
background: none;
border: none;
font-size: 1.5rem;
color: rgb(107 114 128);
cursor: pointer;
padding: 0;
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 0.25rem;
}
.messagebox-close:hover {
background: rgb(243 244 246);
color: rgb(17 24 39);
}
:root.dark .messagebox-close,
:root.auto.dark .messagebox-close {
color: rgb(156 163 175);
}
:root.dark .messagebox-close:hover,
:root.auto.dark .messagebox-close:hover {
background: rgb(55 65 81);
color: rgb(243 244 246);
}
.messagebox-content {
padding: 1rem 1.5rem;
display: flex;
gap: 1rem;
align-items: flex-start;
}
.messagebox-icon {
flex-shrink: 0;
color: rgb(107 114 128);
}
.messagebox-icon svg[data-lucide="alert-triangle"] {
color: rgb(245 158 11);
}
.messagebox-icon svg[data-lucide="info"] {
color: rgb(59 130 246);
}
.messagebox-icon svg[data-lucide="check-circle"] {
color: rgb(34 197 94);
}
.messagebox-icon svg[data-lucide="x-circle"] {
color: rgb(239 68 68);
}
.messagebox-message {
flex: 1;
}
.messagebox-message p {
color: rgb(107 114 128);
line-height: 1.6;
margin: 0;
}
:root.dark .messagebox-message p,
:root.auto.dark .messagebox-message p {
color: rgb(156 163 175);
}
.messagebox-actions {
display: flex;
gap: 0.75rem;
padding: 0 1.5rem 1.5rem 1.5rem;
justify-content: flex-end;
}
.messagebox-actions.center {
justify-content: center;
}
.messagebox-btn {
padding: 0.5rem 1rem;
border-radius: 0.375rem;
font-size: 0.875rem;
font-weight: 500;
border: none;
cursor: pointer;
min-width: 4rem;
}
.cancel-btn {
background: rgb(243 244 246);
color: rgb(75 85 99);
border: 1px solid rgb(209 213 219);
}
.cancel-btn:hover {
background: rgb(229 231 235);
}
:root.dark .cancel-btn,
:root.auto.dark .cancel-btn {
background: rgb(55 65 81);
color: rgb(209 213 219);
border-color: rgb(75 85 99);
}
:root.dark .cancel-btn:hover,
:root.auto.dark .cancel-btn:hover {
background: rgb(75 85 99);
}
.confirm-btn.primary {
background: rgb(59 130 246);
color: white;
}
.confirm-btn.primary:hover {
background: rgb(37 99 235);
}
.confirm-btn.danger {
background: rgb(239 68 68);
color: white;
}
.confirm-btn.danger:hover {
background: rgb(220 38 38);
}
.confirm-btn.success {
background: rgb(34 197 94);
color: white;
}
.confirm-btn.success:hover {
background: rgb(22 163 74);
}
</style>
<template>
<div v-if="isOpen" class="messagebox-overlay" @click="onCancel">
<div class="messagebox-container" @click.stop>
<div class="messagebox-header">
<h3 class="messagebox-title">
{{ title }}
</h3>
<button v-if="showClose" class="messagebox-close" @click="onCancel">×</button>
</div>
<div class="messagebox-content">
<div v-if="type" class="messagebox-icon">
<component :is="iconComponent" :size="48" />
</div>
<div class="messagebox-message">
<p>{{ message }}</p>
</div>
</div>
<div v-if="center" class="messagebox-actions center">
<button class="messagebox-btn cancel-btn" @click="onCancel">
{{ cancelButtonText }}
</button>
<button class="messagebox-btn confirm-btn" :class="confirmButtonClass" @click="onConfirm">
{{ confirmButtonText }}
</button>
</div>
<div v-else class="messagebox-actions">
<button class="messagebox-btn confirm-btn" :class="confirmButtonClass" @click="onConfirm">
{{ confirmButtonText }}
</button>
<button class="messagebox-btn cancel-btn" @click="onCancel">
{{ cancelButtonText }}
</button>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { AlertTriangle, CheckCircle, Info, XCircle } from 'lucide-vue-next'
import { computed } from 'vue'
interface Props {
isOpen: boolean
title?: string
message: string
type?: 'info' | 'success' | 'warning' | 'error'
confirmButtonText?: string
cancelButtonText?: string
showClose?: boolean
center?: boolean
}
interface Emits {
(e: 'confirm'): void
(e: 'cancel'): void
}
const props = withDefaults(defineProps<Props>(), {
title: 'Confirm',
confirmButtonText: 'Confirm',
cancelButtonText: 'Cancel',
showClose: true,
center: false,
type: undefined
})
const emit = defineEmits<Emits>()
const iconComponent = computed(() => {
switch (props.type) {
case 'warning':
return AlertTriangle
case 'info':
return Info
case 'success':
return CheckCircle
case 'error':
return XCircle
default:
return Info
}
})
const confirmButtonClass = computed(() => {
switch (props.type) {
case 'warning':
case 'error':
return 'danger'
case 'success':
return 'success'
default:
return 'primary'
}
})
const onConfirm = () => {
emit('confirm')
}
const onCancel = () => {
emit('cancel')
}
</script>
<script lang="ts">
export default {
name: 'ConfirmMessageBox'
}
</script>
<style scoped>
.messagebox-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 2000;
}
.messagebox-container {
background: white;
border-radius: 0.75rem;
box-shadow:
0 20px 25px -5px rgba(0, 0, 0, 0.1),
0 10px 10px -5px rgba(0, 0, 0, 0.04);
max-width: 32rem;
width: 90%;
max-height: 80vh;
overflow: hidden;
}
:root.dark .messagebox-container,
:root.auto.dark .messagebox-container {
background: rgb(31 41 55);
border: 1px solid rgb(55 65 81);
}
.messagebox-header {
padding: 1.5rem 1.5rem 0 1.5rem;
display: flex;
justify-content: space-between;
align-items: center;
}
.messagebox-title {
font-size: 1.25rem;
font-weight: 600;
color: rgb(17 24 39);
margin: 0;
}
:root.dark .messagebox-title,
:root.auto.dark .messagebox-title {
color: rgb(243 244 246);
}
.messagebox-close {
background: none;
border: none;
font-size: 1.5rem;
color: rgb(107 114 128);
cursor: pointer;
padding: 0;
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 0.25rem;
}
.messagebox-close:hover {
background: rgb(243 244 246);
color: rgb(17 24 39);
}
:root.dark .messagebox-close,
:root.auto.dark .messagebox-close {
color: rgb(156 163 175);
}
:root.dark .messagebox-close:hover,
:root.auto.dark .messagebox-close:hover {
background: rgb(55 65 81);
color: rgb(243 244 246);
}
.messagebox-content {
padding: 1rem 1.5rem;
display: flex;
gap: 1rem;
align-items: flex-start;
}
.messagebox-icon {
flex-shrink: 0;
color: rgb(107 114 128);
}
.messagebox-icon svg[data-lucide='alert-triangle'] {
color: rgb(245 158 11);
}
.messagebox-icon svg[data-lucide='info'] {
color: rgb(59 130 246);
}
.messagebox-icon svg[data-lucide='check-circle'] {
color: rgb(34 197 94);
}
.messagebox-icon svg[data-lucide='x-circle'] {
color: rgb(239 68 68);
}
.messagebox-message {
flex: 1;
}
.messagebox-message p {
color: rgb(107 114 128);
line-height: 1.6;
margin: 0;
}
:root.dark .messagebox-message p,
:root.auto.dark .messagebox-message p {
color: rgb(156 163 175);
}
.messagebox-actions {
display: flex;
gap: 0.75rem;
padding: 0 1.5rem 1.5rem 1.5rem;
justify-content: flex-end;
}
.messagebox-actions.center {
justify-content: center;
}
.messagebox-btn {
padding: 0.5rem 1rem;
border-radius: 0.375rem;
font-size: 0.875rem;
font-weight: 500;
border: none;
cursor: pointer;
min-width: 4rem;
}
.cancel-btn {
background: rgb(243 244 246);
color: rgb(75 85 99);
border: 1px solid rgb(209 213 219);
}
.cancel-btn:hover {
background: rgb(229 231 235);
}
:root.dark .cancel-btn,
:root.auto.dark .cancel-btn {
background: rgb(55 65 81);
color: rgb(209 213 219);
border-color: rgb(75 85 99);
}
:root.dark .cancel-btn:hover,
:root.auto.dark .cancel-btn:hover {
background: rgb(75 85 99);
}
.confirm-btn.primary {
background: rgb(59 130 246);
color: white;
}
.confirm-btn.primary:hover {
background: rgb(37 99 235);
}
.confirm-btn.danger {
background: rgb(239 68 68);
color: white;
}
.confirm-btn.danger:hover {
background: rgb(220 38 38);
}
.confirm-btn.success {
background: rgb(34 197 94);
color: white;
}
.confirm-btn.success:hover {
background: rgb(22 163 74);
}
</style>

View File

@@ -1,270 +1,257 @@
<template>
<Teleport to="body">
<div class="message-container">
<TransitionGroup
name="message"
tag="div"
>
<div
v-for="message in messages"
:key="message.id"
class="message-toast"
:class="getMessageClass(message.type)"
>
<div class="message-icon">
<component
:is="getIconComponent(message.type)"
:size="20"
/>
</div>
<div class="message-content">
{{ message.message }}
</div>
<button
v-if="message.showClose"
class="message-close"
@click="removeMessage(message.id)"
>
<X :size="16" />
</button>
</div>
</TransitionGroup>
</div>
</Teleport>
</template>
<script setup lang="ts">
import { AlertTriangle, CheckCircle, Info, X, XCircle } from 'lucide-vue-next'
import { reactive } from 'vue'
export interface MessageOptions {
message: string
type?: 'success' | 'warning' | 'info' | 'error'
duration?: number
showClose?: boolean
}
interface MessageItem extends MessageOptions {
id: string
timer?: ReturnType<typeof setTimeout>
}
const messages = reactive<MessageItem[]>([])
const getIconComponent = (type: MessageOptions['type']) => {
switch (type) {
case 'success':
return CheckCircle
case 'warning':
return AlertTriangle
case 'error':
return XCircle
default:
return Info
}
}
const getMessageClass = (type: MessageOptions['type']) => {
return `message-${type || 'info'}`
}
const removeMessage = (id: string) => {
const index = messages.findIndex(msg => msg.id === id)
if (index > -1) {
const message = messages[index]
if (message.timer) {
clearTimeout(message.timer)
}
messages.splice(index, 1)
}
}
const addMessage = (options: MessageOptions) => {
const id = `message-${Date.now()}-${Math.random()}`
const duration = options.duration ?? 3000
const showClose = options.showClose ?? true
const message: MessageItem = {
id,
...options,
showClose
}
if (duration > 0) {
message.timer = setTimeout(() => {
removeMessage(id)
}, duration)
}
messages.push(message)
return id
}
// Expose methods for external use
const success = (message: string, options?: Partial<MessageOptions>) => {
return addMessage({ message, type: 'success', ...options })
}
const error = (message: string, options?: Partial<MessageOptions>) => {
return addMessage({ message, type: 'error', ...options })
}
const warning = (message: string, options?: Partial<MessageOptions>) => {
return addMessage({ message, type: 'warning', ...options })
}
const info = (message: string, options?: Partial<MessageOptions>) => {
return addMessage({ message, type: 'info', ...options })
}
defineExpose({
success,
error,
warning,
info,
addMessage,
removeMessage
})
</script>
<script lang="ts">
export default {
name: 'MessageToast'
}
</script>
<style scoped>
.message-container {
position: fixed;
top: 34px;
right: 20px;
z-index: 3000;
pointer-events: none;
}
.message-toast {
display: flex;
align-items: flex-start;
gap: 0.75rem;
padding: 0.75rem 1rem;
margin-bottom: 0.5rem;
border-radius: 0.5rem;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
max-width: 24rem;
min-width: 20rem;
pointer-events: all;
background: white;
border: 1px solid rgb(229 231 235);
word-wrap: break-word;
}
:root.dark .message-toast,
:root.auto.dark .message-toast {
background: rgb(31 41 55);
border-color: rgb(55 65 81);
}
.message-info {
border-left: 4px solid rgb(59 130 246);
}
.message-info .message-icon {
color: rgb(59 130 246);
}
.message-success {
border-left: 4px solid rgb(34 197 94);
}
.message-success .message-icon {
color: rgb(34 197 94);
}
.message-warning {
border-left: 4px solid rgb(245 158 11);
}
.message-warning .message-icon {
color: rgb(245 158 11);
}
.message-error {
border-left: 4px solid rgb(239 68 68);
}
.message-error .message-icon {
color: rgb(239 68 68);
}
.message-icon {
flex-shrink: 0;
margin-top: 0.125rem;
}
.message-content {
flex: 1;
color: rgb(75 85 99);
font-size: 0.875rem;
line-height: 1.25rem;
word-wrap: break-word;
word-break: break-word;
overflow-wrap: break-word;
hyphens: auto;
min-width: 0;
}
:root.dark .message-content,
:root.auto.dark .message-content {
color: rgb(209 213 219);
}
.message-close {
background: none;
border: none;
color: rgb(107 114 128);
cursor: pointer;
padding: 0.25rem;
border-radius: 0.25rem;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
margin-top: 0.125rem;
}
.message-close:hover {
background: rgb(243 244 246);
color: rgb(75 85 99);
}
:root.dark .message-close,
:root.auto.dark .message-close {
color: rgb(156 163 175);
}
:root.dark .message-close:hover,
:root.auto.dark .message-close:hover {
background: rgb(55 65 81);
color: rgb(209 213 219);
}
/* Transition animations */
.message-enter-active,
.message-leave-active {
transition: all 0.3s ease;
}
.message-enter-from {
opacity: 0;
transform: translateX(100%);
}
.message-leave-to {
opacity: 0;
transform: translateX(100%);
}
.message-move {
transition: transform 0.3s ease;
}
</style>
<template>
<Teleport to="body">
<div class="message-container">
<TransitionGroup name="message" tag="div">
<div v-for="message in messages" :key="message.id" class="message-toast" :class="getMessageClass(message.type)">
<div class="message-icon">
<component :is="getIconComponent(message.type)" :size="20" />
</div>
<div class="message-content">
{{ message.message }}
</div>
<button v-if="message.showClose" class="message-close" @click="removeMessage(message.id)">
<X :size="16" />
</button>
</div>
</TransitionGroup>
</div>
</Teleport>
</template>
<script setup lang="ts">
import { AlertTriangle, CheckCircle, Info, X, XCircle } from 'lucide-vue-next'
import { reactive } from 'vue'
export interface MessageOptions {
message: string
type?: 'success' | 'warning' | 'info' | 'error'
duration?: number
showClose?: boolean
}
interface MessageItem extends MessageOptions {
id: string
timer?: ReturnType<typeof setTimeout>
}
const messages = reactive<MessageItem[]>([])
const getIconComponent = (type: MessageOptions['type']) => {
switch (type) {
case 'success':
return CheckCircle
case 'warning':
return AlertTriangle
case 'error':
return XCircle
default:
return Info
}
}
const getMessageClass = (type: MessageOptions['type']) => {
return `message-${type || 'info'}`
}
const removeMessage = (id: string) => {
const index = messages.findIndex(msg => msg.id === id)
if (index > -1) {
const message = messages[index]
if (message.timer) {
clearTimeout(message.timer)
}
messages.splice(index, 1)
}
}
const addMessage = (options: MessageOptions) => {
const id = `message-${Date.now()}-${Math.random()}`
const duration = options.duration ?? 3000
const showClose = options.showClose ?? true
const message: MessageItem = {
id,
...options,
showClose
}
if (duration > 0) {
message.timer = setTimeout(() => {
removeMessage(id)
}, duration)
}
messages.push(message)
return id
}
// Expose methods for external use
const success = (message: string, options?: Partial<MessageOptions>) => {
return addMessage({ message, type: 'success', ...options })
}
const error = (message: string, options?: Partial<MessageOptions>) => {
return addMessage({ message, type: 'error', ...options })
}
const warning = (message: string, options?: Partial<MessageOptions>) => {
return addMessage({ message, type: 'warning', ...options })
}
const info = (message: string, options?: Partial<MessageOptions>) => {
return addMessage({ message, type: 'info', ...options })
}
defineExpose({
success,
error,
warning,
info,
addMessage,
removeMessage
})
</script>
<script lang="ts">
export default {
name: 'MessageToast'
}
</script>
<style scoped>
.message-container {
position: fixed;
top: 34px;
right: 20px;
z-index: 3000;
pointer-events: none;
}
.message-toast {
display: flex;
align-items: flex-start;
gap: 0.75rem;
padding: 0.75rem 1rem;
margin-bottom: 0.5rem;
border-radius: 0.5rem;
box-shadow:
0 4px 6px -1px rgba(0, 0, 0, 0.1),
0 2px 4px -1px rgba(0, 0, 0, 0.06);
max-width: 24rem;
min-width: 20rem;
pointer-events: all;
background: white;
border: 1px solid rgb(229 231 235);
word-wrap: break-word;
}
:root.dark .message-toast,
:root.auto.dark .message-toast {
background: rgb(31 41 55);
border-color: rgb(55 65 81);
}
.message-info {
border-left: 4px solid rgb(59 130 246);
}
.message-info .message-icon {
color: rgb(59 130 246);
}
.message-success {
border-left: 4px solid rgb(34 197 94);
}
.message-success .message-icon {
color: rgb(34 197 94);
}
.message-warning {
border-left: 4px solid rgb(245 158 11);
}
.message-warning .message-icon {
color: rgb(245 158 11);
}
.message-error {
border-left: 4px solid rgb(239 68 68);
}
.message-error .message-icon {
color: rgb(239 68 68);
}
.message-icon {
flex-shrink: 0;
margin-top: 0.125rem;
}
.message-content {
flex: 1;
color: rgb(75 85 99);
font-size: 0.875rem;
line-height: 1.25rem;
word-wrap: break-word;
word-break: break-word;
overflow-wrap: break-word;
hyphens: auto;
min-width: 0;
}
:root.dark .message-content,
:root.auto.dark .message-content {
color: rgb(209 213 219);
}
.message-close {
background: none;
border: none;
color: rgb(107 114 128);
cursor: pointer;
padding: 0.25rem;
border-radius: 0.25rem;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
margin-top: 0.125rem;
}
.message-close:hover {
background: rgb(243 244 246);
color: rgb(75 85 99);
}
:root.dark .message-close,
:root.auto.dark .message-close {
color: rgb(156 163 175);
}
:root.dark .message-close:hover,
:root.auto.dark .message-close:hover {
background: rgb(55 65 81);
color: rgb(209 213 219);
}
/* Transition animations */
.message-enter-active,
.message-leave-active {
transition: all 0.3s ease;
}
.message-enter-from {
opacity: 0;
transform: translateX(100%);
}
.message-leave-to {
opacity: 0;
transform: translateX(100%);
}
.message-move {
transition: transform 0.3s ease;
}
</style>

View File

@@ -1,117 +1,106 @@
<script setup lang="ts">
import { Monitor, Moon, Sun } from 'lucide-vue-next'
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import { useAppStore } from '@/hooks/useAppStore'
interface Props {
collapsed?: boolean
}
defineProps<Props>()
const { t } = useI18n()
const appStore = useAppStore()
const currentTheme = computed(() => appStore.settings.app.theme || 'light')
const themeOptions = computed(() => [
{
value: 'light',
label: t('settings.theme.light'),
icon: Sun,
description: t('settings.theme.lightDesc')
},
{
value: 'dark',
label: t('settings.theme.dark'),
icon: Moon,
description: t('settings.theme.darkDesc')
},
{
value: 'auto',
label: t('settings.theme.auto'),
icon: Monitor,
description: t('settings.theme.autoDesc')
}
])
const currentThemeOption = computed(
() => themeOptions.value.find(option => option.value === currentTheme.value) || themeOptions.value[0]
)
const toggleTheme = () => {
appStore.toggleTheme()
}
</script>
<template>
<div class="theme-switcher">
<button
class="theme-toggle-btn"
:class="{ collapsed }"
:title="t('settings.theme.toggle')"
@click="toggleTheme"
>
<component
:is="currentThemeOption.icon"
:size="18"
/>
<span
v-if="!collapsed"
class="theme-label"
>{{ currentThemeOption.label }}</span>
</button>
</div>
</template>
<style scoped>
.theme-switcher {
position: relative;
display: flex;
align-items: center;
}
.theme-toggle-btn {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 0.75rem;
border: 1px solid var(--color-border);
background: rgba(255, 255, 255, 0.1);
color: var(--color-text-secondary);
border-radius: var(--radius-md);
cursor: pointer;
font-size: 0.875rem;
transition: all 0.2s ease;
}
.theme-toggle-btn.collapsed {
padding: 0.5rem;
gap: 0;
justify-content: center;
}
.theme-toggle-btn:hover {
background: var(--color-surface-elevated);
color: var(--color-text-primary);
}
.theme-label {
font-weight: 500;
}
/* Mobile responsive */
@media (max-width: 768px) {
.theme-label {
display: none;
}
.theme-toggle-btn {
padding: 0.5rem;
gap: 0;
justify-content: center;
}
}
</style>
<script setup lang="ts">
import { Monitor, Moon, Sun } from 'lucide-vue-next'
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import { useAppStore } from '@/hooks/useAppStore'
interface Props {
collapsed?: boolean
}
defineProps<Props>()
const { t } = useI18n()
const appStore = useAppStore()
const currentTheme = computed(() => appStore.settings.app.theme || 'light')
const themeOptions = computed(() => [
{
value: 'light',
label: t('settings.theme.light'),
icon: Sun,
description: t('settings.theme.lightDesc')
},
{
value: 'dark',
label: t('settings.theme.dark'),
icon: Moon,
description: t('settings.theme.darkDesc')
},
{
value: 'auto',
label: t('settings.theme.auto'),
icon: Monitor,
description: t('settings.theme.autoDesc')
}
])
const currentThemeOption = computed(
() => themeOptions.value.find(option => option.value === currentTheme.value) || themeOptions.value[0]
)
const toggleTheme = () => {
appStore.toggleTheme()
}
</script>
<template>
<div class="theme-switcher">
<button class="theme-toggle-btn" :class="{ collapsed }" :title="t('settings.theme.toggle')" @click="toggleTheme">
<component :is="currentThemeOption.icon" :size="18" />
<span v-if="!collapsed" class="theme-label">{{ currentThemeOption.label }}</span>
</button>
</div>
</template>
<style scoped>
.theme-switcher {
position: relative;
display: flex;
align-items: center;
}
.theme-toggle-btn {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 0.75rem;
border: 1px solid var(--color-border);
background: rgba(255, 255, 255, 0.1);
color: var(--color-text-secondary);
border-radius: var(--radius-md);
cursor: pointer;
font-size: 0.875rem;
transition: all 0.2s ease;
}
.theme-toggle-btn.collapsed {
padding: 0.5rem;
gap: 0;
justify-content: center;
}
.theme-toggle-btn:hover {
background: var(--color-surface-elevated);
color: var(--color-text-primary);
}
.theme-label {
font-weight: 500;
}
/* Mobile responsive */
@media (max-width: 768px) {
.theme-label {
display: none;
}
.theme-toggle-btn {
padding: 0.5rem;
gap: 0;
justify-content: center;
}
}
</style>

View File

@@ -1,246 +1,212 @@
<template>
<div
class="title-bar"
data-drag-region
>
<div class="title-bar-content">
<div
v-if="osGlobal !== 'darwin'"
class="title-left"
>
<div class="app-icon">
<img
:src="defaultLogo"
alt="App Icon"
width="20"
height="20"
>
</div>
</div>
<div class="title-center">
<!-- Progress bar in title bar -->
<div
v-if="isShowprogress"
class="progress-container"
>
<div class="progress-bar">
<div
class="progress-fill"
:style="{ width: `${progress}%` }"
/>
</div>
<span class="progress-text">{{ progress }}%</span>
</div>
</div>
<div
class="title-right"
>
<div class="window-controls">
<button
class="control-button pin-button"
:class="{ active: isAlwaysOnTop }"
:title="$t('titleBar.alwaysOnTop')"
@click="setAlwaysOnTop"
>
<PinIcon
:color="isAlwaysOnTop ? '#CE6769' : '#6B7280'"
:size="14"
/>
</button>
<button
class="control-button minimize-button"
:title="$t('titleBar.minimize')"
@click="minimizeWindow"
>
<MinusIcon :size="14" />
</button>
<button
class="control-button mini-button"
:title="$t('titleBar.miniWindow')"
@click="openMiniWindow"
>
<ShrinkIcon :size="14" />
</button>
<button
class="control-button close-button"
:title="$t('titleBar.close')"
@click="closeWindow"
>
<XIcon :size="14" />
</button>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { MinusIcon, PinIcon, ShrinkIcon, XIcon } from 'lucide-vue-next'
import { computed, onBeforeMount, onBeforeUnmount, ref } from 'vue'
import { IRPCActionType } from '@/utils/enum'
import { osGlobal } from '@/utils/global'
const isShowprogress = ref(false)
const progress = ref(0)
const isAlwaysOnTop = ref(false)
const defaultLogo = computed(() => `${import.meta.env.BASE_URL}roundLogo.png`)
function setAlwaysOnTop () {
isAlwaysOnTop.value = !isAlwaysOnTop.value
window.electron.sendRPC(IRPCActionType.MAIN_WINDOW_ON_TOP)
}
function minimizeWindow () {
window.electron.sendRPC(IRPCActionType.MINIMIZE_WINDOW)
}
function openMiniWindow () {
window.electron.sendRPC(IRPCActionType.OPEN_MINI_WINDOW)
}
function closeWindow () {
window.electron.sendRPC(IRPCActionType.CLOSE_WINDOW)
}
const uploadProcessHandler = (data: { progress: number }) => {
isShowprogress.value = data.progress !== 100 && data.progress !== 0
progress.value = data.progress
}
onBeforeMount(() => {
window.electron.ipcRendererOn('updateProgress', uploadProcessHandler)
})
onBeforeUnmount(() => {
window.electron.ipcRendererRemoveAllListeners('updateProgress')
})
</script>
<style scoped>
.title-bar {
position: fixed;
top: 0;
left: 0;
right: 0;
height: 32px;
background: var(--color-background-secondary);
border-bottom: 1px solid var(--color-border);
z-index: 1000;
-webkit-app-region: drag;
}
.title-bar-content {
display: flex;
align-items: center;
justify-content: space-between;
height: 100%;
padding: 0 16px;
}
.title-left {
display: flex;
align-items: center;
gap: 8px;
-webkit-app-region: no-drag;
}
.app-icon {
display: flex;
align-items: center;
color: var(--color-accent);
}
.app-title {
font-size: 14px;
font-weight: 600;
color: var(--color-text-primary);
}
.app-version {
font-size: 12px;
color: var(--color-text-secondary);
background: var(--color-border);
padding: 2px 6px;
border-radius: 4px;
}
.title-center {
flex: 1;
display: flex;
justify-content: center;
align-items: center;
-webkit-app-region: no-drag;
}
.progress-container {
display: flex;
align-items: center;
gap: 8px;
max-width: 200px;
}
.progress-bar {
flex: 1;
height: 4px;
background: var(--color-border);
border-radius: 2px;
overflow: hidden;
}
.progress-fill {
height: 100%;
background: var(--color-success);
border-radius: 2px;
transition: width 0.3s ease;
}
.progress-text {
font-size: 11px;
color: var(--color-text-secondary);
min-width: 35px;
}
.title-right {
display: flex;
align-items: center;
-webkit-app-region: no-drag;
}
.window-controls {
display: flex;
align-items: center;
gap: 8px;
}
.control-button {
display: flex;
align-items: center;
justify-content: center;
width: 28px;
height: 20px;
border: none;
background: transparent;
border-radius: 4px;
color: var(--color-text-secondary);
cursor: pointer;
transition: var(--transition);
}
.control-button:hover {
background: var(--color-surface-elevated);
color: var(--color-text-primary);
}
.pin-button.active {
color: var(--color-accent);
background: var(--color-accent)20;
}
.close-button:hover {
background: var(--color-danger);
color: white;
}
</style>
<template>
<div class="title-bar" data-drag-region>
<div class="title-bar-content">
<div v-if="osGlobal !== 'darwin'" class="title-left">
<div class="app-icon">
<img :src="defaultLogo" alt="App Icon" width="20" height="20" />
</div>
</div>
<div class="title-center">
<!-- Progress bar in title bar -->
<div v-if="isShowprogress" class="progress-container">
<div class="progress-bar">
<div class="progress-fill" :style="{ width: `${progress}%` }" />
</div>
<span class="progress-text">{{ progress }}%</span>
</div>
</div>
<div class="title-right">
<div class="window-controls">
<button
class="control-button pin-button"
:class="{ active: isAlwaysOnTop }"
:title="$t('titleBar.alwaysOnTop')"
@click="setAlwaysOnTop"
>
<PinIcon :color="isAlwaysOnTop ? '#CE6769' : '#6B7280'" :size="14" />
</button>
<button class="control-button minimize-button" :title="$t('titleBar.minimize')" @click="minimizeWindow">
<MinusIcon :size="14" />
</button>
<button class="control-button mini-button" :title="$t('titleBar.miniWindow')" @click="openMiniWindow">
<ShrinkIcon :size="14" />
</button>
<button class="control-button close-button" :title="$t('titleBar.close')" @click="closeWindow">
<XIcon :size="14" />
</button>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { MinusIcon, PinIcon, ShrinkIcon, XIcon } from 'lucide-vue-next'
import { computed, onBeforeMount, onBeforeUnmount, ref } from 'vue'
import { IRPCActionType } from '@/utils/enum'
import { osGlobal } from '@/utils/global'
const isShowprogress = ref(false)
const progress = ref(0)
const isAlwaysOnTop = ref(false)
const defaultLogo = computed(() => `${import.meta.env.BASE_URL}roundLogo.png`)
function setAlwaysOnTop() {
isAlwaysOnTop.value = !isAlwaysOnTop.value
window.electron.sendRPC(IRPCActionType.MAIN_WINDOW_ON_TOP)
}
function minimizeWindow() {
window.electron.sendRPC(IRPCActionType.MINIMIZE_WINDOW)
}
function openMiniWindow() {
window.electron.sendRPC(IRPCActionType.OPEN_MINI_WINDOW)
}
function closeWindow() {
window.electron.sendRPC(IRPCActionType.CLOSE_WINDOW)
}
const uploadProcessHandler = (data: { progress: number }) => {
isShowprogress.value = data.progress !== 100 && data.progress !== 0
progress.value = data.progress
}
onBeforeMount(() => {
window.electron.ipcRendererOn('updateProgress', uploadProcessHandler)
})
onBeforeUnmount(() => {
window.electron.ipcRendererRemoveAllListeners('updateProgress')
})
</script>
<style scoped>
.title-bar {
position: fixed;
top: 0;
left: 0;
right: 0;
height: 32px;
background: var(--color-background-secondary);
border-bottom: 1px solid var(--color-border);
z-index: 1000;
-webkit-app-region: drag;
}
.title-bar-content {
display: flex;
align-items: center;
justify-content: space-between;
height: 100%;
padding: 0 16px;
}
.title-left {
display: flex;
align-items: center;
gap: 8px;
-webkit-app-region: no-drag;
}
.app-icon {
display: flex;
align-items: center;
color: var(--color-accent);
}
.app-title {
font-size: 14px;
font-weight: 600;
color: var(--color-text-primary);
}
.app-version {
font-size: 12px;
color: var(--color-text-secondary);
background: var(--color-border);
padding: 2px 6px;
border-radius: 4px;
}
.title-center {
flex: 1;
display: flex;
justify-content: center;
align-items: center;
-webkit-app-region: no-drag;
}
.progress-container {
display: flex;
align-items: center;
gap: 8px;
max-width: 200px;
}
.progress-bar {
flex: 1;
height: 4px;
background: var(--color-border);
border-radius: 2px;
overflow: hidden;
}
.progress-fill {
height: 100%;
background: var(--color-success);
border-radius: 2px;
transition: width 0.3s ease;
}
.progress-text {
font-size: 11px;
color: var(--color-text-secondary);
min-width: 35px;
}
.title-right {
display: flex;
align-items: center;
-webkit-app-region: no-drag;
}
.window-controls {
display: flex;
align-items: center;
gap: 8px;
}
.control-button {
display: flex;
align-items: center;
justify-content: center;
width: 28px;
height: 20px;
border: none;
background: transparent;
border-radius: 4px;
color: var(--color-text-secondary);
cursor: pointer;
transition: var(--transition);
}
.control-button:hover {
background: var(--color-surface-elevated);
color: var(--color-text-primary);
}
.pin-button.active {
color: var(--color-accent);
background: var(--color-accent) 20;
}
.close-button:hover {
background: var(--color-danger);
color: white;
}
</style>

View File

@@ -1,101 +1,101 @@
<template>
<div>
<!-- MessageToast component -->
<MessageToast ref="messageRef" />
<!-- ConfirmMessageBox component -->
<ConfirmMessageBox
:is-open="confirmVisible"
:title="confirmOptions.title"
:message="confirmOptions.message"
:type="confirmOptions.type"
:confirm-button-text="confirmOptions.confirmButtonText"
:cancel-button-text="confirmOptions.cancelButtonText"
:show-close="confirmOptions.showClose"
:center="confirmOptions.center"
@confirm="handleConfirm"
@cancel="handleCancel"
/>
</div>
</template>
<script setup lang="ts">
import { onMounted, reactive, ref } from 'vue'
import useConfirm, { type ConfirmOptions } from '@/hooks/useConfirm'
import useMessage from '@/hooks/useMessage'
import ConfirmMessageBox from './ConfirmMessageBox.vue'
import MessageToast from './MessageToast.vue'
const messageRef = ref<InstanceType<typeof MessageToast> | null>(null)
const confirmVisible = ref(false)
const confirmOptions = reactive<ConfirmOptions>({
message: '',
title: 'Confirm',
type: 'info',
confirmButtonText: 'Confirm',
cancelButtonText: 'Cancel',
showClose: true,
center: false
})
let confirmResolve: ((value: boolean) => void) | null = null
const handleConfirm = () => {
confirmVisible.value = false
if (confirmResolve) {
confirmResolve(true)
confirmResolve = null
}
}
const handleCancel = () => {
confirmVisible.value = false
if (confirmResolve) {
confirmResolve(false)
confirmResolve = null
}
}
const showConfirm = (options: ConfirmOptions): Promise<boolean> => {
return new Promise((resolve) => {
Object.assign(confirmOptions, {
title: 'Confirm',
type: 'info',
confirmButtonText: 'Confirm',
cancelButtonText: 'Cancel',
showClose: true,
center: false,
...options
})
confirmResolve = resolve
confirmVisible.value = true
})
}
onMounted(() => {
// Initialize message service
const { setMessageService } = useMessage()
if (messageRef.value) {
setMessageService({
success: messageRef.value.success,
error: messageRef.value.error,
warning: messageRef.value.warning,
info: messageRef.value.info
})
}
// Initialize confirm service
const { setConfirmService } = useConfirm()
setConfirmService({
confirm: showConfirm
})
})
</script>
<script lang="ts">
export default {
name: 'UIServiceProvider'
}
</script>
<template>
<div>
<!-- MessageToast component -->
<MessageToast ref="messageRef" />
<!-- ConfirmMessageBox component -->
<ConfirmMessageBox
:is-open="confirmVisible"
:title="confirmOptions.title"
:message="confirmOptions.message"
:type="confirmOptions.type"
:confirm-button-text="confirmOptions.confirmButtonText"
:cancel-button-text="confirmOptions.cancelButtonText"
:show-close="confirmOptions.showClose"
:center="confirmOptions.center"
@confirm="handleConfirm"
@cancel="handleCancel"
/>
</div>
</template>
<script setup lang="ts">
import { onMounted, reactive, ref } from 'vue'
import useConfirm, { type ConfirmOptions } from '@/hooks/useConfirm'
import useMessage from '@/hooks/useMessage'
import ConfirmMessageBox from './ConfirmMessageBox.vue'
import MessageToast from './MessageToast.vue'
const messageRef = ref<InstanceType<typeof MessageToast> | null>(null)
const confirmVisible = ref(false)
const confirmOptions = reactive<ConfirmOptions>({
message: '',
title: 'Confirm',
type: 'info',
confirmButtonText: 'Confirm',
cancelButtonText: 'Cancel',
showClose: true,
center: false
})
let confirmResolve: ((value: boolean) => void) | null = null
const handleConfirm = () => {
confirmVisible.value = false
if (confirmResolve) {
confirmResolve(true)
confirmResolve = null
}
}
const handleCancel = () => {
confirmVisible.value = false
if (confirmResolve) {
confirmResolve(false)
confirmResolve = null
}
}
const showConfirm = (options: ConfirmOptions): Promise<boolean> => {
return new Promise(resolve => {
Object.assign(confirmOptions, {
title: 'Confirm',
type: 'info',
confirmButtonText: 'Confirm',
cancelButtonText: 'Cancel',
showClose: true,
center: false,
...options
})
confirmResolve = resolve
confirmVisible.value = true
})
}
onMounted(() => {
// Initialize message service
const { setMessageService } = useMessage()
if (messageRef.value) {
setMessageService({
success: messageRef.value.success,
error: messageRef.value.error,
warning: messageRef.value.warning,
info: messageRef.value.info
})
}
// Initialize confirm service
const { setConfirmService } = useConfirm()
setConfirmService({
confirm: showConfirm
})
})
</script>
<script lang="ts">
export default {
name: 'UIServiceProvider'
}
</script>

View File

@@ -1,24 +1,24 @@
import { onMounted, onUnmounted } from 'vue'
import { IRPCActionType } from '@/utils/enum'
export function useATagClick () {
const handleATagClick = (e: MouseEvent) => {
if (e.target instanceof HTMLAnchorElement) {
if (e.target.href) {
if (!e.target.href.startsWith('http://localhost:3000')) {
e.preventDefault()
window.electron.sendRPC(IRPCActionType.OPEN_URL, e.target.href)
}
}
}
}
onMounted(() => {
document.addEventListener('click', handleATagClick)
})
onUnmounted(() => {
document.removeEventListener('click', handleATagClick)
})
}
import { onMounted, onUnmounted } from 'vue'
import { IRPCActionType } from '@/utils/enum'
export function useATagClick() {
const handleATagClick = (e: MouseEvent) => {
if (e.target instanceof HTMLAnchorElement) {
if (e.target.href) {
if (!e.target.href.startsWith('http://localhost:3000')) {
e.preventDefault()
window.electron.sendRPC(IRPCActionType.OPEN_URL, e.target.href)
}
}
}
}
onMounted(() => {
document.addEventListener('click', handleATagClick)
})
onUnmounted(() => {
document.removeEventListener('click', handleATagClick)
})
}

View File

@@ -1,83 +1,82 @@
import { defineStore } from 'pinia'
import { ref } from 'vue'
import type { IStringKeyMap } from '#/types/types'
export const useAppStore = defineStore('app', () => {
const settings = ref<IStringKeyMap>({
app: {
theme: 'light'
}
})
const loading = ref(false)
const error = ref<string | undefined>()
function clearError () {
error.value = undefined
}
const loadSettings = () => {
const savedTheme = localStorage.getItem('theme')
if (savedTheme) {
settings.value.app.theme = savedTheme
}
applyTheme(settings.value.app.theme || 'light')
}
function applyTheme (theme: string) {
const root = document.documentElement
root.classList.remove('light', 'dark', 'auto')
if (theme === 'auto') {
root.classList.add('auto')
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches
root.classList.add(prefersDark ? 'dark' : 'light')
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
mediaQuery.addEventListener('change', e => {
if (settings.value.app.theme === 'auto') {
root.classList.remove('light', 'dark')
root.classList.add(e.matches ? 'dark' : 'light')
}
})
} else {
root.classList.add(theme)
}
}
function setTheme (theme: 'light' | 'dark' | 'auto') {
settings.value.app.theme = theme
localStorage.setItem('theme', theme)
applyTheme(theme)
}
function toggleTheme () {
const currentTheme = settings.value.app.theme || 'light'
const themes = ['light', 'dark', 'auto'] as const
const currentIndex = themes.indexOf(currentTheme as any)
const nextTheme = themes[(currentIndex + 1) % themes.length]
setTheme(nextTheme)
}
function init () {
try {
loadSettings()
} catch (err) {
console.error('Application initialization failed:', err)
throw err
}
}
return {
init,
loadSettings,
settings,
loading,
error,
clearError,
setTheme,
toggleTheme,
applyTheme
}
})
import { defineStore } from 'pinia'
import { ref } from 'vue'
import type { IStringKeyMap } from '#/types/types'
export const useAppStore = defineStore('app', () => {
const settings = ref<IStringKeyMap>({
app: {
theme: 'light'
}
})
const loading = ref(false)
const error = ref<string | undefined>()
function clearError() {
error.value = undefined
}
const loadSettings = () => {
const savedTheme = localStorage.getItem('theme')
if (savedTheme) {
settings.value.app.theme = savedTheme
}
applyTheme(settings.value.app.theme || 'light')
}
function applyTheme(theme: string) {
const root = document.documentElement
root.classList.remove('light', 'dark', 'auto')
if (theme === 'auto') {
root.classList.add('auto')
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches
root.classList.add(prefersDark ? 'dark' : 'light')
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
mediaQuery.addEventListener('change', e => {
if (settings.value.app.theme === 'auto') {
root.classList.remove('light', 'dark')
root.classList.add(e.matches ? 'dark' : 'light')
}
})
} else {
root.classList.add(theme)
}
}
function setTheme(theme: 'light' | 'dark' | 'auto') {
settings.value.app.theme = theme
localStorage.setItem('theme', theme)
applyTheme(theme)
}
function toggleTheme() {
const currentTheme = settings.value.app.theme || 'light'
const themes = ['light', 'dark', 'auto'] as const
const currentIndex = themes.indexOf(currentTheme as any)
const nextTheme = themes[(currentIndex + 1) % themes.length]
setTheme(nextTheme)
}
function init() {
try {
loadSettings()
} catch (err) {
console.error('Application initialization failed:', err)
throw err
}
}
return {
init,
loadSettings,
settings,
loading,
error,
clearError,
setTheme,
toggleTheme,
applyTheme
}
})

View File

@@ -1,38 +1,38 @@
import { ref } from 'vue'
export interface ConfirmOptions {
title?: string
message: string
type?: 'info' | 'success' | 'warning' | 'error'
confirmButtonText?: string
cancelButtonText?: string
showClose?: boolean
center?: boolean
}
interface ConfirmService {
confirm: (options: ConfirmOptions) => Promise<boolean>
}
const confirmServiceRef = ref<ConfirmService | null>(null)
export function useConfirm () {
const setConfirmService = (service: ConfirmService) => {
confirmServiceRef.value = service
}
const confirm = (options: ConfirmOptions): Promise<boolean> => {
if (confirmServiceRef.value) {
return confirmServiceRef.value.confirm(options)
}
console.warn('Confirm service not initialized')
return Promise.resolve(false)
}
return {
setConfirmService,
confirm
}
}
export default useConfirm
import { ref } from 'vue'
export interface ConfirmOptions {
title?: string
message: string
type?: 'info' | 'success' | 'warning' | 'error'
confirmButtonText?: string
cancelButtonText?: string
showClose?: boolean
center?: boolean
}
interface ConfirmService {
confirm: (options: ConfirmOptions) => Promise<boolean>
}
const confirmServiceRef = ref<ConfirmService | null>(null)
export function useConfirm() {
const setConfirmService = (service: ConfirmService) => {
confirmServiceRef.value = service
}
const confirm = (options: ConfirmOptions): Promise<boolean> => {
if (confirmServiceRef.value) {
return confirmServiceRef.value.confirm(options)
}
console.warn('Confirm service not initialized')
return Promise.resolve(false)
}
return {
setConfirmService,
confirm
}
}
export default useConfirm

View File

@@ -1,60 +1,60 @@
import { ref } from 'vue'
import type { MessageOptions } from '@/components/ui/MessageToast.vue'
interface MessageService {
success: (message: string, options?: Partial<MessageOptions>) => string
error: (message: string, options?: Partial<MessageOptions>) => string
warning: (message: string, options?: Partial<MessageOptions>) => string
info: (message: string, options?: Partial<MessageOptions>) => string
}
const messageServiceRef = ref<MessageService | null>(null)
export function useMessage () {
const setMessageService = (service: MessageService) => {
messageServiceRef.value = service
}
const success = (message: string, options?: Partial<MessageOptions>) => {
if (messageServiceRef.value) {
return messageServiceRef.value.success(message, options)
}
console.warn('Message service not initialized')
return ''
}
const error = (message: string, options?: Partial<MessageOptions>) => {
if (messageServiceRef.value) {
return messageServiceRef.value.error(message, options)
}
console.warn('Message service not initialized')
return ''
}
const warning = (message: string, options?: Partial<MessageOptions>) => {
if (messageServiceRef.value) {
return messageServiceRef.value.warning(message, options)
}
console.warn('Message service not initialized')
return ''
}
const info = (message: string, options?: Partial<MessageOptions>) => {
if (messageServiceRef.value) {
return messageServiceRef.value.info(message, options)
}
console.warn('Message service not initialized')
return ''
}
return {
setMessageService,
success,
error,
warning,
info
}
}
export default useMessage
import { ref } from 'vue'
import type { MessageOptions } from '@/components/ui/MessageToast.vue'
interface MessageService {
success: (message: string, options?: Partial<MessageOptions>) => string
error: (message: string, options?: Partial<MessageOptions>) => string
warning: (message: string, options?: Partial<MessageOptions>) => string
info: (message: string, options?: Partial<MessageOptions>) => string
}
const messageServiceRef = ref<MessageService | null>(null)
export function useMessage() {
const setMessageService = (service: MessageService) => {
messageServiceRef.value = service
}
const success = (message: string, options?: Partial<MessageOptions>) => {
if (messageServiceRef.value) {
return messageServiceRef.value.success(message, options)
}
console.warn('Message service not initialized')
return ''
}
const error = (message: string, options?: Partial<MessageOptions>) => {
if (messageServiceRef.value) {
return messageServiceRef.value.error(message, options)
}
console.warn('Message service not initialized')
return ''
}
const warning = (message: string, options?: Partial<MessageOptions>) => {
if (messageServiceRef.value) {
return messageServiceRef.value.warning(message, options)
}
console.warn('Message service not initialized')
return ''
}
const info = (message: string, options?: Partial<MessageOptions>) => {
if (messageServiceRef.value) {
return messageServiceRef.value.info(message, options)
}
console.warn('Message service not initialized')
return ''
}
return {
setMessageService,
success,
error,
warning,
info
}
}
export default useMessage

View File

@@ -9,14 +9,8 @@ export interface UseVirtualGridOptions {
bufferFactor?: number
}
export function useVirtualGrid (options: UseVirtualGridOptions) {
const {
items,
itemHeight,
containerHeight,
gridItems = 1,
bufferFactor = 0.5
} = options
export function useVirtualGrid(options: UseVirtualGridOptions) {
const { items, itemHeight, containerHeight, gridItems = 1, bufferFactor = 0.5 } = options
const gridItemsRef = isRef(gridItems) ? gridItems : ref(gridItems)
const scrollTop = ref(0)
@@ -43,7 +37,7 @@ export function useVirtualGrid (options: UseVirtualGridOptions) {
return { startRow: 0, endRow: 0, visibleRows: 0 }
}
const buffer = Math.ceil(height / rowHeight * bufferFactor)
const buffer = Math.ceil((height / rowHeight) * bufferFactor)
const startRow = Math.max(0, Math.floor(scrollTop.value / rowHeight) - buffer)
const visibleRows = Math.ceil(height / rowHeight) + buffer * 2
const endRow = Math.min(totalRows, startRow + visibleRows)
@@ -74,21 +68,21 @@ export function useVirtualGrid (options: UseVirtualGridOptions) {
return startRow * rowHeight
})
function updateScrollTop (newScrollTop: number) {
function updateScrollTop(newScrollTop: number) {
scrollTop.value = newScrollTop
}
function scrollToItem (index: number) {
function scrollToItem(index: number) {
const { itemsPerRow, rowHeight } = gridCalculations.value
const rowIndex = Math.floor(index / itemsPerRow)
scrollTop.value = rowIndex * rowHeight
}
function scrollToTop () {
function scrollToTop() {
scrollTop.value = 0
}
function scrollToBottom () {
function scrollToBottom() {
const { totalHeight } = gridCalculations.value
scrollTop.value = Math.max(0, totalHeight - containerHeight.value)
}

View File

@@ -1,5 +1,5 @@
import { IRPCActionType } from '@/utils/enum'
export function setCurrentLanguage (lang: string) {
export function setCurrentLanguage(lang: string) {
window.electron.sendRPC(IRPCActionType.SET_CURRENT_LANGUAGE, lang)
}

View File

@@ -1,8 +1,5 @@
<template>
<div
id="main"
class="app-container"
>
<div id="main" class="app-container">
<InputBoxDialog />
<TitleBar />
<div class="app-background">
@@ -12,15 +9,9 @@
<main class="main-content">
<div class="content-container">
<router-view v-slot="{ Component, route }">
<transition
name="page"
mode="out-in"
>
<transition name="page" mode="out-in">
<keep-alive :include="keepAlivePages">
<component
:is="Component"
:key="route.path"
/>
<component :is="Component" :key="route.path" />
</keep-alive>
</transition>
</router-view>

View File

@@ -1,55 +1,55 @@
import 'video.js/dist/video-js.css'
import 'highlight.js/styles/stackoverflow-light.css'
import 'highlight.js/lib/common'
import hljsVuePlugin from '@highlightjs/vue-plugin'
import VueVideoPlayer from '@videojs-player/vue'
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
import { createApp } from 'vue'
import { createI18n } from 'vue-i18n'
import VueLazyLoad from 'vue3-lazyload'
import App from '@/App.vue'
import en from '@/i18n/locales/en.json'
import zhCN from '@/i18n/locales/zh-CN.json'
import zhTW from '@/i18n/locales/zh-TW.json'
import router from '@/router'
import { store } from '@/store'
import db from '@/utils/db'
type MessageSchema = typeof zhCN
window.electron.setVisualZoomLevelLimits(1, 1)
const app = createApp(App)
app.config.globalProperties.$$db = db
app.config.globalProperties.triggerRPC = window.electron.triggerRPC
app.config.globalProperties.sendRPC = window.electron.sendRPC
app.config.globalProperties.sendToMain = window.electron.sendToMain
const i18n = createI18n<[MessageSchema], 'en' | 'zh-CN' | 'zh-TW'>({
legacy: false,
locale: localStorage.getItem('currentLanguage') || 'zh-CN',
fallbackLocale: 'zh-CN',
messages: {
en,
'zh-CN': zhCN,
'zh-TW': zhTW
}
})
const pinia = createPinia()
pinia.use(piniaPluginPersistedstate)
app.use(VueLazyLoad, {
loading: './loading.jpg',
error: './unknown-file-type.svg',
delay: 500
})
app.use(i18n)
app.use(router)
app.use(store)
app.use(pinia)
app.use(hljsVuePlugin)
app.use(VueVideoPlayer)
app.mount('#app')
import 'video.js/dist/video-js.css'
import 'highlight.js/styles/stackoverflow-light.css'
import 'highlight.js/lib/common'
import hljsVuePlugin from '@highlightjs/vue-plugin'
import VueVideoPlayer from '@videojs-player/vue'
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
import { createApp } from 'vue'
import { createI18n } from 'vue-i18n'
import VueLazyLoad from 'vue3-lazyload'
import App from '@/App.vue'
import en from '@/i18n/locales/en.json'
import zhCN from '@/i18n/locales/zh-CN.json'
import zhTW from '@/i18n/locales/zh-TW.json'
import router from '@/router'
import { store } from '@/store'
import db from '@/utils/db'
type MessageSchema = typeof zhCN
window.electron.setVisualZoomLevelLimits(1, 1)
const app = createApp(App)
app.config.globalProperties.$$db = db
app.config.globalProperties.triggerRPC = window.electron.triggerRPC
app.config.globalProperties.sendRPC = window.electron.sendRPC
app.config.globalProperties.sendToMain = window.electron.sendToMain
const i18n = createI18n<[MessageSchema], 'en' | 'zh-CN' | 'zh-TW'>({
legacy: false,
locale: localStorage.getItem('currentLanguage') || 'zh-CN',
fallbackLocale: 'zh-CN',
messages: {
en,
'zh-CN': zhCN,
'zh-TW': zhTW
}
})
const pinia = createPinia()
pinia.use(piniaPluginPersistedstate)
app.use(VueLazyLoad, {
loading: './loading.jpg',
error: './unknown-file-type.svg',
delay: 500
})
app.use(i18n)
app.use(router)
app.use(store)
app.use(pinia)
app.use(hljsVuePlugin)
app.use(VueVideoPlayer)
app.mount('#app')

View File

@@ -2,26 +2,12 @@
<div class="switch-container">
<div class="switch-label-wrapper">
<span class="switch-label-text">
<span
v-for="(segment, index) in segments"
:key="index"
:style="segment.style"
>
<span v-for="(segment, index) in segments" :key="index" :style="segment.style">
{{ segment.text }}
</span>
<div
v-if="tooltip"
class="tooltip-wrapper"
>
<div
class="info-icon"
@click="toggleTooltip"
>
<svg
viewBox="0 0 20 20"
fill="currentColor"
class="info-svg"
>
<div v-if="tooltip" class="tooltip-wrapper">
<div class="info-icon" @click="toggleTooltip">
<svg viewBox="0 0 20 20" fill="currentColor" class="info-svg">
<path
fill-rule="evenodd"
d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z"
@@ -29,10 +15,7 @@
/>
</svg>
</div>
<div
v-show="showTooltip"
class="tooltip-content"
>
<div v-show="showTooltip" class="tooltip-content">
{{ tooltip }}
</div>
</div>
@@ -40,19 +23,12 @@
</div>
<div class="switch-control">
<label class="switch">
<input
v-model="value"
type="checkbox"
class="switch-input"
>
<input v-model="value" type="checkbox" class="switch-input" />
<span class="switch-slider">
<span class="switch-button" />
</span>
</label>
<div
v-if="activeText || inactiveText"
class="switch-text"
>
<div v-if="activeText || inactiveText" class="switch-text">
{{ value ? activeText : inactiveText }}
</div>
</div>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -6,13 +6,14 @@
<div class="setting-section">
<div class="form-group">
<div class="form-control">
<button
type="button"
class="action-button warning"
@click="handleConfirmClearDb"
>
<button type="button" class="action-button warning" @click="handleConfirmClearDb">
<Trash2Icon :size="16" />
{{ t('pages.manage.setting.clearCache', { percent: dbSizeAvailableRate, size: formatFileSize(dbSize) || 0 }) }}
{{
t('pages.manage.setting.clearCache', {
percent: dbSizeAvailableRate,
size: formatFileSize(dbSize) || 0
})
}}
</button>
</div>
</div>
@@ -38,10 +39,7 @@
</div>
<!-- Custom Rename Pattern Card -->
<div
v-if="form.customRename"
class="setting-card content-card"
>
<div v-if="form.customRename" class="setting-card content-card">
<div class="card-content">
<div class="setting-section">
<div class="section-header">
@@ -55,7 +53,7 @@
type="text"
class="form-input"
:placeholder="t('pages.manage.setting.customRenameTablePlaceholder')"
>
/>
</div>
<!-- Pattern Reference Table -->
@@ -70,21 +68,12 @@
</tr>
</thead>
<tbody>
<tr
v-for="(row, index) in customRenameFormatTable"
:key="index"
>
<td
class="clickable"
@click="handleCellClick(row, { property: 'placeholder' })"
>
<tr v-for="(row, index) in customRenameFormatTable" :key="index">
<td class="clickable" @click="handleCellClick(row, { property: 'placeholder' })">
{{ row.placeholder }}
</td>
<td>{{ row.description }}</td>
<td
class="clickable"
@click="handleCellClick(row, { property: 'placeholderB' })"
>
<td class="clickable" @click="handleCellClick(row, { property: 'placeholderB' })">
{{ row.placeholderB }}
</td>
<td>{{ row.descriptionB }}</td>
@@ -132,7 +121,7 @@
min="1"
max="9999"
step="1"
>
/>
</div>
</div>
@@ -151,7 +140,7 @@
:placeholder="t('pages.manage.setting.preSignedUrlExpireDesc')"
min="1"
step="1"
>
/>
</div>
</div>
</div>
@@ -168,17 +157,8 @@
</h4>
</div>
<div class="radio-group">
<label
v-for="item in pasteFormatList"
:key="item"
class="radio-option"
>
<input
v-model="form.pasteFormat"
type="radio"
:value="item"
class="radio-input"
>
<label v-for="item in pasteFormatList" :key="item" class="radio-option">
<input v-model="form.pasteFormat" type="radio" :value="item" class="radio-input" />
<span class="radio-custom" />
<span class="radio-text">
{{ t(`pages.manage.setting.copyFormat.${item}`) }}
@@ -187,9 +167,7 @@
</div>
<!-- Custom Copy Format -->
<div
class="form-group"
>
<div class="form-group">
<div class="form-label-wrapper">
<span class="form-label">
{{ t('pages.manage.setting.copyFormat.customTitle') }}
@@ -200,7 +178,7 @@
type="text"
class="form-input"
:placeholder="t('pages.manage.setting.copyFormat.customTips')"
>
/>
</div>
</div>
</div>
@@ -223,12 +201,8 @@
class="form-input group-input"
disabled
:placeholder="t('pages.manage.setting.defaultDownloadFolder')"
>
<button
type="button"
class="input-append-button"
@click="handleDownloadDirClick"
>
/>
<button type="button" class="input-append-button" @click="handleDownloadDirClick">
<FolderIcon :size="16" />
{{ t('pages.manage.setting.browse') }}
</button>
@@ -316,12 +290,8 @@ const switchFieldsConfigList = switchFieldsList.map(item => ({
}
],
tooltip: switchFieldsNoTipsList.includes(item) ? undefined : t(`pages.manage.setting.${item}Tips` as any),
activeText: switchFieldsHasActiveTextList.includes(item)
? t(`pages.manage.setting.${item}On` as any)
: undefined,
inactiveText: switchFieldsHasActiveTextList.includes(item)
? t(`pages.manage.setting.${item}Off` as any)
: undefined
activeText: switchFieldsHasActiveTextList.includes(item) ? t(`pages.manage.setting.${item}On` as any) : undefined,
inactiveText: switchFieldsHasActiveTextList.includes(item) ? t(`pages.manage.setting.${item}Off` as any) : undefined
}))
const switchFieldsSpecialList = [
@@ -363,14 +333,14 @@ const switchFieldsSpecialList = [
}
]
async function initData () {
async function initData() {
const config = (await getConfig()) as IStringKeyMap
settingsKeys.forEach(key => {
form.value[key] = config.settings[key] ?? form.value[key]
})
}
async function handleDownloadDirClick () {
async function handleDownloadDirClick() {
const result = await window.electron.triggerRPC<any>(IRPCActionType.MANAGE_SELECT_DOWNLOAD_FOLDER)
if (result) {
form.value.downloadDir = result
@@ -382,7 +352,7 @@ const handleCellClick = (row: any, column: any) => {
message.success(`${t('pages.manage.setting.copySuccess', { name: row[column.property] })}`)
}
function handleConfirmClearDb () {
function handleConfirmClearDb() {
confirm({
title: t('pages.manage.setting.notice'),
message: t('pages.manage.setting.clearCacheMsg'),
@@ -397,7 +367,7 @@ function handleConfirmClearDb () {
})
}
function confirmClearDb () {
function confirmClearDb() {
fileCacheDbInstance
.delete()
.then(() => {
@@ -409,7 +379,7 @@ function confirmClearDb () {
})
}
async function getIndexDbSize () {
async function getIndexDbSize() {
const size = (await navigator.storage.estimate()).usage ?? 0
const quota = (await navigator.storage.estimate()).quota ?? 0
dbSize.value = size

View File

@@ -33,7 +33,7 @@ export class FileCacheDb extends Dexie {
upyun: Table<IFileCache, string>
webdavplist: Table<IFileCache, string>
constructor () {
constructor() {
super('bucketFileDb')
const tableNames = [
'aliyun',

View File

@@ -10,7 +10,7 @@ export const useManageStore = defineStore('manageConfig', {
}
},
actions: {
async refreshConfig () {
async refreshConfig() {
this.config = (await getConfig()) ?? {}
}
},
@@ -26,23 +26,23 @@ export const useFileTransferStore = defineStore('fileTransfer', {
}
},
actions: {
refreshFileTransferList (newData: IStringKeyMap) {
refreshFileTransferList(newData: IStringKeyMap) {
this.fileTransferList = newData.fullList ?? []
this.success = newData.success
this.finished = newData.finished
},
resetFileTransferList () {
resetFileTransferList() {
this.fileTransferList = []
this.success = false
this.finished = false
},
getFileTransferList () {
getFileTransferList() {
return this.fileTransferList
},
isFinished () {
isFinished() {
return this.finished
},
isSuccess () {
isSuccess() {
return this.success
}
}
@@ -57,23 +57,23 @@ export const useDownloadFileTransferStore = defineStore('downloadFileTransfer',
}
},
actions: {
refreshDownloadFileTransferList (newData: IStringKeyMap) {
refreshDownloadFileTransferList(newData: IStringKeyMap) {
this.downloadFileTransferList = newData.fullList ?? []
this.success = newData.success
this.finished = newData.finished
},
resetDownloadFileTransferList () {
resetDownloadFileTransferList() {
this.downloadFileTransferList = []
this.success = false
this.finished = false
},
getDownloadFileTransferList () {
getDownloadFileTransferList() {
return this.downloadFileTransferList
},
isFinished () {
isFinished() {
return this.finished
},
isSuccess () {
isSuccess() {
return this.success
}
}

View File

@@ -1,72 +1,72 @@
import type { IStringKeyMap } from '#/types/types'
const AliyunAreaCodeName: IStringKeyMap = {
'oss-cn-hangzhou': '华东1(杭州)',
'oss-cn-shanghai': '华东2(上海)',
'oss-cn-nanjing': '华东5(南京)',
'oss-cn-fuzhou': '华东6(福州)',
'oss-cn-wuhan': '华中1(武汉)',
'oss-cn-qingdao': '华北1(青岛)',
'oss-cn-beijing': '华北2(北京)',
'oss-cn-zhangjiakou': '华北3(张家口)',
'oss-cn-huhehaote': '华北5(呼和浩特)',
'oss-cn-wulanchabu': '华北6(乌兰察布)',
'oss-cn-shenzhen': '华南1(深圳)',
'oss-cn-heyuan': '华南2(河源)',
'oss-cn-guangzhou': '华南3(广州)',
'oss-cn-chengdu': '西南1(成都)',
'oss-cn-hongkong': '中国香港',
'oss-us-west-1': '美国(硅谷)',
'oss-us-east-1': '美国(弗吉尼亚)',
'oss-ap-northeast-1': '日本(东京)',
'oss-ap-northeast-2': '韩国(首尔)',
'oss-ap-southeast-1': '新加坡',
'oss-ap-southeast-2': '澳大利亚(悉尼)',
'oss-ap-southeast-3': '马来西亚(吉隆坡)',
'oss-ap-southeast-5': '印度尼西亚(雅加达)',
'oss-ap-southeast-6': '菲律宾(马尼拉)',
'oss-ap-southeast-7': '泰国(曼谷)',
'oss-ap-south-1': '印度(孟买)',
'oss-eu-central-1': '德国(法兰克福)',
'oss-eu-west-1': '英国(伦敦)',
'oss-me-east-1': '阿联酋(迪拜)',
'oss-rg-china-mainland': '无地域属性'
}
const QiniuAreaCodeName: IStringKeyMap = {
z0: '华东-浙江',
'cn-east-2': '华东 浙江2',
z1: '华北-河北',
z2: '华南-广东',
na0: '北美-洛杉矶',
as0: '亚太-新加坡',
'ap-northeast-1': '亚太-首尔',
'ap-southeast-2': '亚太-河内'
}
const TencentAreaCodeName: IStringKeyMap = {
'ap-beijing-1': '北京一区',
'ap-beijing': '北京',
'ap-nanjing': '南京',
'ap-shanghai': '上海',
'ap-guangzhou': '广州',
'ap-chengdu': '成都',
'ap-chongqing': '重庆',
'ap-shenzhen-fsi': '深圳金融',
'ap-shagnhai-fsi': '上海金融',
'ap-beijing-fsi': '北京金融',
'ap-hongkong': '香港',
'ap-singapore': '新加坡',
'ap-mumbai': '孟买',
'ap-jakarta': '雅加达',
'ap-seoul': '首尔',
'ap-bangkok': '曼谷',
'ap-tokyo': '东京',
'na-siliconvalley': '硅谷(美西)',
'na-ashburn': '弗吉尼亚(美东)',
'na-toronto': '多伦多',
'sa-saopaulo': '圣保罗',
'eu-frankfurt': '法兰克福'
}
export { AliyunAreaCodeName, QiniuAreaCodeName, TencentAreaCodeName }
import type { IStringKeyMap } from '#/types/types'
const AliyunAreaCodeName: IStringKeyMap = {
'oss-cn-hangzhou': '华东1(杭州)',
'oss-cn-shanghai': '华东2(上海)',
'oss-cn-nanjing': '华东5(南京)',
'oss-cn-fuzhou': '华东6(福州)',
'oss-cn-wuhan': '华中1(武汉)',
'oss-cn-qingdao': '华北1(青岛)',
'oss-cn-beijing': '华北2(北京)',
'oss-cn-zhangjiakou': '华北3(张家口)',
'oss-cn-huhehaote': '华北5(呼和浩特)',
'oss-cn-wulanchabu': '华北6(乌兰察布)',
'oss-cn-shenzhen': '华南1(深圳)',
'oss-cn-heyuan': '华南2(河源)',
'oss-cn-guangzhou': '华南3(广州)',
'oss-cn-chengdu': '西南1(成都)',
'oss-cn-hongkong': '中国香港',
'oss-us-west-1': '美国(硅谷)',
'oss-us-east-1': '美国(弗吉尼亚)',
'oss-ap-northeast-1': '日本(东京)',
'oss-ap-northeast-2': '韩国(首尔)',
'oss-ap-southeast-1': '新加坡',
'oss-ap-southeast-2': '澳大利亚(悉尼)',
'oss-ap-southeast-3': '马来西亚(吉隆坡)',
'oss-ap-southeast-5': '印度尼西亚(雅加达)',
'oss-ap-southeast-6': '菲律宾(马尼拉)',
'oss-ap-southeast-7': '泰国(曼谷)',
'oss-ap-south-1': '印度(孟买)',
'oss-eu-central-1': '德国(法兰克福)',
'oss-eu-west-1': '英国(伦敦)',
'oss-me-east-1': '阿联酋(迪拜)',
'oss-rg-china-mainland': '无地域属性'
}
const QiniuAreaCodeName: IStringKeyMap = {
z0: '华东-浙江',
'cn-east-2': '华东 浙江2',
z1: '华北-河北',
z2: '华南-广东',
na0: '北美-洛杉矶',
as0: '亚太-新加坡',
'ap-northeast-1': '亚太-首尔',
'ap-southeast-2': '亚太-河内'
}
const TencentAreaCodeName: IStringKeyMap = {
'ap-beijing-1': '北京一区',
'ap-beijing': '北京',
'ap-nanjing': '南京',
'ap-shanghai': '上海',
'ap-guangzhou': '广州',
'ap-chengdu': '成都',
'ap-chongqing': '重庆',
'ap-shenzhen-fsi': '深圳金融',
'ap-shagnhai-fsi': '上海金融',
'ap-beijing-fsi': '北京金融',
'ap-hongkong': '香港',
'ap-singapore': '新加坡',
'ap-mumbai': '孟买',
'ap-jakarta': '雅加达',
'ap-seoul': '首尔',
'ap-bangkok': '曼谷',
'ap-tokyo': '东京',
'na-siliconvalley': '硅谷(美西)',
'na-ashburn': '弗吉尼亚(美东)',
'na-toronto': '多伦多',
'sa-saopaulo': '圣保罗',
'eu-frankfurt': '法兰克福'
}
export { AliyunAreaCodeName, QiniuAreaCodeName, TencentAreaCodeName }

View File

@@ -16,30 +16,30 @@ export const isUrlEncode = (url: string): boolean => {
export const handleUrlEncode = (url: string): string => (isUrlEncode(url) ? url : encodeURI(url))
export function randomStringGenerator (length: number): string {
export function randomStringGenerator(length: number): string {
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
return Array.from({ length })
.map(() => chars.charAt(Math.floor(Math.random() * chars.length)))
.join('')
}
export function renameFileNameWithTimestamp (oldName: string): string {
export function renameFileNameWithTimestamp(oldName: string): string {
return `${Math.floor(Date.now() / 1000)}${randomStringGenerator(5)}${window.node.path.extname(oldName)}`
}
export function renameFileNameWithRandomString (oldName: string, length: number = 5): string {
export function renameFileNameWithRandomString(oldName: string, length: number = 5): string {
return `${randomStringGenerator(length)}${window.node.path.extname(oldName)}`
}
function renameFormatHelper (num: number): string {
function renameFormatHelper(num: number): string {
return num.toString().length === 1 ? `0${num}` : num.toString()
}
function getMd5 (input: any): string {
function getMd5(input: any): string {
return window.node.crypto.createHash('md5').update(input).digest('hex')
}
export function renameFileNameWithCustomString (oldName: string, customFormat: string, affixFileName?: string): string {
export function renameFileNameWithCustomString(oldName: string, customFormat: string, affixFileName?: string): string {
const date = new Date()
const year = date.getFullYear().toString()
const fileBaseName = window.node.path.basename(oldName, window.node.path.extname(oldName))
@@ -80,7 +80,7 @@ export function renameFileNameWithCustomString (oldName: string, customFormat: s
return newName
}
export function renameFile (
export function renameFile(
{ timestampRename, randomStringRename, customRename, customRenameFormat }: IStringKeyMap,
oldName = ''
): string {
@@ -96,7 +96,7 @@ export function renameFile (
}
}
export async function formatLink (url: string, fileName: string, type: string, format?: string): Promise<string> {
export async function formatLink(url: string, fileName: string, type: string, format?: string): Promise<string> {
const encodedUrl = (await getConfig('settings.isEncodeUrl')) ? handleUrlEncode(url) : url
switch (type) {
case 'markdown':
@@ -119,27 +119,27 @@ export async function formatLink (url: string, fileName: string, type: string, f
}
}
export function getFileIconPath (fileName: string) {
export function getFileIconPath(fileName: string) {
const ext = window.node.path.extname(fileName).slice(1).toLowerCase()
return availableIconList.includes(ext) ? `${ext}.webp` : 'unknown.webp'
}
const units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB']
export function formatFileSize (size: number) {
export function formatFileSize(size: number) {
if (size === 0) return ''
const index = Math.floor(Math.log2(size) / 10)
return `${(size / Math.pow(2, index * 10)).toFixed(2)} ${units[index]}`
}
export function formatFileName (fileName: string, length: number = 20) {
export function formatFileName(fileName: string, length: number = 20) {
let ext = window.node.path.extname(fileName)
ext = ext.length > 5 ? ext.slice(ext.length - 5) : ext
const name = window.node.path.basename(fileName, ext)
return isNeedToShorten(fileName, length) ? `${safeSliceF(name, length - 3 - ext.length)}...${ext}` : fileName
}
export function formObjToTableData (obj: any) {
export function formObjToTableData(obj: any) {
const exclude = [undefined, null, '', 'transformedConfig']
return Object.keys(obj)
.filter(key => !exclude.includes(obj[key]))
@@ -150,7 +150,7 @@ export function formObjToTableData (obj: any) {
.sort((a, b) => a.key.localeCompare(b.key))
}
export function isValidUrl (str: string) {
export function isValidUrl(str: string) {
try {
return !!new URL(str)
} catch (e) {
@@ -169,7 +169,7 @@ export const svg = `
" style="stroke-width: 4px; fill: rgba(0, 0, 0, 0)"/>
`
export function customStrMatch (str: string, pattern: string): boolean {
export function customStrMatch(str: string, pattern: string): boolean {
if (!str || !pattern) return false
try {
const reg = new RegExp(pattern, 'ug')
@@ -180,7 +180,7 @@ export function customStrMatch (str: string, pattern: string): boolean {
}
}
export function customStrReplace (str: string, pattern: string, replacement: string): string {
export function customStrReplace(str: string, pattern: string, replacement: string): string {
if (!str || !pattern) return str
replacement = replacement || ''
let result = str

View File

@@ -1,15 +1,15 @@
import { IRPCActionType } from '@/utils/enum'
import type { IObj } from '#/types/types'
export function saveConfig (config: IObj | string, value?: any) {
export function saveConfig(config: IObj | string, value?: any) {
const configObj = typeof config === 'string' ? { [config]: value } : config
window.electron.sendRPC(IRPCActionType.MANAGE_SAVE_CONFIG, configObj)
}
export async function getConfig<T> (key?: string): Promise<T | undefined> {
export async function getConfig<T>(key?: string): Promise<T | undefined> {
return await window.electron.triggerRPC<T>(IRPCActionType.MANAGE_GET_CONFIG, key)
}
export function removeConfig (key: string, propName: string) {
export function removeConfig(key: string, propName: string) {
window.electron.sendRPC(IRPCActionType.MANAGE_REMOVE_CONFIG, key, propName)
}

View File

@@ -4,11 +4,11 @@ const AUTH_KEY_VALUE_RE = /(\w+)=["']?([^'"]{1,10000})["']?/
let NC = 0
const NC_PAD = '00000000'
function md5 (text: any) {
function md5(text: any) {
return window.node.crypto.createHash('md5').update(text).digest('hex')
}
export function digestAuthHeader (
export function digestAuthHeader(
method: string,
uri: string,
wwwAuthenticate: string,
@@ -66,7 +66,7 @@ export function digestAuthHeader (
return authstring
}
export async function getAuthHeader (method: string, host: string, uri: string, username: string, password: string) {
export async function getAuthHeader(method: string, host: string, uri: string, username: string, password: string) {
try {
const response = await fetch(`${host}${uri}`)
if (response.status === 401 && response.headers.get('www-authenticate')) {

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

@@ -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]: '![alt](url)',
[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)) || '![$fileName]($url)'
}
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]: '![alt](url)',
[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)) || '![$fileName]($url)'
}
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>

View File

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

View File

@@ -29,7 +29,7 @@ const setDefaultPicBed = (type: string) => {
}
export const store = {
install (app: App) {
install(app: App) {
app.provide(storeKey, {
state: readonly(state),
setDefaultPicBed

View File

@@ -39,11 +39,11 @@ export const handleUrlEncode = (url: string): string => (isUrlEncode(url) ? url
export const handleStreamlinePluginName = (name: string) => name.replace(/(@[^/]+\/)?picgo-plugin-/, '')
export const enforceNumber = (num: number | string) => (isNaN(+num) ? 0 : +num)
export function isNeedToShorten (alias: string, cutOff = 20) {
export function isNeedToShorten(alias: string, cutOff = 20) {
return [...alias].reduce((len, char) => len + (char.charCodeAt(0) > 255 ? 2 : 1), 0) > cutOff
}
export function safeSliceF (str: string, total: number) {
export function safeSliceF(str: string, total: number) {
let result = ''
let totalLen = 0
for (const s of str) {

View File

@@ -1,190 +1,208 @@
import type { IBuildInCompressOptions, IBuildInWaterMarkOptions } from 'piclist'
import type { IAliYunConfig, IAwsS3PListUserConfig, IGitHubConfig, IImgurConfig, ILocalConfig, ILskyConfig, IPicBedType, IQiniuConfig, IServerConfig, ISftpPlistConfig, IShortKeyConfig, ISMMSConfig, ISyncConfig, ITcYunConfig, IUploaderConfig, IUpYunConfig, IWebdavPlistConfig } from '#/types/types'
export type manualPageOpenType = 'window' | 'browser'
interface IPicGoPlugins {
[key: `picgo-plugin-${string}`]: boolean
}
export interface IConfigStruct {
picBed: {
uploader: string
current?: string
smms?: ISMMSConfig
qiniu?: IQiniuConfig
upyun?: IUpYunConfig
tcyun?: ITcYunConfig
github?: IGitHubConfig
aliyun?: IAliYunConfig
imgur?: IImgurConfig
webdavplist?: IWebdavPlistConfig
local?: ILocalConfig
sftpplist?: ISftpPlistConfig
lskyplist?: ILskyConfig
'aws-s3-plist': IAwsS3PListUserConfig
proxy?: string
transformer?: string
list: IPicBedType[]
[others: string]: any
}
settings: {
shortKey: {
[key: string]: IShortKeyConfig
}
isAlwaysForceReload: boolean
logLevel: string[]
logPath: string
logFileSizeLimit: number
isAutoListenClipboard: boolean
isListeningClipboard: boolean
showUpdateTip: boolean
miniWindowPosition: [number, number]
miniWindowOntop: boolean
mainWindowWidth: number
mainWindowHeight: number
isHideDock: boolean
autoCloseMiniWindow: boolean
autoCloseMainWindow: boolean
isCustomMiniIcon: boolean
customMiniIcon: string
startMode: string
autoRename: boolean
deleteCloudFile: boolean
server: IServerConfig
serverKey: string
pasteStyle: string
aesPassword: string
rename: boolean
sync: ISyncConfig
tempDirPath: string
language: string
customLink: string
manualPageOpen: manualPageOpenType
encodeOutputURL: boolean
useShortUrl: boolean
shortUrlServer: string
c1nToken: string
cfWorkerHost: string
yourlsDomain: string
yourlsSignature: string
sinkDomain: string
sinkToken: string
isSilentNotice: boolean
proxy: string
registry: string
autoCopy: boolean
enableWebServer: boolean
webServerHost: string
webServerPort: number
webServerPath: string
deleteLocalFile: boolean
uploadResultNotification: boolean
uploadNotification: boolean
useBuiltinClipboard: boolean
autoStart: boolean
autoImport: boolean
autoImportPicBed: string[]
}
needReload: boolean
picgoPlugins: IPicGoPlugins
uploader: IUploaderConfig
buildIn: {
compress: IBuildInCompressOptions
watermark: IBuildInWaterMarkOptions
rename: {
enable: boolean
format: string
}
skipProcess: {
skipProcessExtList: string
}
}
debug: boolean
PICGO_ENV: string
}
export const configPaths = {
picBed: {
current: 'picBed.current',
uploader: 'picBed.uploader',
secondUploader: 'picBed.secondUploader',
secondUploaderId: 'picBed.secondUploaderId',
secondUploaderConfig: 'picBed.secondUploaderConfig',
proxy: 'picBed.proxy',
transformer: 'picBed.transformer',
list: 'picBed.list'
},
settings: {
shortKey: {
_path: 'settings.shortKey',
'picgo:upload': 'settings.shortKey[picgo:upload]'
},
isAlwaysForceReload: 'settings.isAlwaysForceReload',
logLevel: 'settings.logLevel',
logPath: 'settings.logPath',
logFileSizeLimit: 'settings.logFileSizeLimit',
isAutoListenClipboard: 'settings.isAutoListenClipboard',
isListeningClipboard: 'settings.isListeningClipboard',
showUpdateTip: 'settings.showUpdateTip',
miniWindowPosition: 'settings.miniWindowPosition',
miniWindowOntop: 'settings.miniWindowOntop',
isHideDock: 'settings.isHideDock',
mainWindowWidth: 'settings.mainWindowWidth',
mainWindowHeight: 'settings.mainWindowHeight',
autoCloseMiniWindow: 'settings.autoCloseMiniWindow',
autoCloseMainWindow: 'settings.autoCloseMainWindow',
isCustomMiniIcon: 'settings.isCustomMiniIcon',
customMiniIcon: 'settings.customMiniIcon',
startMode: 'settings.startMode',
autoRename: 'settings.autoRename',
deleteCloudFile: 'settings.deleteCloudFile',
server: 'settings.server',
serverKey: 'settings.serverKey',
pasteStyle: 'settings.pasteStyle',
aesPassword: 'settings.aesPassword',
rename: 'settings.rename',
sync: 'settings.sync',
tempDirPath: 'settings.tempDirPath',
language: 'settings.language',
customLink: 'settings.customLink',
manualPageOpen: 'settings.manualPageOpen',
encodeOutputURL: 'settings.encodeOutputURL',
useShortUrl: 'settings.useShortUrl',
shortUrlServer: 'settings.shortUrlServer',
c1nToken: 'settings.c1nToken',
cfWorkerHost: 'settings.cfWorkerHost',
yourlsDomain: 'settings.yourlsDomain',
yourlsSignature: 'settings.yourlsSignature',
sinkDomain: 'settings.sinkDomain',
sinkToken: 'settings.sinkToken',
isSilentNotice: 'settings.isSilentNotice',
proxy: 'settings.proxy',
registry: 'settings.registry',
autoCopy: 'settings.autoCopy',
enableWebServer: 'settings.enableWebServer',
webServerHost: 'settings.webServerHost',
webServerPort: 'settings.webServerPort',
webServerPath: 'settings.webServerPath',
deleteLocalFile: 'settings.deleteLocalFile',
uploadResultNotification: 'settings.uploadResultNotification',
uploadNotification: 'settings.uploadNotification',
useBuiltinClipboard: 'settings.useBuiltinClipboard',
autoStart: 'settings.autoStart',
autoImport: 'settings.autoImport',
autoImportPicBed: 'settings.autoImportPicBed',
enableSecondUploader: 'settings.enableSecondUploader'
},
needReload: 'needReload',
picgoPlugins: 'picgoPlugins',
uploader: 'uploader',
buildIn: {
compress: 'buildIn.compress',
watermark: 'buildIn.watermark',
rename: 'buildIn.rename',
skipProcess: 'buildIn.skipProcess'
},
debug: 'debug',
PICGO_ENV: 'PICGO_ENV'
}
import type { IBuildInCompressOptions, IBuildInWaterMarkOptions } from 'piclist'
import type {
IAliYunConfig,
IAwsS3PListUserConfig,
IGitHubConfig,
IImgurConfig,
ILocalConfig,
ILskyConfig,
IPicBedType,
IQiniuConfig,
IServerConfig,
ISftpPlistConfig,
IShortKeyConfig,
ISMMSConfig,
ISyncConfig,
ITcYunConfig,
IUploaderConfig,
IUpYunConfig,
IWebdavPlistConfig
} from '#/types/types'
export type manualPageOpenType = 'window' | 'browser'
interface IPicGoPlugins {
[key: `picgo-plugin-${string}`]: boolean
}
export interface IConfigStruct {
picBed: {
uploader: string
current?: string
smms?: ISMMSConfig
qiniu?: IQiniuConfig
upyun?: IUpYunConfig
tcyun?: ITcYunConfig
github?: IGitHubConfig
aliyun?: IAliYunConfig
imgur?: IImgurConfig
webdavplist?: IWebdavPlistConfig
local?: ILocalConfig
sftpplist?: ISftpPlistConfig
lskyplist?: ILskyConfig
'aws-s3-plist': IAwsS3PListUserConfig
proxy?: string
transformer?: string
list: IPicBedType[]
[others: string]: any
}
settings: {
shortKey: {
[key: string]: IShortKeyConfig
}
isAlwaysForceReload: boolean
logLevel: string[]
logPath: string
logFileSizeLimit: number
isAutoListenClipboard: boolean
isListeningClipboard: boolean
showUpdateTip: boolean
miniWindowPosition: [number, number]
miniWindowOntop: boolean
mainWindowWidth: number
mainWindowHeight: number
isHideDock: boolean
autoCloseMiniWindow: boolean
autoCloseMainWindow: boolean
isCustomMiniIcon: boolean
customMiniIcon: string
startMode: string
autoRename: boolean
deleteCloudFile: boolean
server: IServerConfig
serverKey: string
pasteStyle: string
aesPassword: string
rename: boolean
sync: ISyncConfig
tempDirPath: string
language: string
customLink: string
manualPageOpen: manualPageOpenType
encodeOutputURL: boolean
useShortUrl: boolean
shortUrlServer: string
c1nToken: string
cfWorkerHost: string
yourlsDomain: string
yourlsSignature: string
sinkDomain: string
sinkToken: string
isSilentNotice: boolean
proxy: string
registry: string
autoCopy: boolean
enableWebServer: boolean
webServerHost: string
webServerPort: number
webServerPath: string
deleteLocalFile: boolean
uploadResultNotification: boolean
uploadNotification: boolean
useBuiltinClipboard: boolean
autoStart: boolean
autoImport: boolean
autoImportPicBed: string[]
}
needReload: boolean
picgoPlugins: IPicGoPlugins
uploader: IUploaderConfig
buildIn: {
compress: IBuildInCompressOptions
watermark: IBuildInWaterMarkOptions
rename: {
enable: boolean
format: string
}
skipProcess: {
skipProcessExtList: string
}
}
debug: boolean
PICGO_ENV: string
}
export const configPaths = {
picBed: {
current: 'picBed.current',
uploader: 'picBed.uploader',
secondUploader: 'picBed.secondUploader',
secondUploaderId: 'picBed.secondUploaderId',
secondUploaderConfig: 'picBed.secondUploaderConfig',
proxy: 'picBed.proxy',
transformer: 'picBed.transformer',
list: 'picBed.list'
},
settings: {
shortKey: {
_path: 'settings.shortKey',
'picgo:upload': 'settings.shortKey[picgo:upload]'
},
isAlwaysForceReload: 'settings.isAlwaysForceReload',
logLevel: 'settings.logLevel',
logPath: 'settings.logPath',
logFileSizeLimit: 'settings.logFileSizeLimit',
isAutoListenClipboard: 'settings.isAutoListenClipboard',
isListeningClipboard: 'settings.isListeningClipboard',
showUpdateTip: 'settings.showUpdateTip',
miniWindowPosition: 'settings.miniWindowPosition',
miniWindowOntop: 'settings.miniWindowOntop',
isHideDock: 'settings.isHideDock',
mainWindowWidth: 'settings.mainWindowWidth',
mainWindowHeight: 'settings.mainWindowHeight',
autoCloseMiniWindow: 'settings.autoCloseMiniWindow',
autoCloseMainWindow: 'settings.autoCloseMainWindow',
isCustomMiniIcon: 'settings.isCustomMiniIcon',
customMiniIcon: 'settings.customMiniIcon',
startMode: 'settings.startMode',
autoRename: 'settings.autoRename',
deleteCloudFile: 'settings.deleteCloudFile',
server: 'settings.server',
serverKey: 'settings.serverKey',
pasteStyle: 'settings.pasteStyle',
aesPassword: 'settings.aesPassword',
rename: 'settings.rename',
sync: 'settings.sync',
tempDirPath: 'settings.tempDirPath',
language: 'settings.language',
customLink: 'settings.customLink',
manualPageOpen: 'settings.manualPageOpen',
encodeOutputURL: 'settings.encodeOutputURL',
useShortUrl: 'settings.useShortUrl',
shortUrlServer: 'settings.shortUrlServer',
c1nToken: 'settings.c1nToken',
cfWorkerHost: 'settings.cfWorkerHost',
yourlsDomain: 'settings.yourlsDomain',
yourlsSignature: 'settings.yourlsSignature',
sinkDomain: 'settings.sinkDomain',
sinkToken: 'settings.sinkToken',
isSilentNotice: 'settings.isSilentNotice',
proxy: 'settings.proxy',
registry: 'settings.registry',
autoCopy: 'settings.autoCopy',
enableWebServer: 'settings.enableWebServer',
webServerHost: 'settings.webServerHost',
webServerPort: 'settings.webServerPort',
webServerPath: 'settings.webServerPath',
deleteLocalFile: 'settings.deleteLocalFile',
uploadResultNotification: 'settings.uploadResultNotification',
uploadNotification: 'settings.uploadNotification',
useBuiltinClipboard: 'settings.useBuiltinClipboard',
autoStart: 'settings.autoStart',
autoImport: 'settings.autoImport',
autoImportPicBed: 'settings.autoImportPicBed',
enableSecondUploader: 'settings.enableSecondUploader'
},
needReload: 'needReload',
picgoPlugins: 'picgoPlugins',
uploader: 'uploader',
buildIn: {
compress: 'buildIn.compress',
watermark: 'buildIn.watermark',
rename: 'buildIn.rename',
skipProcess: 'buildIn.skipProcess'
},
debug: 'debug',
PICGO_ENV: 'PICGO_ENV'
}

View File

@@ -1,14 +1,14 @@
export const SHOW_INPUT_BOX = 'SHOW_INPUT_BOX'
export const SHOW_INPUT_BOX_RESPONSE = 'SHOW_INPUT_BOX_RESPONSE'
// picgo plugin
export const PICGO_CONFIG_PLUGIN = 'PICGO_CONFIG_PLUGIN'
export const PICGO_HANDLE_PLUGIN_ING = 'PICGO_HANDLE_PLUGIN_ING'
export const PICGO_HANDLE_PLUGIN_DONE = 'PICGO_HANDLE_PLUGIN_DONE'
export const PICGO_TOGGLE_PLUGIN = 'PICGO_TOGGLE_PLUGIN'
// picgo uploader
export const RENAME_FILE_NAME = 'RENAME_FILE_NAME'
export const GET_RENAME_FILE_NAME = 'GET_RENAME_FILE_NAME'
export const SHOW_MAIN_PAGE_QRCODE = 'SHOW_MAIN_PAGE_QRCODE'
// rpc
export const RPC_ACTIONS = 'RPC_ACTIONS'
export const RPC_ACTIONS_INVOKE = 'RPC_ACTIONS_INVOKE'
export const SHOW_INPUT_BOX = 'SHOW_INPUT_BOX'
export const SHOW_INPUT_BOX_RESPONSE = 'SHOW_INPUT_BOX_RESPONSE'
// picgo plugin
export const PICGO_CONFIG_PLUGIN = 'PICGO_CONFIG_PLUGIN'
export const PICGO_HANDLE_PLUGIN_ING = 'PICGO_HANDLE_PLUGIN_ING'
export const PICGO_HANDLE_PLUGIN_DONE = 'PICGO_HANDLE_PLUGIN_DONE'
export const PICGO_TOGGLE_PLUGIN = 'PICGO_TOGGLE_PLUGIN'
// picgo uploader
export const RENAME_FILE_NAME = 'RENAME_FILE_NAME'
export const GET_RENAME_FILE_NAME = 'GET_RENAME_FILE_NAME'
export const SHOW_MAIN_PAGE_QRCODE = 'SHOW_MAIN_PAGE_QRCODE'
// rpc
export const RPC_ACTIONS = 'RPC_ACTIONS'
export const RPC_ACTIONS_INVOKE = 'RPC_ACTIONS_INVOKE'

View File

@@ -2,11 +2,11 @@ import { getRawData } from '@/utils/common'
import { IRPCActionType } from '@/utils/enum'
import type { IObj } from '#/types/types'
export function saveConfig (config: IObj | string, value?: any) {
export function saveConfig(config: IObj | string, value?: any) {
const configObject = typeof config === 'string' ? { [config]: value } : config
window.electron.sendRPC(IRPCActionType.PICLIST_SAVE_CONFIG, getRawData(configObject))
}
export async function getConfig<T> (key?: string): Promise<T | undefined> {
export async function getConfig<T>(key?: string): Promise<T | undefined> {
return await window.electron.triggerRPC<T>(IRPCActionType.PICLIST_GET_CONFIG, key)
}

View File

@@ -16,11 +16,11 @@ interface IObject {
[propName: string]: any
}
type IResult<T> = T & {
id: string
createdAt: number
updatedAt: number
}
type IResult<T> = T & {
id: string
createdAt: number
updatedAt: number
}
export class GalleryDB implements IGalleryDB {
async #actionHandler<T>(method: string, ...args: any[]): Promise<T | undefined> {
@@ -39,7 +39,7 @@ export class GalleryDB implements IGalleryDB {
return await this.#actionHandler<IResult<T>[]>(IRPCActionType.GALLERY_INSERT_DB_BATCH, value)
}
async updateById (id: string, value: IObject): Promise<boolean> {
async updateById(id: string, value: IObject): Promise<boolean> {
return (await this.#actionHandler<boolean>(IRPCActionType.GALLERY_UPDATE_BY_ID_DB, id, value)) || false
}
@@ -47,7 +47,7 @@ export class GalleryDB implements IGalleryDB {
return await this.#actionHandler<IResult<T> | undefined>(IRPCActionType.GALLERY_GET_BY_ID_DB, id)
}
async removeById (id: string): Promise<void> {
async removeById(id: string): Promise<void> {
return await this.#actionHandler<void>(IRPCActionType.GALLERY_REMOVE_BY_ID_DB, id)
}
}

View File

@@ -1,15 +1,15 @@
import { onBeforeUnmount, onMounted } from 'vue'
function disableDrag (e: DragEvent) {
function disableDrag(e: DragEvent) {
const dropzone = document.getElementById('upload-area')
if (dropzone === null || !dropzone.contains((e.target as Node))) {
if (dropzone === null || !dropzone.contains(e.target as Node)) {
e.preventDefault()
e.dataTransfer!.effectAllowed = 'none'
e.dataTransfer!.dropEffect = 'none'
}
}
export function useDragEventListeners () {
export function useDragEventListeners() {
onMounted(() => {
window.addEventListener('dragenter', disableDrag, false)
window.addEventListener('dragover', disableDrag)

View File

@@ -1,163 +1,163 @@
export const IPasteStyle = {
MARKDOWN: 'markdown',
HTML: 'HTML',
URL: 'URL',
UBB: 'UBB',
CUSTOM: 'Custom'
}
export const IWindowList = {
SETTING_WINDOW: 'SETTING_WINDOW',
TRAY_WINDOW: 'TRAY_WINDOW',
MINI_WINDOW: 'MINI_WINDOW',
RENAME_WINDOW: 'RENAME_WINDOW',
TOOLBOX_WINDOW: 'TOOLBOX_WINDOW'
}
export const IRPCActionType = {
// system rpc
RELOAD_APP: 'RELOAD_APP',
OPEN_URL: 'OPEN_URL',
OPEN_FILE: 'OPEN_FILE',
HIDE_DOCK: 'HIDE_DOCK',
SET_CURRENT_LANGUAGE: 'SET_CURRENT_LANGUAGE',
OPEN_WINDOW: 'OPEN_WINDOW',
OPEN_MINI_WINDOW: 'OPEN_MINI_WINDOW',
CLOSE_WINDOW: 'CLOSE_WINDOW',
MINIMIZE_WINDOW: 'MINIMIZE_WINDOW',
SHOW_MINI_PAGE_MENU: 'SHOW_MINI_PAGE_MENU',
SHOW_MAIN_PAGE_MENU: 'SHOW_MAIN_PAGE_MENU',
SHOW_UPLOAD_PAGE_MENU: 'SHOW_UPLOAD_PAGE_MENU',
SHOW_SECOND_UPLOADER_MENU: 'SHOW_SECOND_UPLOADER_MENU',
SHOW_PLUGIN_PAGE_MENU: 'SHOW_PLUGIN_PAGE_MENU',
SET_MINI_WINDOW_POS: 'SET_MINI_WINDOW_POS',
MINI_WINDOW_ON_TOP: 'MINI_WINDOW_ON_TOP',
MAIN_WINDOW_ON_TOP: 'MAIN_WINDOW_ON_TOP',
UPDATE_MINI_WINDOW_ICON: 'UPDATE_MINI_WINDOW_ICON',
REFRESH_SETTING_WINDOW: 'REFRESH_SETTING_WINDOW',
// picbed RPC
PICBED_GET_PICBED_CONFIG: 'PICBED_GET_PICBED_CONFIG',
PICBED_GET_CONFIG_LIST: 'PICBED_GET_CONFIG_LIST',
PICBED_DELETE_CONFIG: 'PICBED_DELETE_CONFIG',
UPLOADER_CHANGE_CURRENT: 'UPLOADER_CHANGE_CURRENT',
UPLOADER_SELECT: 'UPLOADER_SELECT',
UPLOADER_UPDATE_CONFIG: 'UPLOADER_UPDATE_CONFIG',
UPLOADER_RESET_CONFIG: 'UPLOADER_RESET_CONFIG',
DELETE_ALL_API: 'DELETE_ALL_API',
// toolbox rpc
TOOLBOX_CHECK: 'TOOLBOX_CHECK',
TOOLBOX_CHECK_RES: 'TOOLBOX_CHECK_RES',
TOOLBOX_CHECK_FIX: 'TOOLBOX_CHECK_FIX',
// main app setting rpc
PICLIST_GET_CONFIG: 'PICLIST_GET_CONFIG',
PICLIST_GET_CONFIG_SYNC: 'PICLIST_GET_CONFIG_SYNC',
PICLIST_SAVE_CONFIG: 'PICLIST_SAVE_CONFIG',
PICLIST_OPEN_FILE: 'PICLIST_OPEN_FILE',
PICLIST_OPEN_DIRECTORY: 'PICLIST_OPEN_DIRECTORY',
PICLIST_AUTO_START: 'PICLIST_AUTO_START',
// shortkey setting rpc
SHORTKEY_UPDATE: 'SHORTKEY_UPDATE',
SHORTKEY_BIND_OR_UNBIND: 'SHORTKEY_BIND_OR_UNBIND',
SHORTKEY_TOGGLE_SHORTKEY_MODIFIED_MODE: 'SHORTKEY_TOGGLE_SHORTKEY_MODIFIED_MODE',
// configuration setting rpc
CONFIGURE_MIGRATE_FROM_PICGO: 'CONFIGURE_MIGRATE_FROM_PICGO',
CONFIGURE_UPLOAD_COMMON_CONFIG: 'CONFIGURE_UPLOAD_COMMON_CONFIG',
CONFIGURE_UPLOAD_MANAGE_CONFIG: 'CONFIGURE_UPLOAD_MANAGE_CONFIG',
CONFIGURE_UPLOAD_ALL_CONFIG: 'CONFIGURE_UPLOAD_ALL_CONFIG',
CONFIGURE_DOWNLOAD_COMMON_CONFIG: 'CONFIGURE_DOWNLOAD_COMMON_CONFIG',
CONFIGURE_DOWNLOAD_MANAGE_CONFIG: 'CONFIGURE_DOWNLOAD_MANAGE_CONFIG',
CONFIGURE_DOWNLOAD_ALL_CONFIG: 'CONFIGURE_DOWNLOAD_ALL_CONFIG',
// advanced setting rpc
ADVANCED_UPDATE_SERVER: 'ADVANCED_UPDATE_SERVER',
ADVANCED_STOP_WEB_SERVER: 'ADVANCED_STOP_WEB_SERVER',
ADVANCED_RESTART_WEB_SERVER: 'ADVANCED_RESTART_WEB_SERVER',
// upload and main page rpc
MAIN_GET_PICBED: 'MAIN_GET_PICBED',
UPLOAD_CLIPBOARD_FILES_FROM_UPLOAD_PAGE: 'UPLOAD_CLIPBOARD_FILES_FROM_UPLOAD_PAGE',
UPLOAD_CHOOSED_FILES: 'UPLOAD_CHOOSED_FILES',
// gallery rpc
GALLERY_PASTE_TEXT: 'GALLERY_PASTE_TEXT',
GALLERY_REMOVE_FILES: 'GALLERY_REMOVE_FILES',
GALLERY_GET_DB: 'GALLERY_GET_DB',
GALLERY_GET_BY_ID_DB: 'GALLERY_GET_BY_ID_DB',
GALLERY_UPDATE_BY_ID_DB: 'GALLERY_UPDATE_BY_ID_DB',
GALLERY_REMOVE_BY_ID_DB: 'GALLERY_REMOVE_BY_ID_DB',
GALLERY_INSERT_DB: 'GALLERY_INSERT_DB',
GALLERY_INSERT_DB_BATCH: 'GALLERY_INSERT_DB_BATCH',
// plugin rpc
PLUGIN_GET_LIST: 'PLUGIN_GET_LIST',
PLUGIN_INSTALL: 'PLUGIN_INSTALL',
PLUGIN_IMPORT_LOCAL: 'PLUGIN_IMPORT_LOCAL',
PLUGIN_UPDATE_ALL: 'PLUGIN_UPDATE_ALL',
// tray rpc
TRAY_SET_TOOL_TIP: 'TRAY_SET_TOOL_TIP',
TRAY_GET_SHORT_URL: 'TRAY_GET_SHORT_URL',
TRAY_UPLOAD_CLIPBOARD_FILES: 'TRAY_UPLOAD_CLIPBOARD_FILES',
// manage rpc
MANAGE_GET_CONFIG: 'MANAGE_GET_CONFIG',
MANAGE_SAVE_CONFIG: 'MANAGE_SAVE_CONFIG',
MANAGE_REMOVE_CONFIG: 'MANAGE_REMOVE_CONFIG',
MANAGE_GET_BUCKET_LIST: 'MANAGE_GET_BUCKET_LIST',
MANAGE_GET_BUCKET_LIST_BACKSTAGE: 'MANAGE_GET_BUCKET_LIST_BACKSTAGE',
MANAGE_GET_BUCKET_LIST_RECURSIVELY: 'MANAGE_GET_BUCKET_LIST_RECURSIVELY',
MANAGE_CREATE_BUCKET: 'MANAGE_CREATE_BUCKET',
MANAGE_GET_BUCKET_FILE_LIST: 'MANAGE_GET_BUCKET_FILE_LIST',
MANAGE_GET_BUCKET_DOMAIN: 'MANAGE_GET_BUCKET_DOMAIN',
MANAGE_SET_BUCKET_ACL_POLICY: 'MANAGE_SET_BUCKET_ACL_POLICY',
MANAGE_RENAME_BUCKET_FILE: 'MANAGE_RENAME_BUCKET_FILE',
MANAGE_DELETE_BUCKET_FILE: 'MANAGE_DELETE_BUCKET_FILE',
MANAGE_DELETE_BUCKET_FOLDER: 'MANAGE_DELETE_BUCKET_FOLDER',
MANAGE_GET_PRE_SIGNED_URL: 'MANAGE_GET_PRE_SIGNED_URL',
MANAGE_UPLOAD_BUCKET_FILE: 'MANAGE_UPLOAD_BUCKET_FILE',
MANAGE_DOWNLOAD_BUCKET_FILE: 'MANAGE_DOWNLOAD_BUCKET_FILE',
MANAGE_CREATE_BUCKET_FOLDER: 'MANAGE_CREATE_BUCKET_FOLDER',
MANAGE_OPEN_FILE_SELECT_DIALOG: 'MANAGE_OPEN_FILE_SELECT_DIALOG',
MANAGE_GET_UPLOAD_TASK_LIST: 'MANAGE_GET_UPLOAD_TASK_LIST',
MANAGE_GET_DOWNLOAD_TASK_LIST: 'MANAGE_GET_DOWNLOAD_TASK_LIST',
MANAGE_DELETE_UPLOADED_TASK: 'MANAGE_DELETE_UPLOADED_TASK',
MANAGE_DELETE_ALL_UPLOADED_TASK: 'MANAGE_DELETE_ALL_UPLOADED_TASK',
MANAGE_DELETE_DOWNLOADED_TASK: 'MANAGE_DELETE_DOWNLOADED_TASK',
MANAGE_DELETE_ALL_DOWNLOADED_TASK: 'MANAGE_DELETE_ALL_DOWNLOADED_TASK',
MANAGE_SELECT_DOWNLOAD_FOLDER: 'MANAGE_SELECT_DOWNLOAD_FOLDER',
MANAGE_GET_DEFAULT_DOWNLOAD_FOLDER: 'MANAGE_GET_DEFAULT_DOWNLOAD_FOLDER',
MANAGE_OPEN_DOWNLOADED_FOLDER: 'MANAGE_OPEN_DOWNLOADED_FOLDER',
MANAGE_OPEN_LOCAL_FILE: 'MANAGE_OPEN_LOCAL_FILE',
MANAGE_DOWNLOAD_FILE_FROM_URL: 'MANAGE_DOWNLOAD_FILE_FROM_URL',
MANAGE_CONVERT_PATH_TO_BASE64: 'MANAGE_CONVERT_PATH_TO_BASE64'
}
export const IToolboxItemType = {
IS_CONFIG_FILE_BROKEN: 'IS_CONFIG_FILE_BROKEN',
IS_GALLERY_FILE_BROKEN: 'IS_GALLERY_FILE_BROKEN',
HAS_PROBLEM_WITH_CLIPBOARD_PIC_UPLOAD: 'HAS_PROBLEM_WITH_CLIPBOARD_PIC_UPLOAD',
HAS_PROBLEM_WITH_PROXY: 'HAS_PROBLEM_WITH_PROXY'
}
export const IToolboxItemCheckStatus = {
INIT: 'init',
LOADING: 'loading',
SUCCESS: 'success',
ERROR: 'error'
}
export const ISartMode = {
QUIET: 'quiet',
MINI: 'mini',
MAIN: 'main',
NO_TRAY: 'no-tray'
}
export const II18nLanguage = {
ZH_CN: 'zh-CN',
ZH_TW: 'zh-TW',
EN: 'en'
}
export const IPasteStyle = {
MARKDOWN: 'markdown',
HTML: 'HTML',
URL: 'URL',
UBB: 'UBB',
CUSTOM: 'Custom'
}
export const IWindowList = {
SETTING_WINDOW: 'SETTING_WINDOW',
TRAY_WINDOW: 'TRAY_WINDOW',
MINI_WINDOW: 'MINI_WINDOW',
RENAME_WINDOW: 'RENAME_WINDOW',
TOOLBOX_WINDOW: 'TOOLBOX_WINDOW'
}
export const IRPCActionType = {
// system rpc
RELOAD_APP: 'RELOAD_APP',
OPEN_URL: 'OPEN_URL',
OPEN_FILE: 'OPEN_FILE',
HIDE_DOCK: 'HIDE_DOCK',
SET_CURRENT_LANGUAGE: 'SET_CURRENT_LANGUAGE',
OPEN_WINDOW: 'OPEN_WINDOW',
OPEN_MINI_WINDOW: 'OPEN_MINI_WINDOW',
CLOSE_WINDOW: 'CLOSE_WINDOW',
MINIMIZE_WINDOW: 'MINIMIZE_WINDOW',
SHOW_MINI_PAGE_MENU: 'SHOW_MINI_PAGE_MENU',
SHOW_MAIN_PAGE_MENU: 'SHOW_MAIN_PAGE_MENU',
SHOW_UPLOAD_PAGE_MENU: 'SHOW_UPLOAD_PAGE_MENU',
SHOW_SECOND_UPLOADER_MENU: 'SHOW_SECOND_UPLOADER_MENU',
SHOW_PLUGIN_PAGE_MENU: 'SHOW_PLUGIN_PAGE_MENU',
SET_MINI_WINDOW_POS: 'SET_MINI_WINDOW_POS',
MINI_WINDOW_ON_TOP: 'MINI_WINDOW_ON_TOP',
MAIN_WINDOW_ON_TOP: 'MAIN_WINDOW_ON_TOP',
UPDATE_MINI_WINDOW_ICON: 'UPDATE_MINI_WINDOW_ICON',
REFRESH_SETTING_WINDOW: 'REFRESH_SETTING_WINDOW',
// picbed RPC
PICBED_GET_PICBED_CONFIG: 'PICBED_GET_PICBED_CONFIG',
PICBED_GET_CONFIG_LIST: 'PICBED_GET_CONFIG_LIST',
PICBED_DELETE_CONFIG: 'PICBED_DELETE_CONFIG',
UPLOADER_CHANGE_CURRENT: 'UPLOADER_CHANGE_CURRENT',
UPLOADER_SELECT: 'UPLOADER_SELECT',
UPLOADER_UPDATE_CONFIG: 'UPLOADER_UPDATE_CONFIG',
UPLOADER_RESET_CONFIG: 'UPLOADER_RESET_CONFIG',
DELETE_ALL_API: 'DELETE_ALL_API',
// toolbox rpc
TOOLBOX_CHECK: 'TOOLBOX_CHECK',
TOOLBOX_CHECK_RES: 'TOOLBOX_CHECK_RES',
TOOLBOX_CHECK_FIX: 'TOOLBOX_CHECK_FIX',
// main app setting rpc
PICLIST_GET_CONFIG: 'PICLIST_GET_CONFIG',
PICLIST_GET_CONFIG_SYNC: 'PICLIST_GET_CONFIG_SYNC',
PICLIST_SAVE_CONFIG: 'PICLIST_SAVE_CONFIG',
PICLIST_OPEN_FILE: 'PICLIST_OPEN_FILE',
PICLIST_OPEN_DIRECTORY: 'PICLIST_OPEN_DIRECTORY',
PICLIST_AUTO_START: 'PICLIST_AUTO_START',
// shortkey setting rpc
SHORTKEY_UPDATE: 'SHORTKEY_UPDATE',
SHORTKEY_BIND_OR_UNBIND: 'SHORTKEY_BIND_OR_UNBIND',
SHORTKEY_TOGGLE_SHORTKEY_MODIFIED_MODE: 'SHORTKEY_TOGGLE_SHORTKEY_MODIFIED_MODE',
// configuration setting rpc
CONFIGURE_MIGRATE_FROM_PICGO: 'CONFIGURE_MIGRATE_FROM_PICGO',
CONFIGURE_UPLOAD_COMMON_CONFIG: 'CONFIGURE_UPLOAD_COMMON_CONFIG',
CONFIGURE_UPLOAD_MANAGE_CONFIG: 'CONFIGURE_UPLOAD_MANAGE_CONFIG',
CONFIGURE_UPLOAD_ALL_CONFIG: 'CONFIGURE_UPLOAD_ALL_CONFIG',
CONFIGURE_DOWNLOAD_COMMON_CONFIG: 'CONFIGURE_DOWNLOAD_COMMON_CONFIG',
CONFIGURE_DOWNLOAD_MANAGE_CONFIG: 'CONFIGURE_DOWNLOAD_MANAGE_CONFIG',
CONFIGURE_DOWNLOAD_ALL_CONFIG: 'CONFIGURE_DOWNLOAD_ALL_CONFIG',
// advanced setting rpc
ADVANCED_UPDATE_SERVER: 'ADVANCED_UPDATE_SERVER',
ADVANCED_STOP_WEB_SERVER: 'ADVANCED_STOP_WEB_SERVER',
ADVANCED_RESTART_WEB_SERVER: 'ADVANCED_RESTART_WEB_SERVER',
// upload and main page rpc
MAIN_GET_PICBED: 'MAIN_GET_PICBED',
UPLOAD_CLIPBOARD_FILES_FROM_UPLOAD_PAGE: 'UPLOAD_CLIPBOARD_FILES_FROM_UPLOAD_PAGE',
UPLOAD_CHOOSED_FILES: 'UPLOAD_CHOOSED_FILES',
// gallery rpc
GALLERY_PASTE_TEXT: 'GALLERY_PASTE_TEXT',
GALLERY_REMOVE_FILES: 'GALLERY_REMOVE_FILES',
GALLERY_GET_DB: 'GALLERY_GET_DB',
GALLERY_GET_BY_ID_DB: 'GALLERY_GET_BY_ID_DB',
GALLERY_UPDATE_BY_ID_DB: 'GALLERY_UPDATE_BY_ID_DB',
GALLERY_REMOVE_BY_ID_DB: 'GALLERY_REMOVE_BY_ID_DB',
GALLERY_INSERT_DB: 'GALLERY_INSERT_DB',
GALLERY_INSERT_DB_BATCH: 'GALLERY_INSERT_DB_BATCH',
// plugin rpc
PLUGIN_GET_LIST: 'PLUGIN_GET_LIST',
PLUGIN_INSTALL: 'PLUGIN_INSTALL',
PLUGIN_IMPORT_LOCAL: 'PLUGIN_IMPORT_LOCAL',
PLUGIN_UPDATE_ALL: 'PLUGIN_UPDATE_ALL',
// tray rpc
TRAY_SET_TOOL_TIP: 'TRAY_SET_TOOL_TIP',
TRAY_GET_SHORT_URL: 'TRAY_GET_SHORT_URL',
TRAY_UPLOAD_CLIPBOARD_FILES: 'TRAY_UPLOAD_CLIPBOARD_FILES',
// manage rpc
MANAGE_GET_CONFIG: 'MANAGE_GET_CONFIG',
MANAGE_SAVE_CONFIG: 'MANAGE_SAVE_CONFIG',
MANAGE_REMOVE_CONFIG: 'MANAGE_REMOVE_CONFIG',
MANAGE_GET_BUCKET_LIST: 'MANAGE_GET_BUCKET_LIST',
MANAGE_GET_BUCKET_LIST_BACKSTAGE: 'MANAGE_GET_BUCKET_LIST_BACKSTAGE',
MANAGE_GET_BUCKET_LIST_RECURSIVELY: 'MANAGE_GET_BUCKET_LIST_RECURSIVELY',
MANAGE_CREATE_BUCKET: 'MANAGE_CREATE_BUCKET',
MANAGE_GET_BUCKET_FILE_LIST: 'MANAGE_GET_BUCKET_FILE_LIST',
MANAGE_GET_BUCKET_DOMAIN: 'MANAGE_GET_BUCKET_DOMAIN',
MANAGE_SET_BUCKET_ACL_POLICY: 'MANAGE_SET_BUCKET_ACL_POLICY',
MANAGE_RENAME_BUCKET_FILE: 'MANAGE_RENAME_BUCKET_FILE',
MANAGE_DELETE_BUCKET_FILE: 'MANAGE_DELETE_BUCKET_FILE',
MANAGE_DELETE_BUCKET_FOLDER: 'MANAGE_DELETE_BUCKET_FOLDER',
MANAGE_GET_PRE_SIGNED_URL: 'MANAGE_GET_PRE_SIGNED_URL',
MANAGE_UPLOAD_BUCKET_FILE: 'MANAGE_UPLOAD_BUCKET_FILE',
MANAGE_DOWNLOAD_BUCKET_FILE: 'MANAGE_DOWNLOAD_BUCKET_FILE',
MANAGE_CREATE_BUCKET_FOLDER: 'MANAGE_CREATE_BUCKET_FOLDER',
MANAGE_OPEN_FILE_SELECT_DIALOG: 'MANAGE_OPEN_FILE_SELECT_DIALOG',
MANAGE_GET_UPLOAD_TASK_LIST: 'MANAGE_GET_UPLOAD_TASK_LIST',
MANAGE_GET_DOWNLOAD_TASK_LIST: 'MANAGE_GET_DOWNLOAD_TASK_LIST',
MANAGE_DELETE_UPLOADED_TASK: 'MANAGE_DELETE_UPLOADED_TASK',
MANAGE_DELETE_ALL_UPLOADED_TASK: 'MANAGE_DELETE_ALL_UPLOADED_TASK',
MANAGE_DELETE_DOWNLOADED_TASK: 'MANAGE_DELETE_DOWNLOADED_TASK',
MANAGE_DELETE_ALL_DOWNLOADED_TASK: 'MANAGE_DELETE_ALL_DOWNLOADED_TASK',
MANAGE_SELECT_DOWNLOAD_FOLDER: 'MANAGE_SELECT_DOWNLOAD_FOLDER',
MANAGE_GET_DEFAULT_DOWNLOAD_FOLDER: 'MANAGE_GET_DEFAULT_DOWNLOAD_FOLDER',
MANAGE_OPEN_DOWNLOADED_FOLDER: 'MANAGE_OPEN_DOWNLOADED_FOLDER',
MANAGE_OPEN_LOCAL_FILE: 'MANAGE_OPEN_LOCAL_FILE',
MANAGE_DOWNLOAD_FILE_FROM_URL: 'MANAGE_DOWNLOAD_FILE_FROM_URL',
MANAGE_CONVERT_PATH_TO_BASE64: 'MANAGE_CONVERT_PATH_TO_BASE64'
}
export const IToolboxItemType = {
IS_CONFIG_FILE_BROKEN: 'IS_CONFIG_FILE_BROKEN',
IS_GALLERY_FILE_BROKEN: 'IS_GALLERY_FILE_BROKEN',
HAS_PROBLEM_WITH_CLIPBOARD_PIC_UPLOAD: 'HAS_PROBLEM_WITH_CLIPBOARD_PIC_UPLOAD',
HAS_PROBLEM_WITH_PROXY: 'HAS_PROBLEM_WITH_PROXY'
}
export const IToolboxItemCheckStatus = {
INIT: 'init',
LOADING: 'loading',
SUCCESS: 'success',
ERROR: 'error'
}
export const ISartMode = {
QUIET: 'quiet',
MINI: 'mini',
MAIN: 'main',
NO_TRAY: 'no-tray'
}
export const II18nLanguage = {
ZH_CN: 'zh-CN',
ZH_TW: 'zh-TW',
EN: 'en'
}

View File

@@ -1,19 +1,19 @@
import { ref } from 'vue'
import { IRPCActionType } from '@/utils/enum'
import type { IPicBedType } from '#/types/types'
const osGlobal = ref<string>(window.electron.platform)
const picBedGlobal = ref<IPicBedType[]>([])
const pageReloadCount = ref(0)
async function updatePicBedGlobal () {
picBedGlobal.value = (await window.electron.triggerRPC<IPicBedType[]>(IRPCActionType.MAIN_GET_PICBED))!
}
async function updatePageReloadCount () {
pageReloadCount.value++
}
export { osGlobal, pageReloadCount, picBedGlobal, updatePageReloadCount, updatePicBedGlobal }
import { ref } from 'vue'
import { IRPCActionType } from '@/utils/enum'
import type { IPicBedType } from '#/types/types'
const osGlobal = ref<string>(window.electron.platform)
const picBedGlobal = ref<IPicBedType[]>([])
const pageReloadCount = ref(0)
async function updatePicBedGlobal() {
picBedGlobal.value = (await window.electron.triggerRPC<IPicBedType[]>(IRPCActionType.MAIN_GET_PICBED))!
}
async function updatePageReloadCount() {
pageReloadCount.value++
}
export { osGlobal, pageReloadCount, picBedGlobal, updatePageReloadCount, updatePicBedGlobal }

View File

@@ -1,71 +1,71 @@
import type { IStringKeyMap } from '#/types/types'
export const RELEASE_URL = 'https://api.github.com/repos/Kuingsmile/PicList/releases'
export const RELEASE_URL_BACKUP = 'https://release.piclist.cn'
export const cancelDownloadLoadingFileList = 'cancelDownloadLoadingFileList'
export const refreshDownloadFileTransferList = 'refreshDownloadFileTransferList'
export const picBedsCanbeDeleted = [
'aliyun',
'alist',
'alistplist',
'aws-s3',
'aws-s3-plist',
'dogecloud',
'github',
'huaweicloud-uploader',
'imgur',
'local',
'lskyplist',
'piclist',
'qiniu',
'sftpplist',
'smms',
'tcyun',
'upyun',
'webdavplist'
]
export const picBedManualUrlList: IStringKeyMap = {
zh_cn: {
advancedpiclist: 'https://piclist.cn/configure.html#%E9%AB%98%E7%BA%A7%E8%87%AA%E5%AE%9A%E4%B9%89',
aliyun: 'https://piclist.cn/configure.html#%E9%98%BF%E9%87%8C%E4%BA%91oss',
alistplist: 'https://piclist.cn/configure.html#alist',
'aws-s3': 'https://piclist.cn/configure.html#%E5%86%85%E7%BD%AEaws-s3',
'aws-s3-plist': 'https://piclist.cn/configure.html#%E5%86%85%E7%BD%AEaws-s3',
github: 'https://piclist.cn/configure.html#github%E5%9B%BE%E5%BA%8A',
githubPlus: 'https://piclist.cn/configure.html#github%E5%9B%BE%E5%BA%8A',
imgur: 'https://piclist.cn/configure.html#imgur',
lankong: 'https://github.com/hellodk34/picgo-plugin-lankong',
local: 'https://piclist.cn/configure.html#%E6%9C%AC%E5%9C%B0%E5%9B%BE%E5%BA%8A',
lskyplist: 'https://piclist.cn/configure.html#%E5%85%B0%E7%A9%BA%E5%9B%BE%E5%BA%8A',
tcyun: 'https://piclist.cn/configure.html#%E8%85%BE%E8%AE%AF%E4%BA%91cos',
piclist: 'https://piclist.cn/configure.html#piclist',
qiniu: 'https://piclist.cn/configure.html#%E4%B8%83%E7%89%9B%E4%BA%91',
sftpplist: 'https://piclist.cn/configure.html#%E5%86%85%E7%BD%AEsftp',
smms: 'https://piclist.cn/configure.html#sm-ms',
upyun: 'https://piclist.cn/configure.html#%E5%8F%88%E6%8B%8D%E4%BA%91',
webdavplist: 'https://piclist.cn/configure.html#webdav'
},
en: {
advancedpiclist: 'https://piclist.cn/en/configure.html#advanced',
aliyun: 'https://piclist.cn/en/configure.html#alibaba-cloud',
alistplist: 'https://piclist.cn/en/configure.html#alist',
'aws-s3': 'https://piclist.cn/en/configure.html#built-in-aws-s3',
'aws-s3-plist': 'https://piclist.cn/en/configure.html#built-in-aws-s3',
github: 'https://piclist.cn/en/configure.html#github',
githubPlus: 'https://piclist.cn/en/configure.html#github',
imgur: 'https://piclist.cn/en/configure.html#imgur',
lankong: 'https://github.com/hellodk34/picgo-plugin-lankong',
local: 'https://piclist.cn/en/configure.html#local-image-hosting',
lskyplist: 'https://piclist.cn/en/configure.html#lsky-pro',
tcyun: 'https://piclist.cn/en/configure.html#tencent-cloud-cos',
piclist: 'https://piclist.cn/en/configure.html#piclist',
qiniu: 'https://piclist.cn/en/configure.html#qiniu-cloud',
sftpplist: 'https://piclist.cn/en/configure.html#built-in-sftp',
smms: 'https://piclist.cn/en/configure.html#sm-ms',
upyun: 'https://piclist.cn/en/configure.html#upyun',
webdavplist: 'https://piclist.cn/en/configure.html#webdav'
}
}
import type { IStringKeyMap } from '#/types/types'
export const RELEASE_URL = 'https://api.github.com/repos/Kuingsmile/PicList/releases'
export const RELEASE_URL_BACKUP = 'https://release.piclist.cn'
export const cancelDownloadLoadingFileList = 'cancelDownloadLoadingFileList'
export const refreshDownloadFileTransferList = 'refreshDownloadFileTransferList'
export const picBedsCanbeDeleted = [
'aliyun',
'alist',
'alistplist',
'aws-s3',
'aws-s3-plist',
'dogecloud',
'github',
'huaweicloud-uploader',
'imgur',
'local',
'lskyplist',
'piclist',
'qiniu',
'sftpplist',
'smms',
'tcyun',
'upyun',
'webdavplist'
]
export const picBedManualUrlList: IStringKeyMap = {
zh_cn: {
advancedpiclist: 'https://piclist.cn/configure.html#%E9%AB%98%E7%BA%A7%E8%87%AA%E5%AE%9A%E4%B9%89',
aliyun: 'https://piclist.cn/configure.html#%E9%98%BF%E9%87%8C%E4%BA%91oss',
alistplist: 'https://piclist.cn/configure.html#alist',
'aws-s3': 'https://piclist.cn/configure.html#%E5%86%85%E7%BD%AEaws-s3',
'aws-s3-plist': 'https://piclist.cn/configure.html#%E5%86%85%E7%BD%AEaws-s3',
github: 'https://piclist.cn/configure.html#github%E5%9B%BE%E5%BA%8A',
githubPlus: 'https://piclist.cn/configure.html#github%E5%9B%BE%E5%BA%8A',
imgur: 'https://piclist.cn/configure.html#imgur',
lankong: 'https://github.com/hellodk34/picgo-plugin-lankong',
local: 'https://piclist.cn/configure.html#%E6%9C%AC%E5%9C%B0%E5%9B%BE%E5%BA%8A',
lskyplist: 'https://piclist.cn/configure.html#%E5%85%B0%E7%A9%BA%E5%9B%BE%E5%BA%8A',
tcyun: 'https://piclist.cn/configure.html#%E8%85%BE%E8%AE%AF%E4%BA%91cos',
piclist: 'https://piclist.cn/configure.html#piclist',
qiniu: 'https://piclist.cn/configure.html#%E4%B8%83%E7%89%9B%E4%BA%91',
sftpplist: 'https://piclist.cn/configure.html#%E5%86%85%E7%BD%AEsftp',
smms: 'https://piclist.cn/configure.html#sm-ms',
upyun: 'https://piclist.cn/configure.html#%E5%8F%88%E6%8B%8D%E4%BA%91',
webdavplist: 'https://piclist.cn/configure.html#webdav'
},
en: {
advancedpiclist: 'https://piclist.cn/en/configure.html#advanced',
aliyun: 'https://piclist.cn/en/configure.html#alibaba-cloud',
alistplist: 'https://piclist.cn/en/configure.html#alist',
'aws-s3': 'https://piclist.cn/en/configure.html#built-in-aws-s3',
'aws-s3-plist': 'https://piclist.cn/en/configure.html#built-in-aws-s3',
github: 'https://piclist.cn/en/configure.html#github',
githubPlus: 'https://piclist.cn/en/configure.html#github',
imgur: 'https://piclist.cn/en/configure.html#imgur',
lankong: 'https://github.com/hellodk34/picgo-plugin-lankong',
local: 'https://piclist.cn/en/configure.html#local-image-hosting',
lskyplist: 'https://piclist.cn/en/configure.html#lsky-pro',
tcyun: 'https://piclist.cn/en/configure.html#tencent-cloud-cos',
piclist: 'https://piclist.cn/en/configure.html#piclist',
qiniu: 'https://piclist.cn/en/configure.html#qiniu-cloud',
sftpplist: 'https://piclist.cn/en/configure.html#built-in-sftp',
smms: 'https://piclist.cn/en/configure.html#sm-ms',
upyun: 'https://piclist.cn/en/configure.html#upyun',
webdavplist: 'https://piclist.cn/en/configure.html#webdav'
}
}