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