mirror of
https://github.com/Kuingsmile/PicList.git
synced 2026-05-06 20:42:57 +08:00
🚧 WIP(custom): optimzie setting page and add several custom components
This commit is contained in:
@@ -8,7 +8,8 @@
|
||||
|
||||
- windows下新增便携模式,无需安装即可运行,数据存储在程序目录下的`data`文件夹中,支持自动更新;Linux下新增`rpm`安装包
|
||||
- 新增自定义主题功能,主题仓库[PicList ThemeHub](https://github.com/Kuingsmile/PicList-ThemeHub)
|
||||
- 12个内置主题供选择,如bilibili、二次元、极夜紫、gemini等风格
|
||||
- 12个内置主题供选择,如bilibili、二次元、极夜紫等风格
|
||||
- 重构了几乎全部页面,优化了数十项UI细节问题
|
||||
- 相册页面多项优化,支持显示已选择图片数量,匹配的url列表和记忆过滤器打开状态
|
||||
- 插件页面现在可以浏览所有插件列表,查看详情和安装
|
||||
- 新增教学引导页面,首次运行时会自动弹出
|
||||
|
||||
@@ -8,7 +8,8 @@
|
||||
|
||||
- Added portable mode on Windows, allowing the program to run without installation. Data is stored in the `data` folder within the program directory, and automatic updates are supported. Added `rpm` installation package for Linux
|
||||
- Added custom theme functionality, with a theme repository available at [PicList ThemeHub](https://github.com/Kuingsmile/PicList-ThemeHub)
|
||||
- 12 built-in themes available, such as bilibili, ACG, Night Purple, gemini styles
|
||||
- 12 built-in themes available, such as bilibili, ACG, Night Purple styles
|
||||
- Refactored almost all pages, optimizing dozens of UI detail issues
|
||||
- Multiple optimizations on the album page, supporting display of the number of selected images, matching URL list, and remembering filter open state
|
||||
- Plugin page now allows browsing of all plugin lists, viewing details, and installation
|
||||
- Added tutorial guide page, which automatically pops up on first run
|
||||
|
||||
69
src/renderer/components/common/customButton.vue
Normal file
69
src/renderer/components/common/customButton.vue
Normal file
@@ -0,0 +1,69 @@
|
||||
<template>
|
||||
<button
|
||||
:disabled="disabled"
|
||||
class="flex min-w-fit cursor-pointer items-center justify-center gap-2 rounded-md px-4 py-2 text-sm font-medium transition-all duration-fast ease-apple not-disabled:hover:shadow-sm disabled:cursor-not-allowed disabled:opacity-50"
|
||||
:class="classVar"
|
||||
:data-active="active"
|
||||
@click="emit('click')"
|
||||
>
|
||||
<slot name="icon">
|
||||
<component :is="icon" v-if="icon" :size="iconSize" />
|
||||
</slot>
|
||||
<slot>
|
||||
<span
|
||||
:class="textClassVar"
|
||||
:data-active="active"
|
||||
class="[.primary] text-sm leading-[1.4] font-semibold text-secondary"
|
||||
>{{ text }}</span
|
||||
>
|
||||
</slot>
|
||||
<slot name="extra"> </slot>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
|
||||
const {
|
||||
text,
|
||||
disabled,
|
||||
active = false,
|
||||
icon,
|
||||
iconSize = 16,
|
||||
type = 'primary',
|
||||
} = defineProps<{
|
||||
text: string
|
||||
icon: any
|
||||
active?: boolean
|
||||
iconSize?: number
|
||||
disabled?: boolean
|
||||
type?: 'primary' | 'secondary' | 'tab'
|
||||
}>()
|
||||
|
||||
const textClassVar = computed(() => {
|
||||
switch (type) {
|
||||
case 'primary':
|
||||
return 'text-white'
|
||||
case 'secondary':
|
||||
return 'text-main'
|
||||
case 'tab':
|
||||
return active ? 'text-white' : 'text-secondary'
|
||||
default:
|
||||
return ''
|
||||
}
|
||||
})
|
||||
|
||||
const classVar = computed(() => {
|
||||
switch (type) {
|
||||
case 'primary':
|
||||
return 'bg-accent text-white not-disabled:hover:bg-accent-hover! not-disabled:hover:-translate-y-px'
|
||||
case 'secondary':
|
||||
return 'border border-border! bg-bg-secondary! text-main! not-disabled:hover:bg-surface-elevated! not-disabled:hover:-translate-y-px'
|
||||
case 'tab':
|
||||
return 'flex-1 text-secondary not-disabled:data-[active=false]:hover:bg-accent/30! data-[active=true]:text-white data-[active=true]:bg-accent!'
|
||||
default:
|
||||
return ''
|
||||
}
|
||||
})
|
||||
const emit = defineEmits<(e: 'click') => void>()
|
||||
</script>
|
||||
45
src/renderer/components/common/customInput.vue
Normal file
45
src/renderer/components/common/customInput.vue
Normal file
@@ -0,0 +1,45 @@
|
||||
<template>
|
||||
<div class="flex flex-col">
|
||||
<label class="mb-2 text-sm font-semibold text-secondary">{{ title }}</label>
|
||||
|
||||
<div class="relative w-full">
|
||||
<input
|
||||
v-model="modelValue"
|
||||
:type="type"
|
||||
class="box-border w-full rounded-md border border-border bg-bg-tertiary p-3 pr-10 text-sm text-main transition-all duration-200 ease-apple focus:border-accent focus:outline-none"
|
||||
:placeholder="placeholder"
|
||||
/>
|
||||
|
||||
<button
|
||||
v-if="isPassword"
|
||||
type="button"
|
||||
class="absolute top-1/2 right-3 flex -translate-y-1/2 items-center justify-center text-main"
|
||||
@click="type = type === 'password' ? 'text' : 'password'"
|
||||
>
|
||||
<EyeIcon v-if="type === 'password'" class="text-accent" :size="16" />
|
||||
<EyeClosedIcon v-else class="text-accent" :size="16" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { EyeClosedIcon, EyeIcon } from 'lucide-vue-next'
|
||||
import { onMounted, ref } from 'vue'
|
||||
const modelValue = defineModel<string>()
|
||||
const type = ref('text')
|
||||
const {
|
||||
isPassword = false,
|
||||
title,
|
||||
placeholder,
|
||||
} = defineProps<{
|
||||
isPassword?: boolean
|
||||
title: string
|
||||
placeholder: string
|
||||
}>()
|
||||
onMounted(() => {
|
||||
if (isPassword) {
|
||||
type.value = 'password'
|
||||
}
|
||||
})
|
||||
</script>
|
||||
36
src/renderer/components/common/customNavCard.vue
Normal file
36
src/renderer/components/common/customNavCard.vue
Normal file
@@ -0,0 +1,36 @@
|
||||
<template>
|
||||
<div
|
||||
class="flex cursor-pointer items-center gap-4 rounded-lg border border-border bg-bg-secondary p-4 transition-all duration-200 ease-apple hover:-translate-y-px hover:border-accent hover:shadow-md"
|
||||
@click="emit('click')"
|
||||
>
|
||||
<div class="flex h-[25px] w-[25px] shrink-0 items-center justify-center rounded-lg bg-accent text-white">
|
||||
<component :is="icon" :size="15" />
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<h4 class="text-[0.925rem] leading-[1.4] font-semibold text-secondary">{{ title }}</h4>
|
||||
<slot name="description">
|
||||
<p v-if="description" class="mt-1 text-xs font-medium text-secondary">{{ description }}</p>
|
||||
</slot>
|
||||
</div>
|
||||
<div v-if="!noarrow" class="text-main transition-all duration-200 ease-apple hover:translate-x-1 hover:text-accent">
|
||||
<ChevronRightIcon :size="16" />
|
||||
</div>
|
||||
<slot name="extra"></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ChevronRightIcon } from 'lucide-vue-next'
|
||||
const {
|
||||
title,
|
||||
icon,
|
||||
description = '',
|
||||
noarrow = false,
|
||||
} = defineProps<{
|
||||
icon: any
|
||||
title: string
|
||||
description?: string
|
||||
noarrow?: boolean
|
||||
}>()
|
||||
const emit = defineEmits<(e: 'click') => void>()
|
||||
</script>
|
||||
35
src/renderer/components/common/customSelect.vue
Normal file
35
src/renderer/components/common/customSelect.vue
Normal file
@@ -0,0 +1,35 @@
|
||||
<template>
|
||||
<div class="mb-3 flex items-center gap-2 text-sm font-medium text-main">
|
||||
<slot name="icon">
|
||||
<component :is="icon" v-if="icon" :size="iconSize" class="text-accent" />
|
||||
</slot>
|
||||
<span class="text-[0.925rem] leading-[1.4] font-semibold text-secondary">{{ title }}</span>
|
||||
</div>
|
||||
<select
|
||||
v-model="modelValue"
|
||||
class="border-box w-full rounded-md border border-border bg-bg-tertiary p-3 text-sm text-main transition-all duration-200 ease-apple focus:border-accent focus:outline-none"
|
||||
>
|
||||
<template v-if="selectList.length > 0">
|
||||
<option v-for="item in selectList" :key="item.value" :value="item.value">
|
||||
{{ item.label }}
|
||||
</option>
|
||||
</template>
|
||||
<slot name="extra"></slot>
|
||||
</select>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const modelValue = defineModel<string>()
|
||||
|
||||
const {
|
||||
title,
|
||||
icon,
|
||||
iconSize = 18,
|
||||
selectList = [],
|
||||
} = defineProps<{
|
||||
title: string
|
||||
icon: any
|
||||
selectList?: { value: string; label: string }[]
|
||||
iconSize?: number
|
||||
}>()
|
||||
</script>
|
||||
@@ -1,22 +1,35 @@
|
||||
<template>
|
||||
<label
|
||||
class="flex cursor-pointer items-center gap-4 rounded-lg border border-border bg-bg p-4 transition-all duration-200 ease-apple hover:border-accent hover:bg-surface hover:shadow-sm"
|
||||
class="flex cursor-pointer items-center gap-4 rounded-lg border border-border p-4 transition-all duration-200 ease-apple hover:border-accent hover:bg-surface hover:shadow-sm"
|
||||
:class="noBorder ? 'border-none' : ''"
|
||||
>
|
||||
<input v-model="modelValue" type="checkbox" class="peer hidden" />
|
||||
<span
|
||||
class="bg-linear-180-r relative h-[28px] w-[52px] shrink-0 rounded-full bg-gray-400/80 shadow-sm transition-all duration-medium ease-standard peer-checked:bg-accent before:absolute before:top-[3px] before:left-[3px] before:h-[22px] before:w-[22px] before:rounded-full before:bg-white before:shadow-sm before:transition-all before:duration-200 before:ease-apple before:content-[''] peer-checked:before:translate-x-[24px]"
|
||||
class="bg-linear-180-r relative shrink-0 rounded-full bg-gray-400/80 shadow-sm transition-all duration-medium ease-standard peer-checked:bg-accent before:absolute before:rounded-full before:bg-white before:shadow-sm before:transition-all before:duration-200 before:ease-apple before:content-[''] peer-checked:before:translate-x-[24px]"
|
||||
:class="
|
||||
small
|
||||
? 'h-[21px] w-[44px] before:top-[2px] before:left-[2px] before:h-[17px] before:w-[17px]'
|
||||
: 'h-[28px] w-[52px] before:top-[3px] before:left-[3px] before:h-[22px] before:w-[22px]'
|
||||
"
|
||||
/>
|
||||
<div class="flex flex-1 flex-col gap-1">
|
||||
<span class="text-[0.925rem] leading-[1.4] font-semibold text-secondary">{{ props.title }}</span>
|
||||
<span class="text-xs text-secondary/90">{{ props.description }}</span>
|
||||
<span class="text-[0.925rem] leading-[1.4] font-semibold text-secondary">{{ title }}</span>
|
||||
<span class="text-xs text-secondary/90">{{ description }}</span>
|
||||
</div>
|
||||
</label>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
const modelValue = defineModel<boolean>()
|
||||
const props = defineProps<{
|
||||
const {
|
||||
title = '',
|
||||
description = '',
|
||||
noBorder = false,
|
||||
small = false,
|
||||
} = defineProps<{
|
||||
noBorder?: boolean
|
||||
title?: string
|
||||
description?: string
|
||||
small?: boolean
|
||||
}>()
|
||||
</script>
|
||||
|
||||
84
src/renderer/components/common/multiSelect.vue
Normal file
84
src/renderer/components/common/multiSelect.vue
Normal file
@@ -0,0 +1,84 @@
|
||||
<template>
|
||||
<div :class="tight ? 'mb-0' : 'mb-3'" class="flex items-center gap-2 text-sm font-medium text-main">
|
||||
<slot name="icon">
|
||||
<component :is="icon" v-if="icon" :size="iconSize" class="text-accent" />
|
||||
</slot>
|
||||
<span class="text-[0.925rem] leading-[1.4] font-semibold text-secondary">{{ title }}</span>
|
||||
</div>
|
||||
<div ref="dropdownRef" class="custom-multiselect relative">
|
||||
<button
|
||||
class="flex h-[28px] w-full cursor-pointer items-center justify-between rounded-md border border-border-secondary px-2 py-1.5 text-sm leading-[1.4] text-main transition-all duration-fast ease-apple hover:border-accent-hover focus:[.active]:border-accent-hover focus:[.active]:shadow-sm"
|
||||
:class="{ active: dropDownOpen }"
|
||||
@click="toggleDropdown($event)"
|
||||
>
|
||||
<span v-if="choosed?.length === 0" class="text-center text-xs font-semibold text-secondary">{{
|
||||
zeroPlaceholder
|
||||
}}</span>
|
||||
<span v-else class="text-center text-xs font-semibold text-secondary"
|
||||
>{{ choosed?.length }} {{ t('pages.gallery.selected') }}</span
|
||||
>
|
||||
<ChevronDownIcon :size="16" />
|
||||
</button>
|
||||
<div
|
||||
v-show="dropDownOpen"
|
||||
class="multiselect-dropdown shadow-lg; fixed z-1000 mt-[2px] no-scrollbar max-h-[280px] min-w-[185px] overflow-y-auto rounded-md border border-border-secondary bg-bg-tertiary px-2 py-1.5 text-main"
|
||||
>
|
||||
<label
|
||||
v-for="item in allList"
|
||||
:key="item.type"
|
||||
class="flex min-h-[unset] cursor-pointer items-center justify-between px-2 py-1 text-sm leading-[1.4] transition-all duration-fast ease-apple hover:bg-accent/50"
|
||||
>
|
||||
<input v-model="choosed" type="checkbox" :value="item.type" class="m-0" />
|
||||
{{ item.name }}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onClickOutside } from '@vueuse/core'
|
||||
import { ChevronDownIcon } from 'lucide-vue-next'
|
||||
import { nextTick, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const choosed = defineModel<string[]>('choosed')
|
||||
const { t } = useI18n()
|
||||
const dropdownRef = ref(null)
|
||||
|
||||
const dropDownOpen = ref(false)
|
||||
function toggleDropdown(event?: Event) {
|
||||
dropDownOpen.value = !dropDownOpen.value
|
||||
if (dropDownOpen.value && event) {
|
||||
nextTick(() => {
|
||||
const trigger = event.target as HTMLElement
|
||||
const dropdown = trigger.parentElement?.querySelector('.multiselect-dropdown') as HTMLElement
|
||||
if (dropdown && trigger) {
|
||||
const rect = trigger.getBoundingClientRect()
|
||||
dropdown.style.top = `${rect.bottom + 2}px`
|
||||
dropdown.style.left = `${rect.left}px`
|
||||
dropdown.style.width = `${Math.max(rect.width, 200)}px`
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
onClickOutside(dropdownRef, () => {
|
||||
dropDownOpen.value = false
|
||||
})
|
||||
|
||||
const {
|
||||
tight = true,
|
||||
title,
|
||||
icon = null,
|
||||
iconSize = 18,
|
||||
zeroPlaceholder,
|
||||
allList,
|
||||
} = defineProps<{
|
||||
tight?: boolean
|
||||
title: string
|
||||
icon?: any
|
||||
iconSize?: number
|
||||
zeroPlaceholder: string
|
||||
allList: any
|
||||
}>()
|
||||
</script>
|
||||
@@ -5,7 +5,7 @@
|
||||
<div
|
||||
class="bg-linear-150-r m-0 border-b border-border bg-accent/10 px-4 pt-3.5 pb-2 text-sm font-semibold tracking-wide text-secondary"
|
||||
>
|
||||
{{ $t(titleList[key]) }}
|
||||
{{ titleList[key] }}
|
||||
</div>
|
||||
<div class="grid grid-cols-[repeat(auto-fill,minmax(240px,1fr))] gap-0 py-2">
|
||||
<div
|
||||
|
||||
11
src/renderer/components/common/settingCard.vue
Normal file
11
src/renderer/components/common/settingCard.vue
Normal file
@@ -0,0 +1,11 @@
|
||||
<template>
|
||||
<div class="relative rounded-lg border border-border bg-bg-secondary shadow-sm" :class="p1 ? 'p-1' : 'p-4'">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const { p1 = false } = defineProps<{
|
||||
p1?: boolean
|
||||
}>()
|
||||
</script>
|
||||
42
src/renderer/components/common/settingSection.vue
Normal file
42
src/renderer/components/common/settingSection.vue
Normal file
@@ -0,0 +1,42 @@
|
||||
<template>
|
||||
<div class="w-full rounded-lg border border-border bg-bg-secondary p-6 shadow-sm">
|
||||
<div class="mb-2 flex items-start gap-3">
|
||||
<div class="mb-2 flex h-[30px] w-[30px] shrink-0 items-center justify-center rounded-lg bg-accent text-white">
|
||||
<slot name="icon">
|
||||
<component :is="icon" v-if="icon" :size="iconSize" />
|
||||
</slot>
|
||||
</div>
|
||||
<div>
|
||||
<slot name="title"
|
||||
><h2 class="mb-2 text-lg font-semibold text-main">{{ title }}</h2></slot
|
||||
>
|
||||
<slot name="description">
|
||||
<p v-if="description !== ''" class="mb-6 text-sm text-secondary">
|
||||
{{ description }}
|
||||
</p>
|
||||
</slot>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div :class="onlyOneRow ? 'grid grid-cols-1 gap-4' : 'grid grid-cols-2 gap-4 max-md:grid-cols-1'">
|
||||
<slot />
|
||||
</div>
|
||||
<slot name="extra"></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const {
|
||||
title,
|
||||
description = '',
|
||||
icon,
|
||||
iconSize = 20,
|
||||
onlyOneRow = false,
|
||||
} = defineProps<{
|
||||
title: string
|
||||
description?: string
|
||||
icon: any
|
||||
iconSize?: number
|
||||
onlyOneRow?: boolean
|
||||
}>()
|
||||
</script>
|
||||
85
src/renderer/components/common/singleSelect.vue
Normal file
85
src/renderer/components/common/singleSelect.vue
Normal file
@@ -0,0 +1,85 @@
|
||||
<template>
|
||||
<div :class="tight ? 'mb-0' : 'mb-3'" class="flex items-center gap-2 text-sm font-medium text-main">
|
||||
<slot name="icon">
|
||||
<component :is="icon" v-if="icon" :size="iconSize" class="text-accent" />
|
||||
</slot>
|
||||
<span class="text-[0.925rem] leading-[1.4] font-semibold text-secondary">{{ title }}</span>
|
||||
</div>
|
||||
<div ref="dropdownRef" class="sort-dropdown relative">
|
||||
<button
|
||||
class="flex h-[28px] w-full cursor-pointer items-center justify-between gap-1 rounded-md border border-border-secondary px-2 py-1.5 text-sm leading-[1.4] text-main transition-all duration-fast ease-apple hover:border-accent-hover focus:[.active]:border-accent-hover focus:[.active]:shadow-md"
|
||||
:class="{ active: dropDownOpen }"
|
||||
@click="toggleDropdown($event)"
|
||||
>
|
||||
<SortAscIcon v-if="fronticon" :size="14" />
|
||||
<span class="text-center text-xs font-semibold text-secondary">{{ placeholder || modelValue }}</span>
|
||||
<ChevronDownIcon :size="14" />
|
||||
</button>
|
||||
<div
|
||||
v-show="dropDownOpen"
|
||||
class="sort-options fixed z-10 mt-[2px] min-w-[150px] overflow-hidden rounded-md border border-border-secondary bg-bg-tertiary shadow-lg"
|
||||
>
|
||||
<button
|
||||
v-for="key in keyList"
|
||||
:key="key"
|
||||
class="block min-h-[unset] w-full cursor-pointer border-none bg-bg-tertiary px-2 py-1 text-center text-sm leading-[1.4] text-main transition-all duration-fast ease-apple hover:bg-accent/50"
|
||||
@click="selectItem(key)"
|
||||
>
|
||||
<slot name="item" :item="key"> {{ key }} </slot>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { onClickOutside } from '@vueuse/core'
|
||||
import { ChevronDownIcon, SortAscIcon } from 'lucide-vue-next'
|
||||
import { nextTick, ref } from 'vue'
|
||||
|
||||
const dropdownRef = ref(null)
|
||||
const modelValue = defineModel<string>()
|
||||
|
||||
function selectItem(key: string) {
|
||||
modelValue.value = key
|
||||
dropDownOpen.value = false
|
||||
}
|
||||
|
||||
const dropDownOpen = ref(false)
|
||||
function toggleDropdown(event?: Event) {
|
||||
dropDownOpen.value = !dropDownOpen.value
|
||||
|
||||
if (dropDownOpen.value && event) {
|
||||
nextTick(() => {
|
||||
const trigger = event.target as HTMLElement
|
||||
const dropdown = trigger.parentElement?.querySelector('.sort-options') as HTMLElement
|
||||
if (dropdown && trigger) {
|
||||
const rect = trigger.getBoundingClientRect()
|
||||
dropdown.style.top = `${rect.bottom + 2}px`
|
||||
dropdown.style.left = `${rect.left}px`
|
||||
dropdown.style.width = `${Math.max(rect.width, 160)}px`
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
onClickOutside(dropdownRef, () => {
|
||||
dropDownOpen.value = false
|
||||
})
|
||||
|
||||
const {
|
||||
title,
|
||||
placeholder = '',
|
||||
fronticon = true,
|
||||
keyList,
|
||||
icon = null,
|
||||
tight = true,
|
||||
iconSize = 18,
|
||||
} = defineProps<{
|
||||
title: string
|
||||
icon?: any
|
||||
iconSize?: number
|
||||
tight?: boolean
|
||||
placeholder?: string
|
||||
fronticon?: boolean
|
||||
keyList: string[]
|
||||
}>()
|
||||
</script>
|
||||
@@ -914,6 +914,7 @@
|
||||
"chooseSecondPicBedMode": "Choose Second Image Bed Mode",
|
||||
"chooseShowedPicBed": "Please select the image bed to display in the menu",
|
||||
"clipboardAndNotification": "Clipboard and Notification",
|
||||
"controlShow": "Control Showed PicBeds",
|
||||
"copySuccess": "Copy Successful: {content}",
|
||||
"customLinkFormat": "Custom Link Format",
|
||||
"customLinkFormatDesc": "Set custom link output format",
|
||||
@@ -936,7 +937,7 @@
|
||||
"imageProcessing": "Image Processing Settings",
|
||||
"imageProcessingDesc": "Configure image preprocessing options before upload",
|
||||
"isAutoListenClipboard": "Automatically listen for clipboard uploads when the software starts.",
|
||||
"manualRname": "Manual Rename",
|
||||
"manualRename": "Manual Rename",
|
||||
"placeholder": {
|
||||
"categoryFile": "File Related",
|
||||
"categoryHash": "Hash Related",
|
||||
@@ -968,7 +969,7 @@
|
||||
"shortUrlServer": "Short URL Service",
|
||||
"sinkDomain": "Sink Domain",
|
||||
"sinkToken": "Sink Token",
|
||||
"timestampRname": "Timestamp Rename",
|
||||
"timestampRename": "Timestamp Rename",
|
||||
"title": "Upload",
|
||||
"uploadBehavior": "Upload Behavior",
|
||||
"uploadProcessing": "Upload Processing",
|
||||
|
||||
@@ -914,6 +914,7 @@
|
||||
"chooseSecondPicBedMode": "选择第二图床模式",
|
||||
"chooseShowedPicBed": "请选择显示在菜单的图床",
|
||||
"clipboardAndNotification": "剪贴板和通知",
|
||||
"controlShow": "图床显示控制",
|
||||
"copySuccess": "复制成功: {content}",
|
||||
"customLinkFormat": "自定义链接格式",
|
||||
"customLinkFormatDesc": "设置自定义的链接输出格式",
|
||||
@@ -936,7 +937,7 @@
|
||||
"imageProcessing": "图片预处理设置",
|
||||
"imageProcessingDesc": "配置上传前的图片预处理选项",
|
||||
"isAutoListenClipboard": "软件启动时自动监听剪贴板上传",
|
||||
"manualRname": "手动重命名",
|
||||
"manualRename": "手动重命名",
|
||||
"placeholder": {
|
||||
"categoryFile": "文件相关",
|
||||
"categoryHash": "哈希相关",
|
||||
@@ -968,7 +969,7 @@
|
||||
"shortUrlServer": "短链接服务",
|
||||
"sinkDomain": "Sink 域名",
|
||||
"sinkToken": "Sink Token",
|
||||
"timestampRname": "时间戳重命名",
|
||||
"timestampRename": "时间戳重命名",
|
||||
"title": "上传",
|
||||
"uploadBehavior": "上传行为",
|
||||
"uploadProcessing": "上传处理",
|
||||
|
||||
@@ -914,6 +914,7 @@
|
||||
"chooseSecondPicBedMode": "選擇第二圖床模式",
|
||||
"chooseShowedPicBed": "請選擇顯示在菜單的圖床",
|
||||
"clipboardAndNotification": "剪貼板和通知",
|
||||
"controlShow": "控制顯示的圖床",
|
||||
"copySuccess": "複製成功: {content}",
|
||||
"customLinkFormat": "自定義鏈接格式",
|
||||
"customLinkFormatDesc": "設置自定義的鏈接輸出格式",
|
||||
@@ -936,7 +937,7 @@
|
||||
"imageProcessing": "圖片預處理設置",
|
||||
"imageProcessingDesc": "配置上傳前的圖片預處理選項",
|
||||
"isAutoListenClipboard": "軟件啟動時自動監聽剪貼板上傳",
|
||||
"manualRname": "手動重命名",
|
||||
"manualRename": "手動重命名",
|
||||
"placeholder": {
|
||||
"categoryFile": "文件相關",
|
||||
"categoryHash": "哈希相關",
|
||||
@@ -968,7 +969,7 @@
|
||||
"shortUrlServer": "短鏈接服務",
|
||||
"sinkDomain": "Sink 域名",
|
||||
"sinkToken": "Sink Token",
|
||||
"timestampRname": "時間戳重命名",
|
||||
"timestampRename": "時間戳重命名",
|
||||
"title": "上傳",
|
||||
"uploadBehavior": "上傳行為",
|
||||
"uploadProcessing": "上傳處理",
|
||||
|
||||
@@ -83,31 +83,12 @@
|
||||
>
|
||||
<div class="mb-1 flex w-full flex-wrap items-start gap-3">
|
||||
<div class="filter-group">
|
||||
<label class="filter-label">{{ t('pages.gallery.picBedType') }}</label>
|
||||
<div class="custom-multiselect relative">
|
||||
<button
|
||||
class="flex h-[28px] w-full cursor-pointer items-center justify-between rounded-md border border-border-secondary px-2 py-1.5 text-sm leading-[1.4] text-main transition-all duration-fast ease-apple hover:border-accent-hover focus:[.active]:border-accent-hover focus:[.active]:shadow-sm"
|
||||
:class="{ active: picBedDropdownOpen }"
|
||||
@click="togglePicBedDropdown($event)"
|
||||
>
|
||||
<span v-if="choosedPicBed.length === 0">{{ t('pages.gallery.chooseShowedPicBed') }}</span>
|
||||
<span v-else>{{ choosedPicBed.length }} {{ t('pages.gallery.selected') }}</span>
|
||||
<ChevronDownIcon :size="16" />
|
||||
</button>
|
||||
<div
|
||||
v-show="picBedDropdownOpen"
|
||||
class="multiselect-dropdown shadow-lg; fixed z-1000 mt-[2px] no-scrollbar max-h-[280px] min-w-[185px] overflow-y-auto rounded-md border border-border-secondary bg-bg-tertiary px-2 py-1.5 text-main"
|
||||
>
|
||||
<label
|
||||
v-for="item in filteredPicBedG"
|
||||
:key="item.type"
|
||||
class="flex min-h-[unset] cursor-pointer items-center justify-between px-2 py-1 text-sm leading-[1.4] transition-all duration-fast ease-apple hover:bg-accent-hover"
|
||||
>
|
||||
<input v-model="choosedPicBed" type="checkbox" :value="item.type" class="m-0" />
|
||||
{{ item.name }}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<MultiSelect
|
||||
v-model:choosed="choosedPicBed"
|
||||
:title="t('pages.gallery.picBedType')"
|
||||
:zero-placeholder="t('pages.gallery.chooseShowedPicBed')"
|
||||
:all-list="filteredPicBedG"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="filter-group">
|
||||
@@ -120,55 +101,42 @@
|
||||
</div>
|
||||
|
||||
<div class="filter-group">
|
||||
<label class="filter-label">{{ t('pages.gallery.pasteFormat') }}</label>
|
||||
<select v-model="pasteStyle" class="custom-select" @change="handlePasteStyleChange">
|
||||
<option
|
||||
v-for="(value, key) in pasteStyleMap"
|
||||
:key="key"
|
||||
:value="value"
|
||||
class="bg-bg-tertiary text-sm text-main"
|
||||
>
|
||||
{{ key }}
|
||||
</option>
|
||||
</select>
|
||||
<SingleSelect
|
||||
v-model="pasteStyle"
|
||||
:title="t('pages.gallery.pasteFormat')"
|
||||
:fronticon="false"
|
||||
:key-list="pasteStyleList"
|
||||
>
|
||||
<template #item="{ item }">
|
||||
{{ item }}
|
||||
</template>
|
||||
</SingleSelect>
|
||||
</div>
|
||||
|
||||
<div class="filter-group">
|
||||
<label class="filter-label">{{ t('pages.gallery.urlType') }}</label>
|
||||
<select v-model="useShortUrl" class="custom-select" @change="handleUseShortUrlChange">
|
||||
<option
|
||||
v-for="(value, key) in shortURLMap"
|
||||
:key="key"
|
||||
:value="value"
|
||||
class="bg-bg-tertiary text-sm text-main"
|
||||
>
|
||||
{{ key }}
|
||||
</option>
|
||||
</select>
|
||||
<SingleSelect
|
||||
v-model="useShortUrl"
|
||||
:title="t('pages.gallery.urlType')"
|
||||
:fronticon="false"
|
||||
:key-list="shortURLList"
|
||||
>
|
||||
<template #item="{ item }">
|
||||
{{ item }}
|
||||
</template>
|
||||
</SingleSelect>
|
||||
</div>
|
||||
|
||||
<div class="filter-group">
|
||||
<label class="filter-label">{{ t('pages.gallery.sort') }}</label>
|
||||
<div class="sort-dropdown relative">
|
||||
<button class="sort-button" :class="{ active: sortDropdownOpen }" @click="toggleSortDropdown($event)">
|
||||
<SortAscIcon :size="14" />
|
||||
{{ t(`pages.gallery.sortBy.${currentSortField}`) }}
|
||||
<ChevronDownIcon :size="14" />
|
||||
</button>
|
||||
<div
|
||||
v-show="sortDropdownOpen"
|
||||
class="sort-options fixed z-10 mt-[2px] min-w-[150px] overflow-hidden rounded-md border border-border-secondary bg-bg-tertiary shadow-lg"
|
||||
>
|
||||
<button
|
||||
v-for="key in ['name', 'ext', 'time', 'check']"
|
||||
:key="key"
|
||||
class="block min-h-[unset] w-full cursor-pointer border-none bg-bg-tertiary px-2 py-1 text-center text-sm leading-[1.4] text-main transition-all duration-fast ease-apple hover:bg-accent-hover"
|
||||
@click="sortFile(key as any)"
|
||||
>
|
||||
{{ t(`pages.gallery.sortBy.${key}`) }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<SingleSelect
|
||||
v-model="currentSortField"
|
||||
:placeholder="t(`pages.gallery.sortBy.${currentSortField}`)"
|
||||
:title="t('pages.gallery.sort')"
|
||||
:key-list="['name', 'ext', 'time', 'check']"
|
||||
>
|
||||
<template #item="{ item }">
|
||||
{{ t(`pages.gallery.sortBy.${item}`) }}
|
||||
</template>
|
||||
</SingleSelect>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -240,7 +208,7 @@
|
||||
:key="componentKey"
|
||||
ref="virtualScrollerRef"
|
||||
:view-mode="viewMode"
|
||||
class="virtual-gallery-scroller min-h-0 w-full flex-1 p-1"
|
||||
class="virtual-gallery-scroller min-h-0 w-full flex-1 p-3"
|
||||
:items="filterList"
|
||||
:item-height="300"
|
||||
:grid-breakpoints="effectiveGridBreakpoints"
|
||||
@@ -596,7 +564,6 @@ import {
|
||||
ListIcon,
|
||||
RefreshCwIcon,
|
||||
SearchIcon,
|
||||
SortAscIcon,
|
||||
TrashIcon,
|
||||
XIcon,
|
||||
} from 'lucide-vue-next'
|
||||
@@ -615,6 +582,8 @@ import { useI18n } from 'vue-i18n'
|
||||
import { onBeforeRouteUpdate } from 'vue-router'
|
||||
|
||||
import ALLApi from '@/apis/allApi'
|
||||
import MultiSelect from '@/components/common/multiSelect.vue'
|
||||
import SingleSelect from '@/components/common/singleSelect.vue'
|
||||
import VirtualScroller from '@/components/VirtualScroller.vue'
|
||||
import useConfirm from '@/hooks/useConfirm'
|
||||
import { usePicBed } from '@/hooks/useGlobal'
|
||||
@@ -663,18 +632,10 @@ const debouncedSearchText = ref<string>('')
|
||||
const debouncedSearchTextURL = ref<string>('')
|
||||
const handleBarActive = useStorage<boolean>('galleryHandleBarActive', true)
|
||||
const pasteStyle = ref<string>('')
|
||||
const pasteStyleMap = {
|
||||
Markdown: 'markdown',
|
||||
HTML: 'HTML',
|
||||
URL: 'URL',
|
||||
UBB: 'UBB',
|
||||
Custom: 'Custom',
|
||||
}
|
||||
const pasteStyleList = ['markdown', 'HTML', 'URL', 'UBB', 'Custom']
|
||||
const useShortUrl = ref<string>('')
|
||||
const shortURLMap = {
|
||||
[t('pages.gallery.shortUrl')]: t('pages.gallery.shortUrl'),
|
||||
[t('pages.gallery.longUrl')]: t('pages.gallery.longUrl'),
|
||||
}
|
||||
const shortURLList = [t('pages.gallery.shortUrl'), t('pages.gallery.longUrl')]
|
||||
|
||||
const fileSortNameReverse = ref(false)
|
||||
const fileSortTimeReverse = ref(false)
|
||||
const fileSortExtReverse = ref(false)
|
||||
@@ -850,41 +811,6 @@ function onPreviewImageLoad() {
|
||||
})
|
||||
}
|
||||
|
||||
function togglePicBedDropdown(event?: Event) {
|
||||
picBedDropdownOpen.value = !picBedDropdownOpen.value
|
||||
if (sortDropdownOpen.value) sortDropdownOpen.value = false
|
||||
if (picBedDropdownOpen.value && event) {
|
||||
nextTick(() => {
|
||||
const trigger = event.target as HTMLElement
|
||||
const dropdown = trigger.parentElement?.querySelector('.multiselect-dropdown') as HTMLElement
|
||||
if (dropdown && trigger) {
|
||||
const rect = trigger.getBoundingClientRect()
|
||||
dropdown.style.top = `${rect.bottom + 2}px`
|
||||
dropdown.style.left = `${rect.left}px`
|
||||
dropdown.style.width = `${Math.max(rect.width, 200)}px`
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function toggleSortDropdown(event?: Event) {
|
||||
sortDropdownOpen.value = !sortDropdownOpen.value
|
||||
if (picBedDropdownOpen.value) picBedDropdownOpen.value = false
|
||||
|
||||
if (sortDropdownOpen.value && event) {
|
||||
nextTick(() => {
|
||||
const trigger = event.target as HTMLElement
|
||||
const dropdown = trigger.parentElement?.querySelector('.sort-options') as HTMLElement
|
||||
if (dropdown && trigger) {
|
||||
const rect = trigger.getBoundingClientRect()
|
||||
dropdown.style.top = `${rect.bottom + 2}px`
|
||||
dropdown.style.left = `${rect.left}px`
|
||||
dropdown.style.width = `${Math.max(rect.width, 160)}px`
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function navigateImage(direction: number) {
|
||||
const newIndex = gallerySliderControl.index + direction
|
||||
if (newIndex >= 0 && newIndex < filterList.value.length) {
|
||||
@@ -1522,23 +1448,19 @@ function toggleHandleBar() {
|
||||
handleBarActive.value = !handleBarActive.value
|
||||
}
|
||||
|
||||
async function handlePasteStyleChange(event: Event) {
|
||||
const target = event.target as HTMLSelectElement
|
||||
const val = target.value
|
||||
saveConfig(configPaths.settings.pasteStyle, val)
|
||||
pasteStyle.value = val
|
||||
}
|
||||
watch(pasteStyle, newVal => {
|
||||
saveConfig(configPaths.settings.pasteStyle, newVal)
|
||||
})
|
||||
|
||||
function handleUseShortUrlChange(event: Event) {
|
||||
const target = event.target as HTMLSelectElement
|
||||
const value = target.value
|
||||
saveConfig(configPaths.settings.useShortUrl, value === t('pages.gallery.shortUrl'))
|
||||
useShortUrl.value = value
|
||||
}
|
||||
watch(useShortUrl, newVal => {
|
||||
saveConfig(configPaths.settings.useShortUrl, newVal === t('pages.gallery.shortUrl'))
|
||||
})
|
||||
|
||||
watch(currentSortField, () => {
|
||||
sortFile(currentSortField.value)
|
||||
})
|
||||
|
||||
function sortFile(type: 'name' | 'time' | 'ext' | 'check') {
|
||||
sortDropdownOpen.value = false
|
||||
currentSortField.value = type
|
||||
switch (type) {
|
||||
case 'name':
|
||||
fileSortNameReverse.value = !fileSortNameReverse.value
|
||||
|
||||
@@ -22,8 +22,8 @@
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
import InputBoxDialog from '@/components/InputBoxDialog.vue'
|
||||
import Navigation from '@/components/NavigationPage.vue'
|
||||
import TitleBar from '@/components/ui/TitleBar.vue'
|
||||
import Navigation from '@/pages/NavigationPage.vue'
|
||||
import TitleBar from '@/pages/TitleBar.vue'
|
||||
|
||||
const $router = useRouter()
|
||||
const keepAlivePages = $router
|
||||
@@ -233,6 +233,8 @@ import { computed, nextTick, onBeforeMount, onBeforeUnmount, reactive, Ref, ref,
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
|
||||
import FirstTimeGuide from '@/components/FirstTimeGuide.vue'
|
||||
import ThemeSwitcher from '@/components/ui/ThemeSwitcher.vue'
|
||||
import { usePicBed } from '@/hooks/useGlobal'
|
||||
import useMessage from '@/hooks/useMessage'
|
||||
import * as config from '@/router/config'
|
||||
@@ -240,9 +242,6 @@ import { SHOW_FIRST_TIME_GUIDE, SHOW_MAIN_PAGE_QRCODE } from '@/utils/constant'
|
||||
import { getConfig } from '@/utils/dataSender'
|
||||
import { IRPCActionType } from '@/utils/enum'
|
||||
|
||||
import FirstTimeGuide from './FirstTimeGuide.vue'
|
||||
import ThemeSwitcher from './ui/ThemeSwitcher.vue'
|
||||
|
||||
const version = ref(pkg.version)
|
||||
const isCollapsed = useStorage('navigation-collapsed', false)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -85,8 +85,8 @@ onBeforeUnmount(() => {
|
||||
|
||||
<style scoped>
|
||||
@import 'tailwindcss' reference;
|
||||
@import '../../assets/css/theme.css' reference;
|
||||
@import '../../assets/css/utilities.css' reference;
|
||||
@import '../assets/css/theme.css' reference;
|
||||
@import '../assets/css/utilities.css' reference;
|
||||
|
||||
.control-button {
|
||||
@apply flex h-[20px] w-[28px] cursor-pointer items-center justify-center rounded-sm border-0 bg-transparent text-secondary transition-all duration-fast ease-standard hover:bg-surface-elevated hover:text-main [.close:hover]:bg-danger [.close:hover]:text-white [.mini:hover]:bg-success/85 [.mini:hover]:text-white [.minimize:hover]:bg-accent/85 [.minimize:hover]:text-white;
|
||||
@@ -20,22 +20,11 @@
|
||||
@apply mb-0 text-sm font-semibold text-secondary leading-[1.4];
|
||||
}
|
||||
|
||||
.custom-select {
|
||||
@apply border border-border-secondary rounded-md px-2 py-1.5 w-full min-w-0 h-[28px] text-sm text-main transition-all duration-fast ease-apple cursor-pointer text-center leading-[1.4];
|
||||
@apply focus:border-accent focus:outline-none focus:shadow-md;
|
||||
}
|
||||
|
||||
.date-input {
|
||||
@apply border border-border-secondary rounded-md px-2 py-1.5 min-w-[20px] h-[28px] text-xs text-main transition-all duration-fast ease-apple flex-1 leading-[1.2];
|
||||
@apply focus:border-accent-hover focus:outline-none focus:shadow-md;
|
||||
}
|
||||
|
||||
.sort-button {
|
||||
@apply flex justify-between items-center border border-border-secondary rounded-md px-2 py-1.5 w-full h-[28px] text-sm text-main transition-all duration-fast ease-apple cursor-pointer gap-1 leading-[1.4];
|
||||
@apply hover:border-accent-hover;
|
||||
@apply focus:[.active]:border-accent-hover focus:[.active]:shadow-md;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
@apply border border-border-secondary rounded-md pt-2 pr-3 pb-2 pl-9 w-full text-sm text-main transition-all duration-fast ease-apple;
|
||||
@apply focus:border-accent-hover focus:outline-none focus:shadow-md;
|
||||
|
||||
@@ -1,125 +1,8 @@
|
||||
@import url('./common/advancedAnimation.css');
|
||||
/* stylelint-disable selector-pseudo-class-no-unknown */
|
||||
.piclist-settings {
|
||||
overflow-y: auto;
|
||||
padding: 1.5rem;
|
||||
min-height: 100vh;
|
||||
color: var(--color-text-primary);
|
||||
background: var(--color-background-secondary);
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
}
|
||||
|
||||
.piclist-settings::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Header */
|
||||
.settings-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 1rem;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: 1rem;
|
||||
background: var(--color-background-secondary);
|
||||
box-shadow: 0 2px 8px rgb(0 0 0 / 10%);
|
||||
}
|
||||
|
||||
.header-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.header-icon {
|
||||
color: var(--color-accent);
|
||||
}
|
||||
|
||||
.settings-header h1 {
|
||||
margin: 0;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
/* Tab Navigation */
|
||||
.tab-navigation {
|
||||
display: flex;
|
||||
margin-bottom: 1rem;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: 0.5rem;
|
||||
background: var(--color-background-secondary);
|
||||
box-shadow: 0 2px 8px rgb(0 0 0 / 10%);
|
||||
}
|
||||
|
||||
.tab-button {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border: none;
|
||||
border-radius: var(--radius-md);
|
||||
padding: 0.75rem 1rem;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
color: var(--color-text-secondary);
|
||||
background: transparent;
|
||||
transition: all 0.2s ease;
|
||||
gap: 0.5rem;
|
||||
cursor: pointer;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.tab-button:hover {
|
||||
color: var(--color-text-primary);
|
||||
background: var(--color-background-primary);
|
||||
}
|
||||
|
||||
.tab-button.active {
|
||||
color: white;
|
||||
background: var(--color-accent);
|
||||
box-shadow: 0 2px 4px rgb(64 158 255 / 30%);
|
||||
}
|
||||
|
||||
/* Settings Content */
|
||||
.settings-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.settings-section {
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: 1.5rem;
|
||||
background: var(--color-background-secondary);
|
||||
box-shadow: 0 2px 8px var(--color-border);
|
||||
}
|
||||
|
||||
.settings-section h2 {
|
||||
margin: 0 0 0.5rem;
|
||||
font-size: 1.125rem;
|
||||
font-weight: 600;
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.settings-section p {
|
||||
margin: 0 0 1.5rem;
|
||||
font-size: 0.875rem;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
@import url('./common/advancedAnimation.css');
|
||||
@import 'tailwindcss' reference;
|
||||
@import '../../assets/css/theme.css' reference;
|
||||
@import '../../assets/css/utilities.css' reference;
|
||||
|
||||
/* Form Elements */
|
||||
.form-group {
|
||||
@@ -672,57 +555,6 @@ small {
|
||||
background: var(--color-accent-hover);
|
||||
}
|
||||
|
||||
.release-notes-loading,
|
||||
.release-notes-error,
|
||||
.release-notes-empty {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 2rem;
|
||||
font-size: 0.925rem;
|
||||
text-align: center;
|
||||
color: var(--color-text-secondary);
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.release-notes-error {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 3rem;
|
||||
color: var(--color-error);
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.release-notes-pre {
|
||||
margin: 0;
|
||||
border: none;
|
||||
padding: 1.5rem;
|
||||
font-size: 0.875rem;
|
||||
font-family: 'SF Mono', Monaco, Inconsolata, 'Roboto Mono', Consolas, monospace;
|
||||
white-space: pre-wrap;
|
||||
color: var(--color-text-primary);
|
||||
background: transparent;
|
||||
line-height: 1.6;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
.release-notes-footer {
|
||||
border-top: 1px solid var(--color-border);
|
||||
padding: 0.75rem 1.5rem;
|
||||
text-align: center;
|
||||
background: var(--color-background-secondary);
|
||||
}
|
||||
|
||||
.release-notes-footer small {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 0.75rem;
|
||||
color: var(--color-text-secondary);
|
||||
gap: 0.375rem;
|
||||
}
|
||||
|
||||
/* Rotation animation for loading icons */
|
||||
@keyframes rotate {
|
||||
from {
|
||||
@@ -1472,127 +1304,6 @@ small {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* Update Preferences */
|
||||
.update-preferences-section .update-preference-card {
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: 0.25rem;
|
||||
background: var(--color-background-secondary);
|
||||
}
|
||||
|
||||
.update-preferences-section .switch-label {
|
||||
margin: 0;
|
||||
border: none;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.release-notes-card.enhanced {
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-xl);
|
||||
background: var(--color-background-secondary);
|
||||
box-shadow: 0 4px 16px rgb(0 0 0 / 8%);
|
||||
}
|
||||
|
||||
.release-notes-card.enhanced .release-notes-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
padding: 1rem 1.5rem;
|
||||
background: var(--color-background-secondary);
|
||||
}
|
||||
|
||||
.release-notes-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.release-notes-title svg {
|
||||
color: var(--color-accent);
|
||||
}
|
||||
|
||||
.release-notes-title h3 {
|
||||
margin: 0;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.refresh-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.375rem;
|
||||
}
|
||||
|
||||
.release-notes-card.enhanced .release-notes-content {
|
||||
overflow-y: auto;
|
||||
max-height: 400px;
|
||||
background: var(--color-background-secondary);
|
||||
}
|
||||
|
||||
.release-notes-loading {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 3rem;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-radius: var(--radius-round);
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
color: var(--color-accent);
|
||||
background: rgb(64 158 255 / 12%);
|
||||
}
|
||||
|
||||
.release-notes-loading span {
|
||||
font-size: 0.875rem;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.error-icon {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.release-notes-error span {
|
||||
font-size: 0.875rem;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.release-notes-empty {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 3rem;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.release-notes-empty span {
|
||||
font-size: 0.875rem;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.release-notes-card.enhanced .release-notes-footer {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-top: 1px solid var(--color-border);
|
||||
padding: 0.75rem 1.5rem;
|
||||
background: var(--color-background-secondary);
|
||||
}
|
||||
|
||||
/* Responsive for Advanced & Update */
|
||||
@media (width <= 768px) {
|
||||
.advanced-action-grid {
|
||||
@@ -1746,11 +1457,7 @@ small {
|
||||
}
|
||||
|
||||
/* Theme Actions Grid */
|
||||
.theme-actions-grid {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
margin-top: 0.75rem;
|
||||
}
|
||||
|
||||
|
||||
.theme-action-btn {
|
||||
display: flex;
|
||||
@@ -2617,61 +2324,42 @@ small {
|
||||
}
|
||||
|
||||
.notes-body {
|
||||
overflow-y: auto;
|
||||
padding: 1.25rem;
|
||||
max-height: 195px;
|
||||
font-size: 0.9375rem;
|
||||
line-height: 1.5;
|
||||
background: var(--color-background-secondary);
|
||||
color: var(--color-text-secondary);
|
||||
@apply overflow-y-auto rounded-lg p-5 max-h-[200px] text-base leading-[1.5] bg-bg-tertiary text-secondary;
|
||||
}
|
||||
|
||||
.notes-body :deep(h1),
|
||||
.notes-body :deep(h2),
|
||||
.notes-body :deep(h3) {
|
||||
margin-top: 1.25rem;
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: 700;
|
||||
color: var(--color-text-primary);
|
||||
@apply font-bold text-main mt-5 mb-2;
|
||||
}
|
||||
|
||||
.notes-body :deep(h1:first-child),
|
||||
.notes-body :deep(h2:first-child),
|
||||
.notes-body :deep(h3:first-child) {
|
||||
margin-top: 0;
|
||||
@apply mt-0;
|
||||
}
|
||||
|
||||
.notes-body :deep(p) {
|
||||
margin-bottom: 0.875rem;
|
||||
@apply mb-2;
|
||||
}
|
||||
|
||||
.notes-body :deep(ul),
|
||||
.notes-body :deep(ol) {
|
||||
padding-left: 1.5rem;
|
||||
margin: 0.875rem 0;
|
||||
@apply list-inside my-3.5 pl-6;
|
||||
}
|
||||
|
||||
.notes-body :deep(li) {
|
||||
margin-bottom: 0.375rem;
|
||||
@apply mb-1.5;
|
||||
}
|
||||
|
||||
.notes-body :deep(code) {
|
||||
border-radius: 4px;
|
||||
padding: 0.125rem 0.375rem;
|
||||
font-size: 0.875em;
|
||||
background: var(--color-surface);
|
||||
color: var(--color-text-primary);
|
||||
.notes-body :deep(a) {
|
||||
@apply text-accent underline;
|
||||
}
|
||||
|
||||
.notes-body :deep(pre) {
|
||||
overflow-x: auto;
|
||||
border-radius: var(--radius-md);
|
||||
padding: 1rem;
|
||||
margin: 0.875rem 0;
|
||||
background: var(--color-surface);
|
||||
.notes-body :deep(a:hover) {
|
||||
@apply text-accent-hover;
|
||||
}
|
||||
|
||||
.notes-body :deep(pre code) {
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
.notes-body :deep(img) {
|
||||
@apply max-w-full rounded-md;
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { createRouter, createWebHashHistory } from 'vue-router'
|
||||
|
||||
import MainPage from '@/layouts/Main.vue'
|
||||
import ManageBucketPage from '@/manage/pages/BucketPage.vue'
|
||||
import ManageEmptyPage from '@/manage/pages/EmptyPage.vue'
|
||||
import ManageLoginPage from '@/manage/pages/LogInPage.vue'
|
||||
import ManageMainPage from '@/manage/pages/ManageMain.vue'
|
||||
import ManageSettingPage from '@/manage/pages/ManageSetting.vue'
|
||||
import GalleryPage from '@/pages/Gallery.vue'
|
||||
import MainPage from '@/pages/Main.vue'
|
||||
import MiniPage from '@/pages/MiniPage.vue'
|
||||
import PicBedsPage from '@/pages/picbeds/index.vue'
|
||||
import SettingPage from '@/pages/PicGoSetting.vue'
|
||||
|
||||
Reference in New Issue
Block a user